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