Overall Statistics
Total Trades
981
Average Win
1.07%
Average Loss
-0.12%
Compounding Annual Return
13.382%
Drawdown
30.700%
Expectancy
2.216
Net Profit
298.488%
Sharpe Ratio
0.601
Probabilistic Sharpe Ratio
4.236%
Loss Rate
69%
Win Rate
31%
Profit-Loss Ratio
9.29
Alpha
-0.006
Beta
1.022
Annual Standard Deviation
0.181
Annual Variance
0.033
Information Ratio
-0.036
Tracking Error
0.11
Treynor Ratio
0.106
Total Fees
$1240.20
Estimated Strategy Capacity
$81000000.00
Lowest Capacity Asset
BHP 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.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Scheduling;
using QuantConnect.Util;
using Simpl.QCParams;

namespace SigmaMR {
public class SigmaMeanReversion : QCAlgorithm {
  readonly Dictionary<Symbol, SymbolData> SymbolData = new();
  SymbolData.Parameters? _algoParams;
  
  decimal _leverage;
  bool _keepUniverse;
  HashSet<int> _sectorBlacklist = new() { MorningstarSectorCode.FinancialServices, MorningstarSectorCode.Energy };

  public override void Initialize() {
    SetStartDate(2011, 1, 1);
    SetEndDate(2022, 1, 1);
    SetCash(100000);

    var p = new ParsersForAlgo(this);

    _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);

    // Only do universe selection monthly
    Schedule.On(DateRules.MonthStart(), TimeRules.Midnight, () => _keepUniverse = false);

    Schedule.On(DateRules.EveryDay(), TimeRules.Midnight, () => Portfolio
      .Where(x => SymbolData.ContainsKey(x.Key))
      .DoForEach(x => Plot("Profit", x.Key.ToString(), x.Value.Profit)));

    SetWarmup(_algoParams.WarmupDays, Resolution.Daily);
  }

  IEnumerable<Symbol> CoarseSelector(IEnumerable<CoarseFundamental> coarse) {
    if (_keepUniverse) return Universe.Unchanged;

    return (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) {
    if (_keepUniverse) return Universe.Unchanged;
    _keepUniverse = true; // We've re-selected, hold it for a while now

    return (from sec in fine
      where !_sectorBlacklist.Contains(sec.AssetClassification.MorningstarSectorCode)
      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) {
      // todo: why would this ever be untrue?
      if (!SymbolData.ContainsKey(sec.Symbol)) continue;

      SymbolData[sec.Symbol].Stop();
      SymbolData.Remove(sec.Symbol);
    }
  }
}

class SymbolData {
  readonly QCAlgorithm _algo;
  readonly Symbol _symbol;
  readonly decimal _allocation;
  readonly Parameters _parameters;
  readonly IndicatorBase[] _indicators;
  readonly ScheduledEvent _tradeEvent;

  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
    _tradeEvent = _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();
  }

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

  public void Stop() {
    _tradeEvent.Enabled = false;
  }
}
}
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;
    }
  }
}
}