| Overall Statistics |
|
Total Trades 954 Average Win 0.73% Average Loss -0.55% Compounding Annual Return 7.609% Drawdown 14.200% Expectancy 0.391 Net Profit 229.019% Sharpe Ratio 0.644 Probabilistic Sharpe Ratio 4.949% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 1.31 Alpha 0.067 Beta 0.011 Annual Standard Deviation 0.106 Annual Variance 0.011 Information Ratio -0.131 Tracking Error 0.203 Treynor Ratio 6.42 Total Fees $1011.39 Estimated Strategy Capacity $62000.00 |
namespace QuantConnect
{
using System;
using System.Net;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Parameters;
using QuantConnect.Data.Custom;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Parameters;
/// <summary>
/// ETF Global Rotation Strategy
/// </summary>
public class ETFReplay: QCAlgorithm
{
// we'll hold some computed data in these guys
List<SymbolData> SymbolData = new List<SymbolData>();
[Parameter("Keeps")]
public int Keeps = 3;
[Parameter("CashReservePct")]
public decimal CashReservePct = 0.010m;
[Parameter("WeightRa")]
public decimal WeightRa = 0.20m;
[Parameter("WeightRb")]
public decimal WeightRb = 0.50m;
[Parameter("WeightV")]
public decimal WeightV = 0.30m; // vol weight (decimal percentage)
[Parameter("FilterSMA")]
public int FilterSMA = 8; // MA filter, monthly length
[Parameter("LookbackRa")]
public string LookbackRa = "20D"; // return "a" lookback
[Parameter("LookbackRb")]
public string LookbackRb = "3M"; // return "b" lookback
[Parameter("LookbackV")]
public string LookbackV = "20D"; // volatility lookback
[Parameter("SymbolsUrl")]
public string Url = "";
[Parameter("RebalancePeriod")]
public string RebalancePeriod = "Daily";
List<string> GrowthSymbols = new List<String>{
// safety symbol
"SHY"
};
private bool first = true;
private DateTime LastRotationTime = DateTime.Now;
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
/// </summary>
public override void Initialize()
{
SetCash(10000);
SetStartDate(2005, 1, 1);
SetGrowthSymbols();
Debug("WeightRa: " + WeightRa);
Debug("WeightRb: " + WeightRb);
Debug("WeightV: " + WeightV);
Debug("SymbolUrl: " + Url);
Debug("RebalancePeriod: " + RebalancePeriod);
int periodADays = DaysFromLookback(LookbackRa);
int periodBDays = DaysFromLookback(LookbackRb);
int periodVolDays = DaysFromLookback(LookbackV);
Debug(String.Format("LookbackRa: {0} ({1} days)", LookbackRa, periodADays));
Debug(String.Format("LookbackRb: {0} ({1} days)", LookbackRb, periodBDays));
Debug(String.Format("LookbackV: {0} ({1} days)", LookbackV, periodVolDays));
foreach (var symbol in GrowthSymbols)
{
Debug("adding symbol to universe: " + symbol);
AddSecurity(SecurityType.Equity, symbol, Resolution.Daily);
// TODO - add parameters for lookback periods
var periodAPerf = MOMP(symbol, periodADays, Resolution.Daily);
var periodBPerf = MOMP(symbol, periodBDays, Resolution.Daily); // (252/12) * x months
// FIXME -The moving average is calculated using the closing price from the last day of each month(e.g. a 10 month moving average has 10 datapoints)
var filterSMA = SMA(symbol, 21*FilterSMA, Resolution.Daily);
// we use log returns instead of definition in replay
var std = new StandardDeviation(periodVolDays);
decimal annualizationFactor = (decimal)Math.Sqrt(252);
var logReturns = LOGR(symbol, periodVolDays, Resolution.Daily);
CompositeIndicator<IndicatorDataPoint> volatility = std.Of(logReturns).Times(annualizationFactor);
SymbolData.Add(new SymbolData
{
Symbol = symbol,
ReturnA = periodAPerf,
ReturnB = periodBPerf,
Volatility = volatility,
FilterSMA = filterSMA
});
}
// assumption we don't look back more than a year
SetWarmup(TimeSpan.FromDays(365));
}
// Convert the lookback strings to number of days (e.g. 20D->20, 2M->42)
public int DaysFromLookback (string period) {
string strAmount = period.Substring(0, period.Length - 1);
int amount = Int32.Parse(strAmount);
int multiplier = period.EndsWith("M") ? 21 : 1;
return multiplier * amount;
}
// Downloads the symbols from dropbox, file should contain each symbol on a new line and nothing else
private void SetGrowthSymbols() {
// using (var client = new WebClient())
// {
// // fetch the file from dropbox
// var file = client.DownloadString(Url);
// if (file.Length > 0) {
// string[] contents = file.Split(new string[] {"\n", "\r\n"}, StringSplitOptions.RemoveEmptyEntries);
// foreach(var symbol in contents){
// var cleaned = symbol.Trim();
// // skip comments
// if (cleaned.StartsWith("#")) continue;
// Debug("Adding to universe: " + cleaned);
// GrowthSymbols.Add(cleaned);
// }
// }
// }
// Alpha symbols
GrowthSymbols.Add("SPY");
GrowthSymbols.Add("DVY");
GrowthSymbols.Add("PFF");
GrowthSymbols.Add("MDY");
GrowthSymbols.Add("AGG");
GrowthSymbols.Add("IWM");
GrowthSymbols.Add("TBF");
GrowthSymbols.Add("IEF");
GrowthSymbols.Add("TIP");
GrowthSymbols.Add("EUM");
GrowthSymbols.Add("DBA");
GrowthSymbols.Add("EFA");
GrowthSymbols.Add("FXA");
GrowthSymbols.Add("EEM");
GrowthSymbols.Add("IDV");
GrowthSymbols.Add("FXE");
GrowthSymbols.Add("EFZ");
GrowthSymbols.Add("SH");
GrowthSymbols.Add("DBC");
}
private Dictionary<String, decimal> GetEtfReplayRankings()
{
// Rank according to ETF Replay rules (http://www.etfreplay.com/members/how_the_screener_works.aspx)
var orderedReturnA = SymbolData.OrderByDescending(x => x.ReturnA);
var orderedReturnB = SymbolData.OrderByDescending(x => x.ReturnB);
var orderedVol = SymbolData.OrderBy(x => x.Volatility);
var rankings = new Dictionary<String, decimal>();
// add all the symbols
foreach(var sd in SymbolData)
{
rankings[sd.Symbol] = 0.0m;
}
for(int i = 0; i < orderedReturnA.Count(); ++i)
{
var current = orderedReturnA.ElementAt(i);
rankings[current.Symbol] += i * WeightRa;
}
for(int i = 0; i < orderedReturnB.Count(); ++i)
{
var current = orderedReturnB.ElementAt(i);
rankings[current.Symbol] += i * WeightRb;
}
for(int i = 0; i < orderedVol.Count(); ++i)
{
var current = orderedVol.ElementAt(i);
rankings[current.Symbol] += i * WeightV;
}
return rankings;
}
public override void OnData(Slice data)
{
if (IsWarmingUp) return;
Log("OnData slice: date=" + data.Time + ", cnt=" + data.Count);
try
{
bool rebalance = false;
if (first)
{
rebalance = true;
first = false;
}else
{
// var delta = Time.Subtract(LastRotationTime);
// rebalance = delta > RotationInterval;
rebalance = LastRotationTime.Month != Time.Month;
if (RebalancePeriod == "Quarterly") {
// January - 1
// April - 4
// July - 7
// Oct - 10
rebalance = (rebalance &&
((Time.Month == 1) ||
(Time.Month == 4) ||
(Time.Month == 7) ||
(Time.Month == 10)));
}
}
if (rebalance)
{
LastRotationTime = Time;
// pick which one is best from growth and safety symbols
//var orderedObjScores = SymbolData.OrderByDescending(x => x.ObjectiveScore).ToList();
var rankings = GetEtfReplayRankings();
var orderedObjScores = SymbolData.OrderBy(x => rankings[x.Symbol]).ToList();
List<String> nextHoldings = new List<string>();
Debug("OrderedObjScores Count: " + orderedObjScores.Count());
for (int i = 0; i < Keeps; ++i)
{
var currentSymbolData = orderedObjScores[i];
var lastClose = Securities[currentSymbolData.Symbol].Close;
bool maFilter = lastClose > currentSymbolData.FilterSMA;
if (maFilter)
{
// this meets our investment criteria
nextHoldings.Add(currentSymbolData.Symbol);
}
// FIXME - allocate to "Safety" symbol, right now saftey symbol is part of the growth symbols so it should bubble up but that doesn't match replay
}
Log(">>NextPositions<<");
foreach(var position in nextHoldings)
{
Log("\t>>" + position);
}
if (nextHoldings.Count == 0)
{
// goto 100% cash
Log("LIQUIDATE>>CASH");
Liquidate();
}else {
foreach(var kvp in Portfolio.Securities.Values)
{
// liquidate anything we are currently invested in but not part of our next portfolio state
if (kvp.Invested && !nextHoldings.Contains(kvp.Symbol))
{
Log("LIQUIDATE>>" + kvp.Symbol);
Liquidate(kvp.Symbol);
}
}
decimal allocationPercentage = (1.0m - CashReservePct) / Keeps;
foreach(var symbol in nextHoldings)
{
Log("BUY>>" + symbol);
SetHoldings(symbol, allocationPercentage);
}
}
}
}
catch (Exception ex)
{
Error("OnData: " + ex.Message + "\r\n\r\n" + ex.StackTrace);
}
}
}
class SymbolData
{
public string Symbol;
public MomentumPercent ReturnA { get; set; }
public MomentumPercent ReturnB { get; set; }
public CompositeIndicator<IndicatorDataPoint> Volatility { get; set; }
public SimpleMovingAverage FilterSMA { get; set; }
}
}