| Overall Statistics |
|
Total Trades 444 Average Win 0.72% Average Loss -0.59% Compounding Annual Return 17.818% Drawdown 20.700% Expectancy 0.595 Net Profit 109.357% Sharpe Ratio 1.257 Loss Rate 28% Win Rate 72% Profit-Loss Ratio 1.22 Alpha 0.021 Beta 0.85 Annual Standard Deviation 0.137 Annual Variance 0.019 Information Ratio -0.104 Tracking Error 0.059 Treynor Ratio 0.203 Total Fees $536.43 |
namespace QuantConnect
{
/*
* QuantConnect University: Futures Example
*
* QuantConnect allows importing generic data sources! This example demonstrates importing a futures
* data from the popular open data source Quandl.
*
* QuantConnect has a special deal with Quandl giving you access to Stevens Continuous Futurs (SCF) for free.
* If you'd like to download SCF for local backtesting, you can download it through Quandl.com.
*/
public class DualMomentumSectorRotation : QCAlgorithm
{
// we'll use this to tell us when the month has ended
DateTime LastRotationTime = DateTime.MinValue;
TimeSpan RotationInterval = TimeSpan.FromDays(30);
List<string> GEMSymbols = new List<string>
{
"SPY",
"BIL",
"AGG"
};
// these are the growth symbols we'll rotate through
List<string> SectorSymbols = new List<string>
{
"XLV", //healthcare
"XLK", //technology
"XLI", //industrial
"XLU", //utilities
"XLF", //financials
"XLY", //consumerdisc
"XLP", //consumerstap
"XLB", //basic materials
"XLE", // energy
"PSR", //real estate
"IYZ", // communications
};
// we'll hold some computed data in these guys
// List<SymbolData> SectorSymbolData = new List<SymbolData>();
Dictionary<string, SymbolData> SectorSymbolData = new Dictionary<string, SymbolData>();
Dictionary<string, SymbolData> GEMSymbolData = new Dictionary<string, SymbolData>();
List<string> strongSectors = new List<string>();
public override void Initialize()
{
//SetStartDate(2003, 7, 1);
SetStartDate(2010, 7, 1);
SetEndDate(2015, 1, 1);
SetCash(25000);
// define our daily trade bar consolidator. we can access the daily bar
// from the DataConsolidated events
var dailyConsolidator = new TradeBarConsolidator(TimeSpan.FromDays(1));
// attach our event handler. the event handler is a function that will be called each time we produce
// a new consolidated piece of data.
dailyConsolidator.DataConsolidated += OnDataDaily;
foreach (var symbol in SectorSymbols)
{
AddSecurity(SecurityType.Equity, symbol, Resolution.Minute);
Securities[symbol].SetDataNormalizationMode(DataNormalizationMode.TotalReturn);
Securities[symbol].SetLeverage(1);
var momentum = MOMP(symbol, 252, Resolution.Daily);
var ma = SMA(symbol, 252/12*10, Resolution.Daily); // 10-month Simple Moving Average
var close = SMA(symbol, 1, Resolution.Daily); // Store the most recent close
SubscriptionManager.AddConsolidator(symbol, dailyConsolidator);
SectorSymbolData.Add(symbol, new SymbolData
{
Symbol = symbol,
MomScore = momentum,
MovingAvg = ma,
Close = close
});
}
foreach (var symbol in GEMSymbols)
{
AddSecurity(SecurityType.Equity, symbol, Resolution.Minute);
Securities[symbol].SetDataNormalizationMode(DataNormalizationMode.TotalReturn);
Securities[symbol].SetLeverage(1);
var momentum = MOMP(symbol, 252, Resolution.Daily);
SubscriptionManager.AddConsolidator(symbol, dailyConsolidator);
GEMSymbolData.Add(symbol, new SymbolData
{
Symbol = symbol,
MomScore = momentum
});
}
}
private bool first = true;
//Data Event Handler: New data arrives here. "TradeBars" type is a dictionary of strings so you can access it by symbol.
private void OnData(TradeBars data)
{
var _momSPY = GEMSymbolData["SPY"].MomScore;
var _momTbill = GEMSymbolData["BIL"].MomScore;
var Bonds = GEMSymbolData["AGG"].Symbol;
if (first)
{
first = false;
LastRotationTime = Time;
return;
}
}
private void OnDataDaily(object sender, TradeBar consolidated)
{
var _momSPY = GEMSymbolData["SPY"].MomScore;
var _momTbill = GEMSymbolData["BIL"].MomScore;
var Bonds = GEMSymbolData["AGG"].Symbol;
List<string> longSectors1 = new List<string>();
List<string> longSectors2 = new List<string>();
decimal holdingPercent = 1m;
decimal bondHoldings = 1m;
if (!_momSPY.IsReady)
{
if (Portfolio["SPY"].Quantity == 0)
{
SetHoldings("SPY", holdingPercent);
}
}
if (first)
{
first = false;
LastRotationTime = consolidated.Time;
return;
}
var delta = Time.Subtract(LastRotationTime);
// Check whether we have exceeded the rotation interval, time to assess rotation
if (delta > RotationInterval)
{
// Easy rebalance for backtest
foreach(string symbol in Securities.Keys)
{
Log("Liquidating " + Portfolio[symbol].Quantity + "of " + symbol);
Liquidate(symbol);
}
LastRotationTime = Time;
List<SymbolData> sectorSymbolDataList = new List<SymbolData>();
foreach (var x in SectorSymbolData) sectorSymbolDataList.Add(x.Value);
var orderedMomScores = sectorSymbolDataList.OrderByDescending(x => x.MomScore.Current.Value).ToList();
int numberOfSectors = 4;
for (int i = 0; i < numberOfSectors; i++)
{
strongSectors.Add(orderedMomScores[i].Symbol);
Log("Strong Symbol #" + i + "is " + orderedMomScores[i].Symbol);
}
foreach (var x in orderedMomScores)
{
Log(">>SCORE>>" + x.Symbol + ">>" + x.MomScore);
}
//
// Modify this section of code
// Loop through each ETF in strongSectors
// If TMOM and MA rules -> record ETF in longSectors1
// If TMOM rule only -> record ETF in longSectors2
// If MA rule only -> record ETF in longSectors2
// Set bondHoldings = 100%
// Loop through each ETF in longSectors1
// Allocate holdings to the ETF equivalent to 1 / NumberOfSectors
// Reduce bondHoldings percentage by 1 / NumberOfSectors
// Loop through each ETF in longSectors2
// Allocate holdings to the ETF equivalent to half of 1 / NumberOfSectors
// Reduce bondHoldings percentage by half of 1 / NumberOfSectors
// Allocate remaining holdings to bonds (percentage is indicated in bondHoldings variable)
// Loop through each ETF in strongSectors
foreach (string etf in strongSectors)
{
bool tmomRule = SectorSymbolData[etf].MomScore > _momTbill;
bool maRule = SectorSymbolData[etf].Close > SectorSymbolData[etf].MovingAvg;
if (tmomRule && maRule)
longSectors1.Add(etf);
if (tmomRule && !maRule)
longSectors2.Add(etf);
else if (!tmomRule && maRule)
longSectors2.Add(etf);
}
// Loop through each ETF in longSectors1
foreach (string etf in longSectors1)
{
SetHoldings(etf, holdingPercent * (1m / numberOfSectors));
bondHoldings -= (1m / numberOfSectors);
}
// Loop through each ETF in longSectors2
foreach (string etf in longSectors2)
{
SetHoldings(etf, holdingPercent * (0.5m / numberOfSectors));
bondHoldings -= (0.5m / numberOfSectors);
}
// Allocate remaining holdings to bonds
SetHoldings(Bonds, bondHoldings);
/* if (_momSPY < 0)
{
SetHoldings(Bonds, holdingPercent);
Log("Holding Percent is " + holdingPercent);
Log("Set Holdings to " + Portfolio[Bonds].Quantity + "of " + Bonds);
} else
{
foreach (var etf in strongSectors)
{
SetHoldings(etf, holdingPercent * (1m / numberOfSectors));
Log("Count of strongSectors is " + numberOfSectors);
}
}
*/
strongSectors.Clear();
}
}
public MMomentumPercent MMOMP(string symbol, int period, int periodexc, Resolution? resolution = null)
{
var mmomp = new MMomentumPercent(period, periodexc);
RegisterIndicator(symbol, mmomp, resolution);
return mmomp;
}
}
class SymbolData
{
public string Symbol;
public MomentumPercent MomScore { get; set; }
public SimpleMovingAverage MovingAvg { get; set; }
// Quick and dirty - store most recent close here
public SimpleMovingAverage Close { get; set; }
}
public class MMomentumPercent : WindowIndicator<IndicatorDataPoint>
{
private int periodexc = 21; // Number of most recent days to exclude from calculation
public MMomentumPercent(int period, int periodexc)
: base("MMOMP" + period, period)
{
}
protected override decimal ComputeNextValue(IReadOnlyWindow<IndicatorDataPoint> window, IndicatorDataPoint input)
{
if (window.Samples <= window.Size)
{
// keep returning the delta from the first item put in there to init
return (window[periodexc] - window[window.Count - 1]) / window[periodexc];
}
return (window[periodexc] - window.MostRecentlyRemoved) / window[periodexc];
}
}
}