Overall Statistics |
Total Trades 230 Average Win 4.18% Average Loss -1.54% Compounding Annual Return 24.885% Drawdown 15.700% Expectancy 1.775 Net Profit 1662.264% Sharpe Ratio 1.856 Probabilistic Sharpe Ratio 99.042% Loss Rate 25% Win Rate 75% Profit-Loss Ratio 2.72 Alpha 0.253 Beta 0.072 Annual Standard Deviation 0.141 Annual Variance 0.02 Information Ratio 0.602 Tracking Error 0.238 Treynor Ratio 3.652 Total Fees $4530.91 |
using MathNet.Numerics.LinearAlgebra; using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Data; using QuantConnect.Scheduling; using System; using System.Collections.Generic; using System.Linq; /// <summary> /// In and Out Strategy. /// Originally from Quantopian: https://www.quantopian.com/posts/new-strategy-in-and-out /// Continued on QuantConnect: https://www.quantconnect.com/forum/discussion/9597/the-in-amp-out-strategy-continued-from-quantopian /// C# v1.1: This C# version's seed is Tristan F's Python code v1.1, posted on 11/19/2020. /// </summary> namespace QuantConnect.Algorithm.CSharp.Jon.InAndOut { public class InAndOut001Algorithm : QCAlgorithm { private readonly IEnumerable<int> _momentumPeriods = Enumerable.Range(55, 11); private readonly int _historyDays = 252; private readonly int _waitDays = 15; private readonly double _percentile = 0.01d; private int _outDays = 0; private bool _isIn = true; private string[] _keyIndicatorTickers; private List<Symbol> _symbols = new List<Symbol>(); private IDictionary<string, decimal> _weights = new Dictionary<string, decimal>(); private IDictionary<string, decimal> _inWeights = new Dictionary<string, decimal>(); private IDictionary<string, decimal> _outWeights = new Dictionary<string, decimal>(); private IDictionary<string, decimal> _lastWeights = new Dictionary<string, decimal>(); public override void Initialize() { #region Backtest Parameters - these are ignored in live trading. SetStartDate(2008, 1, 1); //SetEndDate(2015, 1, 3); SetCash(100000); // Use the Alpha Streams Brokerage Model, developed in conjunction with // funds to model their actual fees, costs, etc. // Please do not add any additional reality modelling, such as Slippage, Fees, Buying Power, etc. //SetBrokerageModel(new AlphaStreamsBrokerageModel()); #endregion #region Assets var benchmarkTickers = new[] { "SPY" }; var inTickers = new[] { "QQQ" }; var outTickers = new[] { "TLT", "IEF" }; _keyIndicatorTickers = new[] { "DBB", "IGE", "SHY", "XLI" }; var modIndicatorTickers = new[] { "UUP", "SLV", "GLD", "XLU", "FXA", "FXF" }; var tickers = benchmarkTickers.Union(inTickers).Union(outTickers).Union(_keyIndicatorTickers).Union(modIndicatorTickers); foreach (var ticker in tickers) { _symbols.Add(AddEquity(ticker, Resolution.Minute).Symbol); } #endregion #region Initialize Weights var inOutTickers = inTickers.Union(outTickers); foreach (var inOutTicker in inOutTickers) { _weights[inOutTicker] = 0.0m; _inWeights[inOutTicker] = 0.0m; _outWeights[inOutTicker] = 0.0m; } foreach (var inTicker in inTickers) { _inWeights[inTicker] = 1.0m / inTickers.Count(); } foreach (var outTicker in outTickers) { _outWeights[outTicker] = 1.0m / outTickers.Count(); } _lastWeights = _weights; #endregion #region Schedule Functions var benchmark = _symbols.First(); Schedule.On(DateRules.EveryDay(benchmark), TimeRules.AfterMarketOpen(benchmark, 90), () => { RebalanceOut(); }); Schedule.On(DateRules.WeekEnd(benchmark), TimeRules.AfterMarketOpen(benchmark, 90), () => { RebalanceIn(); }); #endregion } private void RebalanceOut() { var history = History(_symbols, _historyDays, Resolution.Daily); var historicalMatrix = GetHistoricalMatrix(history); var shiftedMatrix = GetShiftedMatrix(historicalMatrix); var returnMatrix = historicalMatrix.PointwiseDivide(shiftedMatrix).Subtract(1); var keyMatrix = GetKeyMatrix(returnMatrix); var isExtreme = IsExtremeCondition(keyMatrix); var adjustedWaitDays = _waitDays; if (isExtreme) { _outDays = 0; _isIn = false; _weights = _outWeights; } if (_outDays >= adjustedWaitDays) { _isIn = true; } _outDays++; } private void RebalanceIn() { if (_isIn) { _weights = _inWeights; } } public override void OnData(Slice slice) { var weightsEqual = _weights.Count == _lastWeights.Count && !_weights.Except(_lastWeights).Any(); if (!weightsEqual) { var targets = new List<PortfolioTarget>(); foreach (var weight in _weights) { var symbol = _symbols.Find(s => s.Value == weight.Key); var target = new PortfolioTarget(symbol, weight.Value); targets.Add(target); } SetHoldings(targets); _lastWeights = _weights; } } private Matrix<double> GetHistoricalMatrix(IEnumerable<Slice> history) { var result = Matrix<double>.Build.Dense(_historyDays, _symbols.Count(), double.NaN); var rowIndex = 0; foreach (var slice in history) { var vector = new List<double>(); foreach (var symbol in _symbols) { if (slice.ContainsKey(symbol)) { vector.Add(decimal.ToDouble(slice[symbol].Close)); } else { vector.Add(double.NaN); } } result.SetRow(rowIndex++, vector.ToArray()); } return RemoveNans(result); } private Matrix<double> GetShiftedMatrix(Matrix<double> matrix) { var result = Matrix<double>.Build.Dense(matrix.RowCount, _symbols.Count()); foreach (var period in _momentumPeriods) { result = result.Add(Shift(matrix, period)); } return result.Map(m => m / _momentumPeriods.Count()); } private Matrix<double> GetKeyMatrix(Matrix<double> matrix) { var result = Matrix<double>.Build.Dense(matrix.RowCount, 1); foreach (var ticker in _keyIndicatorTickers) { var subMatrix = GetSubmatrix(matrix, ticker); result = result.Append(subMatrix); } var uup = GetSubmatrix(matrix, "UUP"); uup = uup.Map(m => -m); result = result.Append(uup); var slv = GetSubmatrix(matrix, "SLV"); var gld = GetSubmatrix(matrix, "GLD"); result = result.Append(slv.Subtract(gld)); var xli = GetSubmatrix(matrix, "XLI"); var xlu = GetSubmatrix(matrix, "XLU"); result = result.Append(xli.Subtract(xlu)); var fxa = GetSubmatrix(matrix, "FXA"); var fxf = GetSubmatrix(matrix, "FXF"); result = result.Append(fxa.Subtract(fxf)); return RemoveNans(result.RemoveColumn(0)); } private bool IsExtremeCondition(Matrix<double> matrix) { var lastRow = matrix.SubMatrix(matrix.RowCount - 1, 1, 0, matrix.ColumnCount); var list = new List<double>(); foreach (var column in matrix.EnumerateColumns()) { list.Add(GetPercentile(column.ToArray(), _percentile)); } var percentiles = Matrix<double>.Build.Dense(1, matrix.ColumnCount, list.ToArray()); // todo_jon: Here the percentiles are off by a bit. // Is it because the GetPercentile function is not the same as python's nanpercentile? // Is it due to differences in datatype rounding decimal vs double? // 3rd item (SHY): py = 0.00243602, c# = 0.00236798 // 5th item (UUP): py = -0.01071986, c# = -0.0108586 // To reproduce, use: // StartDate: 01/01/2008 // EndDate: 01/02/2008 // Debug(percentiles.ToString()); return percentiles.Subtract(lastRow).Exists(m => m > 0); } private Matrix<double> RemoveNans(Matrix<double> matrix) { if (matrix == null) { return null; } while (matrix.Find(n => n.Equals(double.NaN)) != null) { var v = matrix.Find(n => n.Equals(double.NaN)); matrix = matrix.RemoveRow(v.Item1); } return matrix; } private Matrix<double> Shift(Matrix<double> matrix, int shift) { matrix = matrix.SubMatrix(0, matrix.RowCount - shift, 0, matrix.ColumnCount); for (int i = 0; i < shift; i++) { matrix = matrix.InsertRow(i, Vector<double>.Build.Dense(matrix.ColumnCount, double.NaN)); } return matrix; } private Matrix<double> GetSubmatrix(Matrix<double> matrix, string ticker) { var symbol = _symbols.Find(s => s.Value == ticker); var index = _symbols.IndexOf(symbol); return matrix.SubMatrix(0, matrix.RowCount, index, 1); } private double GetPercentile(double[] sequence, double percentile) { Array.Sort(sequence); int count = sequence.Length; double n = (count - 1) * percentile + 1; // Another method: double n = (count + 1) * percentile; if (n == 1.0d) { return sequence[0]; } else if (n == count) { return sequence[count - 1]; } else { int k = (int)n; double d = n - k; return sequence[k - 1] + d * (sequence[k] - sequence[k - 1]); } } } }