| 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]);
}
}
}
}