Overall Statistics |
Total Trades 1339 Average Win 0.87% Average Loss -0.13% Compounding Annual Return 10.971% Drawdown 26.900% Expectancy 1.369 Net Profit 214.528% Sharpe Ratio 0.587 Probabilistic Sharpe Ratio 4.030% Loss Rate 69% Win Rate 31% Profit-Loss Ratio 6.63 Alpha 0.004 Beta 0.732 Annual Standard Deviation 0.147 Annual Variance 0.022 Information Ratio -0.236 Tracking Error 0.112 Treynor Ratio 0.118 Total Fees $1930.34 Estimated Strategy Capacity $64000000.00 Lowest Capacity Asset ORCL R735QTJ8XC9X |
// Sigma Mean Reversion // -------------------- // Symbols which have a sudden change in momentum are likely to revert to historical momentum // // Measure historical 90d ROC { t -> (s[t] - s[t-90])/s[t-90] } // Use historical 90d ROC to predict future 90d ROC // When local (1-5d) ROC is extreme against expected future 90d ROC, anticipate reversion using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using QuantConnect; using QuantConnect.Algorithm; using QuantConnect.Data.Fundamental; using QuantConnect.Data.UniverseSelection; using QuantConnect.Indicators; using QuantConnect.Util; using Simpl.QCParams; namespace SigmaMR { public class SigmaMeanReversion : QCAlgorithm { readonly Dictionary<Symbol, SymbolData> SymbolData = new(); SymbolData.Parameters? _algoParams; decimal _leverage = 2m; public override void Initialize() { SetStartDate(2011, 1, 1); SetEndDate(2022, 1, 1); SetCash(100000); var p = new ParsersForAlgo(this); var leverage = p.Decimal("Leverage").FetchOrDie(); _algoParams = new SymbolData.Parameters( ExitAtMidpoint: p.BoolOfInt("MidpointExit").FetchOrDie(), RocPeriodDays: p.Int("ROCPeriodDays").FetchOrDie(), RocEstimationDays: p.Int("TrendEstPeriodDays").FetchOrDie(), RocSignalSmoothingDays: p.Int("SignalSmoothPeriodDays").FetchOrDie(), UpperExtremeSigmas: p.Decimal("UpperSigma").FetchOrDie(), LowerExtremeSigmas: p.Decimal("LowerSigma").FetchOrDie() ); UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw; UniverseSettings.Resolution = Resolution.Daily; AddUniverse(CoarseSelector, FineSelector); // var tickers = new[] { "XLK", "XLV", "XLP", "XLI", "XLU" }; // var tickers = new[] { "AAPL", "MSFT", "AMZN", "FB", "TSLA", "NVDA", "GOOG", "GOOGL", "AVGO", "ADBE" }; // foreach (var ticker in tickers) { // var equity = AddEquity(ticker, Resolution.Daily); // equity.SetDataNormalizationMode(DataNormalizationMode.Raw); // var data = new SymbolData(this, equity.Symbol, algoParams, allocation: leverage * 0.99m / tickers.Length); // SymbolData[equity.Symbol] = data; // } Schedule.On(DateRules.EveryDay(), TimeRules.Midnight, () => Portfolio.DoForEach(x => Plot("Profit", x.Key.ToString(), x.Value.Profit))); SetWarmup(_algoParams.WarmupDays, Resolution.Daily); } IEnumerable<Symbol> CoarseSelector(IEnumerable<CoarseFundamental> coarse) => (from sec in coarse where sec.HasFundamentalData where sec.Price > 10m orderby sec.DollarVolume descending select sec.Symbol).Take(500); IEnumerable<Symbol> FineSelector(IEnumerable<FineFundamental> fine) => (from sec in fine where sec.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.Technology orderby sec.MarketCap descending select sec.Symbol).Take(10); public override void OnSecuritiesChanged(SecurityChanges changes) { foreach (var sec in changes.AddedSecurities) { SymbolData[sec.Symbol] = new SymbolData(this, sec.Symbol, _algoParams!, allocation: _leverage * 0.99m / 10); } // foreach (var sec in changes.RemovedSecurities) { // /* // // ## Dont konw if I really want to REMOVE it, as it could be in an open position? // symbol = security.Symbol // if not self.Portfolio[symbol].Invested: #ONLY if flat (Dont remove if mid trade) // if symbol in self.SymbolData: // self.SymbolData[symbol].KillConsolidator() // self.SymbolData.pop(symbol) // // */ // } } } class SymbolData { readonly QCAlgorithm _algo; readonly Symbol _symbol; readonly decimal _allocation; readonly Parameters _parameters; readonly IndicatorBase[] _indicators; public record Parameters( int RocPeriodDays, int RocEstimationDays, int RocSignalSmoothingDays, bool ExitAtMidpoint, decimal UpperExtremeSigmas, decimal LowerExtremeSigmas ) { public int WarmupDays => RocPeriodDays + Math.Max(RocEstimationDays, RocSignalSmoothingDays); }; public IndicatorBase<IndicatorDataPoint> Momentum { get; } public IndicatorBase<IndicatorDataPoint> Signal { get; } public IndicatorBase<IndicatorDataPoint> StdDev { get; } public IndicatorBase<IndicatorDataPoint> BandMid { get; } public IndicatorBase<IndicatorDataPoint> BandUpper { get; } public IndicatorBase<IndicatorDataPoint> BandLower { get; } public bool LongEntry => Signal < BandLower; public bool ShortEntry => Signal > BandUpper; public bool LongExit => Signal > BandMid && _algo.Portfolio[_symbol].IsLong; public bool ShortExit => Signal < BandMid && _algo.Portfolio[_symbol].IsShort; public bool IsReady => _indicators.All(ind => ind.IsReady); public int WarmUpPeriodDays => _parameters.RocPeriodDays + Math.Max(_parameters.RocSignalSmoothingDays, _parameters.RocEstimationDays); public SymbolData(QCAlgorithm algo, Symbol symbol, Parameters parameters, decimal allocation) { _algo = algo; _symbol = symbol; _allocation = allocation; _parameters = parameters; Momentum = new RateOfChange(_symbol, _parameters.RocPeriodDays); Signal = new SimpleMovingAverage(_parameters.RocSignalSmoothingDays).Of(Momentum); StdDev = new StandardDeviation(_parameters.RocEstimationDays).Of(Momentum); BandMid = new SimpleMovingAverage(_parameters.RocEstimationDays).Of(Momentum); BandUpper = BandMid.Plus(StdDev.Times(_parameters.UpperExtremeSigmas)); BandLower = BandMid.Minus(StdDev.Times(_parameters.LowerExtremeSigmas)); _indicators = new IndicatorBase[] { Momentum, Signal, StdDev, BandMid, BandUpper, BandLower }; // All indicators are driven off of the core Momentum indicator, so it's the only one that needs to be plugged in _algo.RegisterIndicator(_symbol, Momentum, Resolution.Daily); // Schedule trading logic each day _algo.Schedule.On( name: $"Trade {_symbol}", dateRule: _algo.DateRules.EveryDay(_symbol), timeRule: _algo.TimeRules.BeforeMarketClose(_symbol, 10), callback: TradeLogic ); } void TradeLogic(string message, DateTime time) { _algo.Debug($"Trading on {_symbol} at {time}"); Entry(); Exit(); } public void Plot() { _algo?.Plot($"{_symbol} Data", "Raw", Momentum); _algo?.Plot($"{_symbol} Data", "Signal", Signal); _algo?.Plot($"{_symbol} Data", "Mid", BandMid); _algo?.Plot($"{_symbol} Data", "Upper", BandUpper); _algo?.Plot($"{_symbol} Data", "Lower", BandLower); } void Entry() { if (LongEntry) { _algo.Debug($"Long Entry ({_symbol}) -- Cross below lower band {Signal} < {BandLower}"); _algo.SetHoldings(_symbol, _allocation, tag: "MA Cross LE"); } else if (ShortEntry) { _algo.Debug($"Short Entry ({_symbol}) -- Cross above upper band {Signal} < {BandUpper}"); _algo.SetHoldings(_symbol, -_allocation, tag: "MA Cross SE"); } } void Exit() { if (!_parameters.ExitAtMidpoint) return; if (LongExit) { _algo.Debug($"Long Exit ({_symbol}) -- Cross above middle band when long {Signal} > {BandMid}"); _algo.Liquidate(_symbol, "MA Cross LX"); } if (ShortExit) { _algo.Debug($"Short Exit ({_symbol}) -- Cross below middle band when short {Signal} < {BandMid}"); _algo.Liquidate(_symbol, "MA Cross SX"); } } } }
using System; using QuantConnect.Algorithm; namespace Simpl.QCParams { /// <summary>A parsing function like <see cref="int.TryParse(string?, out int)"/>.</summary> /// <typeparam name="S">The type of the source value, often <see cref="string"/>.</typeparam> /// <typeparam name="T">The type of the result value.</typeparam> public delegate bool TryParseFunc<in S, T>(S msg, out T tgt); /// <summary> /// An object responsible for generating a value via sourcing it and enumerating one or more parsing steps. May fail and /// return a default value or throw an error. /// </summary> /// <typeparam name="A">The type this parse results in</typeparam> public interface IParser<A> { public string Root { get; } public A FetchVal(A orElse = default!) => Fetch(orElse).Value; public (bool Found, A Value) Fetch(A orElse = default!); public A FetchOrDie() { var (found, value) = Fetch(); if (!found) throw new Exception($"Could not find/parse parameter [{Root}]"); return value; } public IParser<B> Map<B>(Func<A, B> map) => new MappedImpl<A, B>(this, map); public IParser<B> Lift<B>(TryParseFunc<A, B> tryParseFunc) => new WithTryParse<A, B>(this, tryParseFunc); } // ------------------------------------------------------------------------ // private impls record MappedImpl<A, B>(IParser<A> First, Func<A, B> Map) : IParser<B> { string IParser<B>.Root => First.Root; public (bool Found, B Value) Fetch(B orElse) { var (found, value) = First.Fetch(); return found ? (true, Map(value)) : (false, orElse); } } record WithTryParse<S, T>(IParser<S> First, TryParseFunc<S, T> TryParseFunc) : IParser<T> { string IParser<T>.Root => First.Root; public (bool Found, T Value) Fetch(T orElse) { var (found, value) = First.Fetch(); if (!found) return (false, orElse); return TryParseFunc(value, out var toReturn) ? (true, toReturn) : (false, orElse); } } record RawImpl(QCAlgorithm Algo, string Name) : IParser<string> { string IParser<string>.Root => Name; public (bool Found, string Value) Fetch(string orElse) { var tmp = Algo.GetParameter(Name); return tmp is not null ? (true, tmp) : (false, orElse); } } }
using QuantConnect.Algorithm; namespace Simpl.QCParams { /// <summary> /// Helpers for creating <see cref="IParser{A}"/> values against a specified <see cref="QCAlgorithm"/>. /// </summary> /// <param name="Algo"></param> public record ParsersForAlgo(QCAlgorithm Algo) { public IParser<string> String(string name) => new RawImpl(Algo, name); public IParser<int> Int(string name) => String(name).Lift<int>(int.TryParse); public IParser<bool> BoolOfInt(string name) => Int(name).Lift<bool>(BoolFromInt); public IParser<decimal> Decimal(string name) => String(name).Lift<decimal>(decimal.TryParse); static bool BoolFromInt(int toParse, out bool parsed) { switch (toParse) { case 0: parsed = false; return true; case 1: parsed = true; return true; default: parsed = default; return false; } } } }