| Overall Statistics |
|
Total Trades 472 Average Win 3.40% Average Loss -0.61% Compounding Annual Return 29.766% Drawdown 19.400% Expectancy 2.376 Net Profit 3580.875% Sharpe Ratio 1.4 Probabilistic Sharpe Ratio 88.975% Loss Rate 49% Win Rate 51% Profit-Loss Ratio 5.58 Alpha 0.196 Beta 0.152 Annual Standard Deviation 0.15 Annual Variance 0.023 Information Ratio 0.564 Tracking Error 0.205 Treynor Ratio 1.386 Total Fees $16765.09 Estimated Strategy Capacity $380000.00 Lowest Capacity Asset TLH TP8J6Z7L419H |
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum
{
using System;
using QuantConnect.Algorithm.CSharp;
/// <summary>
/// Date utilities for the InOutMultiMomentum algorithm.
/// </summary>
public static class DateUtilities
{
/// <summary>
/// Adds the warmup time plus padding to a given <see cref="DateTime"/>.
/// </summary>
/// <param name="initialDate"><see cref="DateTime"/> to add warmup to.</param>
/// <returns><see cref="DateTime"/> with warm up time and padding added.</returns>
public static DateTime AddWarmUp(this DateTime initialDate) => initialDate.AddDays(InOutMultiMomentumAlgorithm.WarmupDays + 5);
}
}
using InOutMultiMomentum.UniverseSelection;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace InOutMultiMomentum
{
public class InOutAlphaModel : AlphaModel
{
public const int VolatilityPeriods = 126; // "VOLA"
private const int BaseReturn = 83; // "BASE_RET"
/// <summary>
/// Earliest possible date the algorithm can begin backtesting at.
/// </summary>
/// <remarks>
/// UUP began trading on 2-20-2017; we need to pad out to obtain sufficient data.
/// </remarks>
public readonly DateTime EarliestStartDate = new DateTime(2007, 2, 20).AddWarmUp();
/// <summary>
/// Time span after which alpha model state is considered invalid and discarded.
/// </summary>
private readonly TimeSpan OldestAlphaModelState = TimeSpan.FromDays(5);
private readonly string AlphaModelStateKeyName = $"{nameof(InOutAlphaModel)}-State";
private const int PredictionInterval = 30;
private const int GenerateAlphaAfterMarketOpenMinutes = 100; // 11:10 AM
private const string MarketTicker = "SPY";
private const string SilverTicker = "SLV";
private const string GoldTicker = "GLD";
private const string IndustrialsTicker = "XLI";
private const string UtilitiesTicker = "XLU";
private const string BaseMetalsTicker = "DBB";
private const string USDollarTicker = "UUP";
private readonly QCAlgorithm _algorithm;
private readonly Dictionary<Symbol, TradeBarConsolidator> _consolidators = new();
private readonly Dictionary<string, Symbol> _indicatorPairs = new();
private readonly PriceHistoryCollection _priceHistory;
private readonly Resolution _resolution;
private readonly TimeSpan _predictionInterval;
private readonly Dictionary<Symbol, SymbolData> _riskOnSymbolData = new();
private readonly Dictionary<Symbol, SymbolData> _riskOffSymbolData = new();
//private int _outDayCount = 0;
private bool _generateInsights = false;
public InOutAlphaModel(QCAlgorithm algorithm, Resolution resolution = Resolution.Daily)
{
_algorithm = algorithm;
_resolution = resolution;
var indicatorTickers = new[]
{
MarketTicker,
SilverTicker,
GoldTicker,
IndustrialsTicker,
UtilitiesTicker,
BaseMetalsTicker,
USDollarTicker
};
// Set up indicator pairs
foreach (var ticker in indicatorTickers)
{
var symbol = _algorithm.AddEquity(ticker, _resolution, Market.USA).Symbol;
_indicatorPairs.Add(ticker, symbol);
var consolidator = new TradeBarConsolidator(TimeSpan.FromDays(1));
consolidator.DataConsolidated += ConsolidationHandler;
_algorithm.SubscriptionManager.AddConsolidator(symbol, consolidator);
_consolidators.Add(symbol, consolidator);
}
// Load in history for indicator pairs
var history = _algorithm.History(_indicatorPairs.Values, VolatilityPeriods + 1, _resolution);
_priceHistory = new PriceHistoryCollection(history);
// Schedule this alpha to only produce insights at 11:10 AM.
_algorithm.Schedule.On(
_algorithm.DateRules.EveryDay(),
_algorithm.TimeRules.AfterMarketOpen(MarketTicker, GenerateAlphaAfterMarketOpenMinutes),
() => _generateInsights = true);
// Remaining setup
_predictionInterval = _resolution.ToTimeSpan().Multiply(PredictionInterval);
Name = $"{nameof(InOutAlphaModel)}({_resolution})";
}
~InOutAlphaModel()
{
foreach (var (symbol, consolidator) in _consolidators)
{
_algorithm.SubscriptionManager.RemoveConsolidator(symbol, consolidator);
}
}
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var added in changes.AddedSecurities)
{
var symbol = added.Symbol;
var ticker = symbol.Value;
if (!_riskOnSymbolData.ContainsKey(symbol) && !_riskOffSymbolData.ContainsKey(symbol))
{
var selectionModel = (InOutUniverseSelectionModel)algorithm.UniverseSelection;
var symbolData = new SymbolData(algorithm, symbol);
if (selectionModel.RiskOnTickers.Contains(ticker))
{
_riskOnSymbolData.Add(symbol, symbolData);
}
else if (selectionModel.RiskOffTickers.Contains(ticker))
{
_riskOffSymbolData.Add(symbol, symbolData);
}
}
}
foreach (var removed in changes.RemovedSecurities)
{
var symbol = removed.Symbol;
if (_riskOnSymbolData.ContainsKey(symbol))
{
_riskOnSymbolData.Remove(symbol);
}
if (_riskOffSymbolData.ContainsKey(symbol))
{
_riskOffSymbolData.Remove(symbol);
}
}
}
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
if (_algorithm.IsWarmingUp || !_generateInsights || _riskOnSymbolData.Count == 0 || _riskOffSymbolData.Count == 0)
{
yield break;
}
_generateInsights = false;
// Load alpha model state from object store
var state = LoadState(algorithm);
// Calculate alpha model signal
var signal = CalculateRiskOffSignal();
var selectRiskOnAssets = false;
if (signal.RiskOff)
{
state.OutDayCount = 0;
//_outDayCount = 0;
}
else
{
state.OutDayCount += 1;
//_outDayCount += 1;
//if (_outDayCount > signal.WaitDays)
if (state.OutDayCount > signal.WaitDays)
{
selectRiskOnAssets = true;
}
}
var selected = selectRiskOnAssets
? SelectAsset(_riskOnSymbolData.Values.ToList())
: SelectAsset(_riskOffSymbolData.Values.ToList());
foreach (var sd in _riskOnSymbolData.Concat(_riskOffSymbolData).ToDictionary(kvp => kvp.Key, kvp => kvp.Value).Values)
{
if (sd.Symbol == selected)
{
// Upwards-trending symbol
yield return Insight.Price(sd.Symbol, _predictionInterval, InsightDirection.Up);
}
else if ((selectRiskOnAssets && _riskOnSymbolData.ContainsKey(sd.Symbol)) ||
(!selectRiskOnAssets && _riskOffSymbolData.ContainsKey(sd.Symbol)))
{
// Flat-trending symbols
yield return Insight.Price(sd.Symbol, _predictionInterval, InsightDirection.Flat);
}
else
{
// Down-trending symbols
yield return Insight.Price(sd.Symbol, _predictionInterval, InsightDirection.Down);
}
}
// Save model state
state.Updated = algorithm.Time.Date;
SaveState(algorithm, state);
}
private IndicatorSignal CalculateRiskOffSignal()
{
var marketStandardDeviation = _priceHistory.StandardDeviation(_indicatorPairs[MarketTicker]);
var marketVolatility = marketStandardDeviation * Math.Sqrt(252);
var waitDays = Convert.ToInt32(Math.Floor(marketVolatility * BaseReturn));
var period = Convert.ToInt32(Math.Floor((1.0 - marketVolatility) * BaseReturn));
var returns = new Dictionary<string, double>();
foreach (var (ticker, symbol) in _indicatorPairs)
{
var percentChange = _priceHistory.PercentChange(symbol, period);
returns[ticker] = percentChange.Last();
}
var riskOff =
(returns[SilverTicker] < returns[GoldTicker]) &&
(returns[IndustrialsTicker] < returns[UtilitiesTicker]) &&
(returns[BaseMetalsTicker] < returns[USDollarTicker]);
return new IndicatorSignal
{
RiskOff = riskOff,
WaitDays = waitDays
};
}
private void ConsolidationHandler(object sender, TradeBar consolidated)
{
// Update daily close price and trim by volatility
_priceHistory.Update(consolidated);
_priceHistory.Trim(consolidated.Symbol, VolatilityPeriods + 1);
}
private AlphaModelState LoadState(QCAlgorithm algorithm)
{
if (algorithm.ObjectStore.ContainsKey(AlphaModelStateKeyName))
{
var state = algorithm.ObjectStore.ReadJson<AlphaModelState>(AlphaModelStateKeyName);
if (state != null)
{
if (algorithm.Time.Date <= (state.Updated + OldestAlphaModelState).Date)
{
algorithm.Debug($"Loaded alpha model state from {state.Updated:yyyy-MM-dd} (OutDays={state.OutDayCount})");
return state;
}
}
}
algorithm.Debug("Created new alpha model state.");
return new AlphaModelState();
}
private void SaveState(QCAlgorithm algorithm, AlphaModelState state)
{
algorithm.Debug($"Saved alpha model state from {state.Updated:yyyy-MM-dd} to object store");
algorithm.ObjectStore.SaveJson(AlphaModelStateKeyName, state);
}
private Symbol SelectAsset(List<SymbolData> assets)
{
var returns = new Dictionary<Symbol, decimal>();
//_algorithm.Debug($"Assets: {string.Join(", ", _riskOnSymbolData.Concat(_riskOffSymbolData).Select(kvp => kvp.Key).ToList())}");
foreach (var sd in assets)
{
//_algorithm.Debug($"Return: {sd.Symbol} = {sd.Returns}");
returns.Add(sd.Symbol, sd.Returns);
}
return returns.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value).First().Key;
}
public class SymbolData
{
private const int ReturnLookbackPeriods = 252; // "RET"
private const int ExclusionPeriods = 21; // "EXCL"
private readonly QCAlgorithm _algorithm;
public decimal Returns
{
get
{
var priceHistory = _algorithm.History<TradeBar>(Symbol, TimeSpan.FromDays(ReturnLookbackPeriods + ExclusionPeriods), Resolution.Daily);
var closePrices = priceHistory.Select(x => x.Close).ToList();
return closePrices[^ExclusionPeriods] / closePrices[0];
}
}
public Symbol Symbol { get; private set; }
public SymbolData(QCAlgorithm algorithm, Symbol symbol)
{
_algorithm = algorithm;
Symbol = symbol;
}
}
public class AlphaModelState
{
public DateTime Updated;
public int OutDayCount;
}
public class IndicatorSignal
{
public bool RiskOff;
public int WaitDays;
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using InOutMultiMomentum.UniverseSelection;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
/// <summary>
/// Portfolio construction model for the InOutMultiMomentum algorithm.
/// </summary>
public class InOutPortfolioConstructionModel : PortfolioConstructionModel
{
private readonly decimal riskOnLeverage;
private readonly decimal riskOffLeverage;
private readonly Dictionary<Symbol, SymbolData> riskOnSymbolData = new();
private readonly Dictionary<Symbol, SymbolData> riskOffSymbolData = new();
/// <summary>
/// Initializes a new instance of the <see cref="InOutPortfolioConstructionModel"/> class.
/// </summary>
/// <param name="riskOnLeverage">Leverage to use when the algorithm is in risk-on mode.</param>
/// <param name="riskOffLeverage">Leverage to use when the algorithm is in risk-off mode.</param>
public InOutPortfolioConstructionModel(decimal riskOnLeverage, decimal riskOffLeverage)
{
this.riskOnLeverage = riskOnLeverage;
this.riskOffLeverage = riskOffLeverage;
}
/// <inheritdoc/>
public override IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights)
{
if (algorithm.IsWarmingUp || this.riskOnSymbolData.Count == 0 || this.riskOffSymbolData.Count == 0)
{
yield break;
}
foreach (var insight in insights)
{
if (insight.Direction == InsightDirection.Up)
{
var leverage = this.riskOnSymbolData.ContainsKey(insight.Symbol)
? this.riskOnLeverage
: this.riskOffLeverage;
yield return PortfolioTarget.Percent(algorithm, insight.Symbol, leverage);
}
else
{
yield return PortfolioTarget.Percent(algorithm, insight.Symbol, 0.0m);
}
}
}
/// <inheritdoc/>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var added in changes.AddedSecurities)
{
var symbol = added.Symbol;
var ticker = symbol.Value;
if (!this.riskOnSymbolData.ContainsKey(symbol) && !this.riskOffSymbolData.ContainsKey(symbol))
{
var selectionModel = (InOutUniverseSelectionModel)algorithm.UniverseSelection;
var symbolData = new SymbolData(symbol);
if (selectionModel.RiskOnTickers.Contains(ticker))
{
this.riskOnSymbolData.Add(symbol, symbolData);
}
else if (selectionModel.RiskOffTickers.Contains(ticker))
{
this.riskOffSymbolData.Add(symbol, symbolData);
}
}
}
foreach (var removed in changes.RemovedSecurities)
{
var symbol = removed.Symbol;
if (this.riskOnSymbolData.ContainsKey(symbol))
{
_ = this.riskOnSymbolData.Remove(symbol);
}
if (this.riskOffSymbolData.ContainsKey(symbol))
{
_ = this.riskOffSymbolData.Remove(symbol);
}
}
}
/// <summary>
/// Symbol data for securities used by the algorithm.
/// </summary>
public class SymbolData
{
/// <summary>
/// Initializes a new instance of the <see cref="SymbolData"/> class.
/// </summary>
/// <param name="symbol"><see cref="Symbol"/> of this data object.</param>
public SymbolData(Symbol symbol) => this.Symbol = symbol;
/// <summary>
/// Gets the <see cref="Symbol"/> for this data object.
/// </summary>
public Symbol Symbol { get; }
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum
{
using System;
using System.Collections.Generic;
using System.Linq;
using MathNet.Numerics.Statistics;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Data.Market;
/// <summary>
/// Collection of price history used by the <see cref="InOutMultiMomentumAlphaModel"/>.
/// </summary>
public class PriceHistoryCollection
{
private readonly Dictionary<Symbol, SortedDictionary<DateTime, decimal>> priceHistory = new();
/// <summary>
/// Initializes a new instance of the <see cref="PriceHistoryCollection"/> class.
/// </summary>
/// <param name="initialHistory">Initial history retrieved for indicator pairs.</param>
public PriceHistoryCollection(IEnumerable<Slice> initialHistory)
{
var symbols = initialHistory.SelectMany(slice => slice.Keys)
.Distinct()
.ToList();
foreach (var symbol in symbols)
{
var symbolHistory = initialHistory.Get<TradeBar>(symbol);
foreach (var bar in symbolHistory)
{
this.Update(bar);
}
}
}
/// <summary>
/// Calculates the percentage of change between elements of the given period.
/// </summary>
/// <param name="symbol"><see cref="Symbol"/> for which to calculate from.</param>
/// <param name="period">Period of the comparison.</param>
/// <returns>Array of percentage value changes for the <paramref name="symbol"/> over the given <paramref name="period"/>.</returns>
public double[] PercentChange(Symbol symbol, int period = 1)
=> PercentChange(this.priceHistory[symbol].Values.ToDoubleArray(), period);
/// <summary>
/// Calculates the standard deviation of the percentage change for the given <paramref name="symbol"/>.
/// </summary>
/// <param name="symbol"><see cref="Symbol"/> for which to calculate.</param>
/// <returns>Standard deviation over the percentage change of the <paramref name="symbol"/>.</returns>
public double StandardDeviation(Symbol symbol)
{
var percentChangeSeries = this.PercentChange(symbol);
return percentChangeSeries.StandardDeviation();
}
/// <summary>
/// Trims price data for the indicated number of <paramref name="periods"/> of the given <paramref name="symbol"/>.
/// </summary>
/// <param name="symbol"><see cref="Symbol"/> to trim price data from.</param>
/// <param name="periods">Number of periods of data to trim.</param>
public void Trim(Symbol symbol, int periods)
{
var toKeep = this.priceHistory[symbol].Keys
.OrderBy(k => k)
.Skip(Math.Max(0, this.priceHistory[symbol].Count - periods))
.ToList();
var toRemove = this.priceHistory[symbol].Keys.Where(x => !toKeep.Contains(x)).ToList();
foreach (var key in toRemove)
{
_ = this.priceHistory[symbol].Remove(key);
}
}
/// <summary>
/// Updates price data.
/// </summary>
/// <param name="bar"><see cref="TradeBar"/> of data to collect price information from.</param>
public void Update(TradeBar bar)
{
var closePrice = bar.Close;
var endTime = bar.EndTime;
var symbol = bar.Symbol;
if (!this.priceHistory.ContainsKey(symbol))
{
this.priceHistory[symbol] = new SortedDictionary<DateTime, decimal>();
}
if (endTime != default && closePrice != default)
{
this.priceHistory[symbol][endTime] = closePrice;
}
}
/// <summary>
/// Calculates the percentage of change between elements of the given period.
/// </summary>
/// <param name="series">Price data series to calculate from.</param>
/// <param name="period">Period of the comparison.</param>
/// <returns>Array of percentage value changes over the given <paramref name="period"/>.</returns>
private static double[] PercentChange(double[] series, int period)
{
var returnSeries = new List<double>();
for (var i = period; i < series.Length; i++)
{
var b = series[i];
var a = series[i - period];
returnSeries.Add((b - a) / Math.Abs(a));
}
return returnSeries.ToArray();
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using QuantConnect;
using QuantConnect.Algorithm.CSharp;
using QuantConnect.Algorithm.Framework.Selection;
/// <summary>
/// Base selection model for any InOutMultiMomentum algorithm instance.
/// </summary>
public abstract class InOutUniverseSelectionModel : ManualUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="InOutUniverseSelectionModel"/> class.
/// </summary>
/// <param name="name">Name of this selection model.</param>
/// <param name="riskOnTickers">List of tickers to use when the algorithm is in risk-on mode.</param>
/// <param name="riskOffTickers">List of tickers ot use when the algorithm is in risk-off mode.</param>
public InOutUniverseSelectionModel(string name, Dictionary<string, DateTime> riskOnTickers, Dictionary<string, DateTime> riskOffTickers)
: base(riskOnTickers.Concat(riskOffTickers).Select(kvp => Symbol.Create(kvp.Key, SecurityType.Equity, Market.USA)).ToList())
{
this.Name = name;
this.RiskOnTickers = riskOnTickers.Keys.ToList();
this.RiskOffTickers = riskOffTickers.Keys.ToList();
this.StartDate = riskOnTickers.Concat(riskOffTickers)
.Select(kvp => kvp.Value).ToList()
.OrderByDescending(x => x.Date).First().AddWarmUp();
}
/// <summary>
/// Gets the name of this selection model.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Gets the earliest date this selection model may be backtested from.
/// </summary>
public DateTime StartDate { get; private set; }
/// <summary>
/// Gets the list of tickers to use when the algorithm is in risk-on mode.
/// </summary>
public List<string> RiskOnTickers { get; private set; }
/// <summary>
/// Gets the list of tickers to use when the algorithm is in risk-off mode.
/// </summary>
public List<string> RiskOffTickers { get; private set; }
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF3xMixedIndexUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF3xMixedIndexUniverse"/> class.
/// </summary>
public LETF3xMixedIndexUniverse()
: base(
nameof(LETF3xMixedIndexUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "TQQQ", new DateTime(2010, 2, 9) },
{ "UPRO", new DateTime(2009, 6, 25) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TMF", new DateTime(2009, 4, 16) },
{ "TYD", new DateTime(2009, 4, 16) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class UnleveredMixedIndexUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="UnleveredMixedIndexUniverse"/> class.
/// </summary>
public UnleveredMixedIndexUniverse()
: base(
nameof(UnleveredMixedIndexUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "QQQ", new DateTime(1999, 3, 10) },
{ "SPY", new DateTime(1993, 1, 22) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TLT", new DateTime(2002, 7, 22) },
{ "TLH", new DateTime(2007, 1, 5) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class UnleveredSP500Universe : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="UnleveredSP500Universe"/> class.
/// </summary>
public UnleveredSP500Universe()
: base(
nameof(UnleveredSP500Universe),
new Dictionary<string, DateTime>() // Risk on
{
{ "SPY", new DateTime(1993, 1, 22) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TLT", new DateTime(2002, 7, 22) },
{ "TLH", new DateTime(2007, 1, 5) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF2xMixedIndexUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF2xMixedIndexUniverse"/> class.
/// </summary>
public LETF2xMixedIndexUniverse()
: base(
nameof(LETF2xMixedIndexUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "QLD", new DateTime(2006, 6, 19) },
{ "SSO", new DateTime(2006, 6, 19) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "UBT", new DateTime(2010, 1, 19) },
{ "UST", new DateTime(2010, 1, 19) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF2xSP500Universe : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF2xSP500Universe"/> class.
/// </summary>
public LETF2xSP500Universe()
: base(
nameof(LETF2xSP500Universe),
new Dictionary<string, DateTime>() // Risk on
{
{ "SSO", new DateTime(2006, 6, 19) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "UBT", new DateTime(2010, 1, 19) },
{ "UST", new DateTime(2010, 1, 19) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF2xTechIndexUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF2xTechIndexUniverse"/> class.
/// </summary>
public LETF2xTechIndexUniverse()
: base(
nameof(LETF2xTechIndexUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "QLD", new DateTime(2006, 6, 19) },
{ "ROM", new DateTime(2007, 1, 30) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "UBT", new DateTime(2010, 1, 19) },
{ "UST", new DateTime(2010, 1, 19) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF3xTechIndex1xBondUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF3xTechIndex1xBondUniverse"/> class.
/// </summary>
public LETF3xTechIndex1xBondUniverse()
: base(
nameof(LETF3xTechIndex1xBondUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "TECL", new DateTime(2008, 12, 17) },
{ "TQQQ", new DateTime(2010, 2, 9) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "EDV", new DateTime(2007, 1, 5) },
{ "VGSH", new DateTime(2009, 11, 19) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF3xSP500Universe : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF3xSP500Universe"/> class.
/// </summary>
public LETF3xSP500Universe()
: base(
nameof(LETF3xSP500Universe),
new Dictionary<string, DateTime>() // Risk on
{
{ "UPRO", new DateTime(2009, 6, 25) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TMF", new DateTime(2009, 4, 16) },
{ "TYD", new DateTime(2009, 4, 16) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class LETF3xTechIndexUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="LETF3xTechIndexUniverse"/> class.
/// </summary>
public LETF3xTechIndexUniverse()
: base(
nameof(LETF3xTechIndexUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "TECL", new DateTime(2008, 12, 17) },
{ "TQQQ", new DateTime(2010, 2, 9) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TMF", new DateTime(2009, 4, 16) },
{ "TYD", new DateTime(2009, 4, 16) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class UnleveredTechIndexUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="UnleveredTechIndexUniverse"/> class.
/// </summary>
public UnleveredTechIndexUniverse()
: base(
nameof(UnleveredTechIndexUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "QQQ", new DateTime(1999, 3, 10) },
{ "FDN", new DateTime(2006, 6, 19) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TLT", new DateTime(2002, 7, 22) },
{ "TLH", new DateTime(2007, 1, 5) },
})
{
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace QuantConnect.Algorithm.CSharp
{
using System;
using System.Collections.Generic;
using System.Linq;
using InOutMultiMomentum;
using InOutMultiMomentum.UniverseSelection;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Brokerages;
using QuantConnect.Parameters;
/// <inheritdoc/>
#pragma warning disable SA1649
public class InOutMultiMomentumAlgorithm : QCAlgorithm
#pragma warning restore SA1649
{
/// <summary>
/// Indicates the number of days worth of data required to warm up the algorithm.
/// </summary>
public const int WarmupDays = 350;
/// <summary>
/// All available universe selection models.
/// </summary>
private readonly List<InOutUniverseSelectionModel> universeSelectionModels = new()
{
new LETF2xMixedIndexUniverse(),
new LETF2xSP500Universe(),
new LETF2xTechIndexUniverse(),
new LETF3xMixedIndexUniverse(),
new LETF3xSP500Universe(),
new LETF3xTechIndex1xBondUniverse(),
new LETF3xTechIndexUniverse(),
new NTSXUniverse(),
new UnleveredMixedIndexUniverse(),
new UnleveredSP500Universe(),
new UnleveredTechIndexUniverse(),
};
/// <summary>
/// Defines the desired starting date for backtesting.
/// </summary>
/// <remarks>
/// Note that if the backtest cannot start due to an indicator ETF not having data, this value is ignored.
/// </remarks>
public DateTime BacktestStartDate { get; set; } = new DateTime(2008, 1, 1);
/// <summary>
/// Defines the desired leverage when the algorithm is in "risk on" mode.
/// </summary>
[Parameter("RiskOnLeverage")]
public decimal RiskOnLeverage { get; set; }
/// <summary>
/// Defines the desired leverage when the algorithm is in "risk off" mode.
/// </summary>
[Parameter("RiskOffLeverage")]
public decimal RiskOffLeverage { get; set; }
/// <summary>
/// If set, defines which risk management model to use.
/// </summary>
[Parameter("RiskManagementModel")]
public string RiskManagementModel { get; set; }
/// <summary>
/// Defines the percentage parameter for the risk management model.
/// </summary>
[Parameter("RiskManagementModelPercent")]
public decimal RiskManagementModelPercent { get; set; }
/// <summary>
/// Defines which universe selection model to use.
/// </summary>
[Parameter("UniverseSelectionModel")]
public string UniverseSelectionModel { get; set; }
/// <inheritdoc/>
public override void Initialize()
{
var universeSelection = this.universeSelectionModels
.FirstOrDefault(x => x.Name == this.UniverseSelectionModel);
if (universeSelection == null)
{
throw new ArgumentOutOfRangeException($"Unrecognized universe selection model: {this.UniverseSelectionModel}");
}
var alpha = new InOutMultiMomentumAlphaModel(this);
var startDate = new List<DateTime>
{
universeSelection.StartDate,
alpha.EarliestStartDate,
this.BacktestStartDate,
}.OrderByDescending(x => x).First();
this.Debug($"Universe: {universeSelection.StartDate:yyyy-MM-dd}, Alpha: {alpha.EarliestStartDate:yyyy-MM-dd}, Backtest: {this.BacktestStartDate:yyyy-MM-dd}");
this.Debug($"Start date: {startDate:yyyy-MM-dd}");
this.SetCash(100000);
// this.SetEndDate(2020, 12, 31);
this.SetStartDate(startDate);
this.SetBenchmark("SPY");
this.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
this.SetWarmUp(TimeSpan.FromDays(WarmupDays));
this.UniverseSettings.Resolution = Resolution.Minute;
this.SetAlpha(new InOutMultiMomentumAlphaModel(this));
this.SetPortfolioConstruction(new InOutPortfolioConstructionModel(this.RiskOnLeverage, this.RiskOffLeverage));
this.SetUniverseSelection(universeSelection);
switch (this.RiskManagementModel)
{
case nameof(MaximumDrawdownPercentPerSecurity):
this.SetRiskManagement(new MaximumDrawdownPercentPerSecurity(this.RiskManagementModelPercent));
break;
case nameof(MaximumDrawdownPercentPortfolio):
this.SetRiskManagement(new MaximumDrawdownPercentPortfolio(this.RiskManagementModelPercent));
break;
case nameof(MaximumUnrealizedProfitPercentPerSecurity):
this.SetRiskManagement(new MaximumUnrealizedProfitPercentPerSecurity(this.RiskManagementModelPercent));
break;
case nameof(TrailingStopRiskManagementModel):
this.SetRiskManagement(new TrailingStopRiskManagementModel(this.RiskManagementModelPercent));
break;
default:
if (!string.IsNullOrWhiteSpace(this.RiskManagementModel))
{
throw new ArgumentOutOfRangeException($"Invalid risk management model: {this.RiskManagementModel}");
}
break;
}
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using InOutMultiMomentum.UniverseSelection;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
/// <summary>
/// InOutMultiMomentum alpha model.
/// </summary>
public class InOutMultiMomentumAlphaModel : AlphaModel
{
/// <summary>
/// Number of volatility periods.
/// </summary>
public const int VolatilityPeriods = 126; // "VOLA"
private const int BaseReturn = 83; // "BASE_RET"
private const int PredictionInterval = 30;
private const int GenerateAlphaAfterMarketOpenMinutes = 100; // 11:10 AM
private const string MarketTicker = "SPY";
private const string SilverTicker = "SLV";
private const string GoldTicker = "GLD";
private const string IndustrialsTicker = "XLI";
private const string UtilitiesTicker = "XLU";
private const string BaseMetalsTicker = "DBB";
private const string USDollarTicker = "UUP";
private readonly QCAlgorithm algorithm;
private readonly string alphaModelStateKeyName = $"{nameof(InOutMultiMomentumAlphaModel)}-State";
private readonly Dictionary<Symbol, TradeBarConsolidator> consolidators = new();
private readonly Dictionary<string, Symbol> indicatorPairs = new();
private readonly TimeSpan oldestAlphaModelState = TimeSpan.FromDays(5);
private readonly TimeSpan predictionInterval;
private readonly PriceHistoryCollection priceHistory;
private readonly Resolution resolution;
private readonly Dictionary<Symbol, SymbolData> riskOnSymbolData = new();
private readonly Dictionary<Symbol, SymbolData> riskOffSymbolData = new();
private bool generateInsights;
/// <summary>
/// Initializes a new instance of the <see cref="InOutMultiMomentumAlphaModel"/> class.
/// </summary>
/// <param name="algorithm">Algorithm executing this alpha model.</param>
/// <param name="resolution">Resolution at which to fetch historical data.</param>
public InOutMultiMomentumAlphaModel(QCAlgorithm algorithm, Resolution resolution = Resolution.Daily)
{
this.algorithm = algorithm;
this.resolution = resolution;
var indicatorTickers = new[]
{
MarketTicker,
SilverTicker,
GoldTicker,
IndustrialsTicker,
UtilitiesTicker,
BaseMetalsTicker,
USDollarTicker,
};
// Set up indicator pairs
foreach (var ticker in indicatorTickers)
{
var symbol = this.algorithm.AddEquity(ticker, this.resolution, Market.USA).Symbol;
this.indicatorPairs.Add(ticker, symbol);
var consolidator = new TradeBarConsolidator(TimeSpan.FromDays(1));
consolidator.DataConsolidated += this.ConsolidationHandler;
this.algorithm.SubscriptionManager.AddConsolidator(symbol, consolidator);
this.consolidators.Add(symbol, consolidator);
}
// Load in history for indicator pairs
var history = this.algorithm.History(this.indicatorPairs.Values, VolatilityPeriods + 1, this.resolution);
this.priceHistory = new PriceHistoryCollection(history);
// Schedule this alpha to only produce insights at 11:10 AM.
_ = this.algorithm.Schedule.On(
this.algorithm.DateRules.EveryDay(),
this.algorithm.TimeRules.AfterMarketOpen(MarketTicker, GenerateAlphaAfterMarketOpenMinutes),
() => this.generateInsights = true);
// Remaining setup
this.predictionInterval = this.resolution.ToTimeSpan().Multiply(PredictionInterval);
this.Name = $"{nameof(InOutMultiMomentumAlphaModel)}({this.resolution})";
}
/// <summary>
/// Finalizes an instance of the <see cref="InOutMultiMomentumAlphaModel"/> class.
/// </summary>
~InOutMultiMomentumAlphaModel()
{
foreach (var (symbol, consolidator) in this.consolidators)
{
this.algorithm.SubscriptionManager.RemoveConsolidator(symbol, consolidator);
}
}
/// <summary>
/// Earliest possible date the algorithm can begin backtesting at.
/// </summary>
/// <remarks>
/// UUP began trading on 2-20-2017; we need to pad out to obtain sufficient data.
/// </remarks>
public DateTime EarliestStartDate { get; } = new DateTime(2007, 2, 20).AddWarmUp();
/// <inheritdoc/>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var added in changes.AddedSecurities)
{
var symbol = added.Symbol;
var ticker = symbol.Value;
if (!this.riskOnSymbolData.ContainsKey(symbol) && !this.riskOffSymbolData.ContainsKey(symbol))
{
var selectionModel = (InOutUniverseSelectionModel)algorithm.UniverseSelection;
var symbolData = new SymbolData(algorithm, symbol);
if (selectionModel.RiskOnTickers.Contains(ticker))
{
this.riskOnSymbolData.Add(symbol, symbolData);
}
else if (selectionModel.RiskOffTickers.Contains(ticker))
{
this.riskOffSymbolData.Add(symbol, symbolData);
}
}
}
foreach (var removed in changes.RemovedSecurities)
{
var symbol = removed.Symbol;
if (this.riskOnSymbolData.ContainsKey(symbol))
{
_ = this.riskOnSymbolData.Remove(symbol);
}
if (this.riskOffSymbolData.ContainsKey(symbol))
{
_ = this.riskOffSymbolData.Remove(symbol);
}
}
}
/// <inheritdoc/>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
if (algorithm.IsWarmingUp || !this.generateInsights || this.riskOnSymbolData.Count == 0 || this.riskOffSymbolData.Count == 0)
{
yield break;
}
this.generateInsights = false;
// Load alpha model state from object store
var state = this.LoadState();
// Calculate alpha model signal
var signal = this.CalculateRiskOffSignal();
var selectRiskOnAssets = false;
if (signal.RiskOff)
{
state.OutDayCount = 0;
}
else
{
state.OutDayCount += 1;
if (state.OutDayCount > signal.WaitDays)
{
selectRiskOnAssets = true;
}
}
var selected = selectRiskOnAssets
? SelectAsset(this.riskOnSymbolData.Values.ToList())
: SelectAsset(this.riskOffSymbolData.Values.ToList());
foreach (var sd in this.riskOnSymbolData.Concat(this.riskOffSymbolData).ToDictionary(kvp => kvp.Key, kvp => kvp.Value).Values)
{
if (sd.Symbol == selected)
{
// Upwards-trending symbol
yield return Insight.Price(sd.Symbol, this.predictionInterval, InsightDirection.Up);
}
else if ((selectRiskOnAssets && this.riskOnSymbolData.ContainsKey(sd.Symbol)) ||
(!selectRiskOnAssets && this.riskOffSymbolData.ContainsKey(sd.Symbol)))
{
// Flat-trending symbols
yield return Insight.Price(sd.Symbol, this.predictionInterval, InsightDirection.Flat);
}
else
{
// Down-trending symbols
yield return Insight.Price(sd.Symbol, this.predictionInterval, InsightDirection.Down);
}
}
// Save model state
state.Updated = algorithm.Time.Date;
this.SaveState(state);
}
/// <summary>
/// Selects the asset with the highest momentum from a list of provided assets.
/// </summary>
/// <param name="assets">List of <see cref="SymbolData"/> to select an asset from.</param>
/// <returns><see cref="Symbol"/> with the highest momentum.</returns>
private static Symbol SelectAsset(List<SymbolData> assets)
{
if (assets.Count == 1)
{
return assets[0].Symbol;
}
var returns = new Dictionary<Symbol, decimal>();
foreach (var sd in assets)
{
returns.Add(sd.Symbol, sd.Returns);
}
return returns.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value).First().Key;
}
/// <summary>
/// Calculates if the algorithm should be risk-off.
/// </summary>
/// <returns><see cref="IndicatorSignal"/> data.</returns>
private IndicatorSignal CalculateRiskOffSignal()
{
var marketStandardDeviation = this.priceHistory.StandardDeviation(this.indicatorPairs[MarketTicker]);
var marketVolatility = marketStandardDeviation * Math.Sqrt(252);
var waitDays = Convert.ToInt32(Math.Floor(marketVolatility * BaseReturn));
var period = Convert.ToInt32(Math.Floor((1.0 - marketVolatility) * BaseReturn));
var returns = new Dictionary<string, double>();
foreach (var (ticker, symbol) in this.indicatorPairs)
{
var percentChange = this.priceHistory.PercentChange(symbol, period);
returns[ticker] = percentChange.Last();
}
var riskOff =
(returns[SilverTicker] < returns[GoldTicker]) &&
(returns[IndustrialsTicker] < returns[UtilitiesTicker]) &&
(returns[BaseMetalsTicker] < returns[USDollarTicker]);
return new IndicatorSignal
{
RiskOff = riskOff,
WaitDays = waitDays,
};
}
/// <summary>
/// Data consolidation event handler.
/// </summary>
/// <param name="sender">Source object.</param>
/// <param name="consolidated">Consolidated data.</param>
private void ConsolidationHandler(object sender, TradeBar consolidated)
{
// Update daily close price and trim by volatility
this.priceHistory.Update(consolidated);
this.priceHistory.Trim(consolidated.Symbol, VolatilityPeriods + 1);
}
/// <summary>
/// Loads the alpha model's state from the object store.
/// </summary>
/// <returns><see cref="AlphaModelState"/></returns>
private AlphaModelState LoadState()
{
if (this.algorithm.ObjectStore.ContainsKey(this.alphaModelStateKeyName))
{
var state = this.algorithm.ObjectStore.ReadJson<AlphaModelState>(this.alphaModelStateKeyName);
if (state != null)
{
if (this.algorithm.Time.Date <= (state.Updated + this.oldestAlphaModelState).Date)
{
return state;
}
}
}
return new AlphaModelState();
}
/// <summary>
/// Saves the indicated alpha model state to the object store.
/// </summary>
/// <param name="state"><see cref="AlphaModelState"/> to save to the object store.</param>
private void SaveState(AlphaModelState state) =>
_ = this.algorithm.ObjectStore.SaveJson(this.alphaModelStateKeyName, state);
/// <summary>
/// Symbol data for the securities used by the alpha model.
/// </summary>
public class SymbolData
{
private const int ReturnLookbackPeriods = 252; // "RET"
private const int ExclusionPeriods = 21; // "EXCL"
private readonly QCAlgorithm algorithm;
/// <summary>
/// Initializes a new instance of the <see cref="SymbolData"/> class.
/// </summary>
/// <param name="algorithm">Algorithm executing this alpha model.</param>
/// <param name="symbol"><see cref="Symbol"/> of this data object.</param>
public SymbolData(QCAlgorithm algorithm, Symbol symbol)
{
this.algorithm = algorithm;
this.Symbol = symbol;
}
/// <summary>
/// Gets the returns for this symbol.
/// </summary>
public decimal Returns
{
get
{
var priceHistory = this.algorithm.History<TradeBar>(this.Symbol, TimeSpan.FromDays(ReturnLookbackPeriods + ExclusionPeriods), Resolution.Daily);
var closePrices = priceHistory.Select(x => x.Close).ToList();
return closePrices[^ExclusionPeriods] / closePrices[0];
}
}
/// <summary>
/// Gets the <see cref="Symbol"/> for this data object.
/// </summary>
public Symbol Symbol { get; private set; }
}
/// <summary>
/// Alpha model state data.
/// </summary>
public class AlphaModelState
{
/// <summary>
/// Gets or sets the number of days the alpha model has been in risk-off mode.
/// </summary>
public int OutDayCount { get; set; }
/// <summary>
/// Gets or sets the last time the alpha model's state was updated.
/// </summary>
public DateTime Updated { get; set; }
}
/// <summary>
/// Indicator signal data.
/// </summary>
public class IndicatorSignal
{
/// <summary>
/// Gets or sets if the algorithm has calculated it should be in risk-off mode.
/// </summary>
public bool RiskOff { get; set; }
/// <summary>
/// Gets or sets the number of days the algorithm should be in risk-off mode.
/// </summary>
public int WaitDays { get; set; }
}
}
}
// --------------------------------------------------
// Copyright (c) John Cardenas. All rights reserved.
// --------------------------------------------------
namespace InOutMultiMomentum.UniverseSelection
{
using System;
using System.Collections.Generic;
/// <inheritdoc/>
public class NTSXUniverse : InOutUniverseSelectionModel
{
/// <summary>
/// Initializes a new instance of the <see cref="NTSXUniverse"/> class.
/// </summary>
public NTSXUniverse()
: base(
nameof(NTSXUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "NTSX", new DateTime(2018, 8, 02) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TLT", new DateTime(2002, 7, 22) },
{ "TLH", new DateTime(2007, 1, 5) },
})
{
}
}
}
using System;
using System.Collections.Generic;
namespace InOutMultiMomentum.UniverseSelection
{
public class NTSXBondUniverse : InOutUniverseSelectionModel
{
public NTSXBondUniverse() :
base(
nameof(NTSXBondUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "NTSX", new DateTime(2018, 8, 2) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "TMF", new DateTime(2009, 4, 16) },
{ "TYD", new DateTime(2009, 4, 16) },
}
)
{
}
}
}
using System;
using System.Collections.Generic;
namespace InOutMultiMomentum.UniverseSelection
{
public class NTSXSWANUniverse : InOutUniverseSelectionModel
{
public NTSXSWANUniverse() :
base(
nameof(NTSXSWANUniverse),
new Dictionary<string, DateTime>() // Risk on
{
{ "NTSX", new DateTime(2018, 8, 2) },
},
new Dictionary<string, DateTime>() // Risk off
{
{ "SWAN", new DateTime(2018, 11, 16) },
}
)
{
}
}
}