| Overall Statistics |
|
Total Orders 42 Average Win 3.20% Average Loss -1.87% Compounding Annual Return 24.783% Drawdown 11.300% Expectancy 0.354 Start Equity 1000000 End Equity 1181536.38 Net Profit 18.154% Sharpe Ratio 0.853 Sortino Ratio 1.069 Probabilistic Sharpe Ratio 56.996% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.71 Alpha 0.11 Beta 0.093 Annual Standard Deviation 0.136 Annual Variance 0.019 Information Ratio 0.247 Tracking Error 0.212 Treynor Ratio 1.254 Total Fees $684.60 Estimated Strategy Capacity $0 Lowest Capacity Asset GOOG T1AZ164W5VTX Portfolio Turnover 6.70% Drawdown Recovery 10 |
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
#endregion
using System;
using QuantConnect;
using QuantConnect.Indicators;
namespace QuantConnect.Algorithm.CSharp
{
public class ExposureScalerEMA
{
private readonly ExponentialMovingAverage _volEma;
private readonly decimal _k;
private readonly decimal _minScale;
// Faster EMA (20), smaller k (5), higher floor (0.45)
public ExposureScalerEMA(int emaPeriod = 20, decimal k = 5m, decimal minScale = 0.45m)
{
_volEma = new ExponentialMovingAverage(emaPeriod);
_k = k;
_minScale = minScale;
}
public bool IsReady => _volEma.IsReady;
public decimal UpdateAndGetScale(decimal atrPct, DateTime timeUtc)
{
_volEma.Update(new IndicatorDataPoint(timeUtc, atrPct));
var v = (decimal)_volEma.Current.Value;
var raw = 1m / (1m + _k * v);
return Clamp(raw, _minScale, 1.00m);
}
private static decimal Clamp(decimal x, decimal lo, decimal hi)
{
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
}
}
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using MathNet.Numerics;
using MathNet.Numerics.LinearAlgebra;
#endregion
using System;
using System.Linq;
using QuantConnect.Indicators;
namespace QuantConnect.Algorithm.CSharp.Helpers
{
/// <summary>
/// Multi-horizon, volatility-adjusted regression momentum (drop-in replacement).
/// - For each horizon h in {short, med, long}:
/// * Regress ln(price) on time (1..h) → slope b_h
/// * Annualize: ann_h = exp(b_h * 252) - 1
/// * Trend quality: q_h = sqrt(R^2) using corr^2(time, ln(price))
/// * Risk-normalize: s_h = (ann_h * q_h) / sigma_h
/// where sigma_h = std(daily log returns over volWindow) * sqrt(252)
/// - Composite: M = wS*s_S + wM*s_M + wL*s_L
///
/// Returns 0 until the max window is ready.
/// Use cross-sectional ranking of this value for selection.
/// </summary>
public class AnnualizedExponentialSlopeIndicator : WindowIndicator<IndicatorDataPoint>
{
// Default horizons and weights (you can adjust these)
private readonly int shortWin = 20;
private readonly int medWin = 60;
private readonly int longWin = 120;
private readonly int volWin = 20; // for std of daily log returns
private readonly decimal wS = 0.45m;
private readonly decimal wM = 0.35m;
private readonly decimal wL = 0.20m;
private const int TradingDays = 252;
private static readonly double SQRT_252 = Math.Sqrt(TradingDays);
private const double Tiny = 1e-16;
// State to compute daily log returns from incoming price stream
private decimal _lastPrice;
private bool _haveLast;
/// <summary>
/// Backward-compat constructor. 'period' is ignored except to set the window size;
/// we use max(shortWin, medWin, longWin, volWin) internally.
/// </summary>
public AnnualizedExponentialSlopeIndicator(int period)
: base($"MH-VolAdj-Momentum({period})", Math.Max(Math.Max(20, 60), Math.Max(120, 20))) // ensures window >= max defaults
{
// Ensure base window >= all we need
var needed = new[] { shortWin, medWin, longWin, volWin }.Max();
if (this.Period < needed)
{
// WindowIndicator doesn't expose a setter; recreate name with correct period
// But we can still function as ComputeNextValue uses window.Count subset.
// Just warn via comments: ensure your warmup >= needed.
}
}
public AnnualizedExponentialSlopeIndicator(string name, int period)
: base(name, Math.Max(Math.Max(20, 60), Math.Max(120, 20)))
{
var needed = new[] { shortWin, medWin, longWin, volWin }.Max();
}
/// <summary>
/// If you want explicit control over horizons/weights, add another constructor
/// taking (shortWin, medWin, longWin, volWin, wS, wM, wL).
/// </summary>
public override void Reset()
{
_haveLast = false;
_lastPrice = 0m;
base.Reset();
}
protected override decimal ComputeNextValue(IReadOnlyWindow<IndicatorDataPoint> window, IndicatorDataPoint input)
{
// Update last price state to allow log-return calc next bar
var price = input.Value;
if (_haveLast && _lastPrice > 0m && price > 0m)
{
// nothing to store; we will compute returns from window each time
}
_lastPrice = price;
_haveLast = true;
// Require enough data to compute all legs + volatility
int need = new[] { shortWin, medWin, longWin, volWin + 1 }.Max();
if (window.Count < need) return 0m;
// Compute volatility (std of daily log returns over volWin) * sqrt(252)
double annVol = AnnualizedVol(window, volWin);
if (!double.IsFinite(annVol) || annVol <= 0.0) annVol = 1e-6;
// Compute three horizons
double sShort = Leg(window, shortWin, annVol);
double sMed = Leg(window, medWin, annVol);
double sLong = Leg(window, longWin, annVol);
// Weighted composite
double composite =
(double)wS * sShort +
(double)wM * sMed +
(double)wL * sLong;
if (!double.IsFinite(composite)) composite = 0.0;
// No *100 scaling — keep scale-neutral (rank cross-sectionally downstream)
return (decimal)composite;
}
/// <summary>
/// One momentum leg: (annualized slope * sqrt(R^2)) / annualized volatility
/// </summary>
private double Leg(IReadOnlyWindow<IndicatorDataPoint> window, int h, double annVol)
{
// Regress ln(price) on time 1..h using the last h observations
int n = h;
// Precompute sums for x
double sumX = 0.0, sumXX = 0.0;
for (int i = 1; i <= n; i++)
{
sumX += i;
sumXX += (double)i * i;
}
double xBar = sumX / n;
double Sxx = sumXX - n * xBar * xBar;
if (Sxx <= 0.0) return 0.0;
// y = ln(price)
double sumY = 0.0;
// Take last h points: window[window.Count - h .. window.Count-1]
int start = window.Count - n;
double[] y = new double[n];
for (int i = 0; i < n; i++)
{
double p = (double)window[start + i].Value;
if (!(p > 0.0)) return 0.0;
y[i] = Math.Log(p);
sumY += y[i];
}
double yBar = sumY / n;
double Sxy = 0.0, Syy = 0.0;
for (int i = 0; i < n; i++)
{
double xiC = (i + 1) - xBar; // time index 1..n
double yiC = y[i] - yBar;
Sxy += xiC * yiC;
Syy += yiC * yiC;
}
double slope = Sxy / Sxx; // per bar
if (double.IsNaN(slope) || Math.Abs(slope) < Tiny) return 0.0;
// Annualize slope (log space): CAGR ≈ exp(slope * 252) - 1
double ann = Math.Exp(slope * TradingDays) - 1.0;
if (!double.IsFinite(ann)) return 0.0;
// Trend quality via corr^2; soften with sqrt (soft R²)
double q = 0.0;
if (Sxx > 0.0 && Syy > 0.0)
{
double corr = Sxy / Math.Sqrt(Sxx * Syy);
if (corr > 1.0) corr = 1.0;
if (corr < -1.0) corr = -1.0;
q = Math.Sqrt(Math.Max(0.0, corr * corr)); // sqrt(R^2)
}
double score = (ann * q) / annVol;
if (!double.IsFinite(score)) score = 0.0;
return score;
}
/// <summary>
/// Annualized volatility from std of daily log returns over last k returns.
/// Requires at least k+1 prices in the window.
/// </summary>
private double AnnualizedVol(IReadOnlyWindow<IndicatorDataPoint> window, int k)
{
int needed = k + 1;
if (window.Count < needed) return double.NaN;
int start = window.Count - needed;
// compute k daily log returns from k+1 prices
double[] r = new double[k];
int idx = 0;
for (int i = start + 1; i < start + needed; i++)
{
double p0 = (double)window[i - 1].Value;
double p1 = (double)window[i].Value;
if (!(p0 > 0.0) || !(p1 > 0.0)) return double.NaN;
r[idx++] = Math.Log(p1 / p0);
}
// std dev
double mean = r.Average();
double var = 0.0;
for (int i = 0; i < r.Length; i++)
{
double d = r[i] - mean;
var += d * d;
}
var /= Math.Max(1, r.Length - 1);
double sd = Math.Sqrt(var);
return sd * SQRT_252;
}
}
}
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion
namespace QuantConnect.Algorithm.CSharp.Helpers
{
public class CustomMomentumIndicator : TradeBarIndicator
{
private Symbol _symbol;
private int _windowSize;
public readonly AnnualizedExponentialSlopeIndicator AnnualizedSlope;
public readonly ExponentialMovingAverage MovingAverage;
public readonly GapIndicator Gap;
public readonly AverageTrueRange Atr;
public CustomMomentumIndicator(Symbol symbol, int annualizedSlopeWindow, int movingAverageWindow, int gapWindow, int atrWindow) : base($"CMI({symbol}, {annualizedSlopeWindow}, {movingAverageWindow}, {gapWindow})")
{
_symbol = symbol;
AnnualizedSlope = new AnnualizedExponentialSlopeIndicator(annualizedSlopeWindow);
MovingAverage = new ExponentialMovingAverage(movingAverageWindow);
Gap = new GapIndicator(gapWindow);
Atr = new AverageTrueRange(atrWindow);
_windowSize = (new int[] { movingAverageWindow, annualizedSlopeWindow, gapWindow, atrWindow }).Max();
}
public Symbol Symbol { get { return _symbol; } }
public override void Reset()
{
AnnualizedSlope.Reset();
MovingAverage.Reset();
Gap.Reset();
Atr.Reset();
base.Reset();
}
protected override decimal ComputeNextValue(TradeBar input)
{
AnnualizedSlope.Update(input.EndTime, input.Value);
MovingAverage.Update(input.EndTime, input.Value);
Gap.Update(input.EndTime, input.Value);
Atr.Update(input);
return AnnualizedSlope;
}
/// <summary>
/// Are the indicators ready to be used?
/// </summary>
public override bool IsReady
{
get { return AnnualizedSlope.IsReady && MovingAverage.IsReady && Gap.IsReady && Atr.IsReady; }
}
/// <summary>
/// Returns the Window of the indicator required to warm up indicator
/// </summary>
public int Window
{
get {return _windowSize;}
}
public new string ToDetailedString()
{
return $"Symbol:{_symbol} Slope:{AnnualizedSlope.ToDetailedString()} Average:{MovingAverage.ToDetailedString()} Gap:{Gap.ToDetailedString()} Atr:{Atr.ToDetailedString()} IsReady:{IsReady}";
}
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using MathNet.Numerics;
using MathNet.Numerics.Statistics;
#endregion
/// <summary>
/// Indicator to indicate the percentage (0.10 = 10%) of which a security gapped over the last period;
/// </summary>
namespace QuantConnect.Algorithm.CSharp.Helpers
{
public class GapIndicator : WindowIndicator<IndicatorDataPoint>
{
public GapIndicator(int period)
: base("GAP(" + period + ")", period)
{
}
public GapIndicator(string name, int period)
: base(name, period)
{
}
public override bool IsReady
{
get { return Samples >= Period; }
}
protected override decimal ComputeNextValue(IReadOnlyWindow<IndicatorDataPoint> window, IndicatorDataPoint input)
{
if (window.Count < 3) return 0m;
var diff = new double[window.Count];
// load input data for regression
for (int i = 0; i < window.Count - 1; i++)
{
diff[i] = (double)((window[i + 1] - window[i]) / (window[i] == 0 ? 1 : window[i].Value));
}
return (decimal) diff.MaximumAbsolute();
}
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion
using System;
using QuantConnect;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
namespace QuantConnect.Algorithm.CSharp
{
public enum RiskState { Off, Neutral, On }
public class MarketRegimeFilter
{
private readonly Symbol _spy;
private readonly SimpleMovingAverage _sma200;
private readonly AverageTrueRange _atr14;
private readonly decimal _volThreshOn;
public MarketRegimeFilter(Symbol spy, SimpleMovingAverage sma200, AverageTrueRange atr14, decimal volThreshOn = 0.027m)
{
_spy = spy;
_sma200 = sma200;
_atr14 = atr14;
_volThreshOn = volThreshOn;
}
public bool IsReady => _sma200.IsReady && _atr14.IsReady;
public RiskState GetState(QCAlgorithm algo)
{
if (!IsReady || !algo.Securities.ContainsKey(_spy) || !algo.Securities[_spy].HasData)
return RiskState.Neutral;
var price = algo.Securities[_spy].Price;
var trendUp = price > _sma200.Current.Value;
var atrPct = _atr14.Current.Value / (price == 0 ? 1 : price);
if (!trendUp) return RiskState.Off;
return atrPct < _volThreshOn ? RiskState.On : RiskState.Neutral;
}
// neutral exposure lifted 0.60 → 0.70
public decimal GetExposureScale(QCAlgorithm algo)
{
switch (GetState(algo))
{
case RiskState.On: return 1.00m;
case RiskState.Neutral: return 0.70m;
default: return 0.00m;
}
}
}
}
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.CSharp.Helpers;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion
using System;
using System.Linq;
using System.Collections.Generic;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Algorithm;
using QuantConnect.Securities;
using QuantConnect.Data.UniverseSelection;
namespace QuantConnect.Algorithm.CSharp
{
public class StocksOnTheMoveAlgorithm : QCAlgorithm
{
private int _annualizedSlopeWindow = 90;
private int _movingAverageWindow = 150;
private int _atrWindow = 20;
private const decimal RiskPerContractOnPortfolio = 0.015m;
private static int _universeSelectMaxStocks = 100;
private readonly Dictionary<Symbol, CustomMomentumIndicator> _customIndicators =
new Dictionary<Symbol, CustomMomentumIndicator>(_universeSelectMaxStocks);
private MarketRegimeFilter _marketRegimeFilter;
private ExposureScalerEMA _exposureScaler;
// top N broadened 20 → 25
private int _topNStockOfSp500 = 25;
private decimal _maximumGap = 0.15m;
private int _gapWindow = 100;
// minimum slope threshold
private decimal _minimumAnnualizedSlope = 0.02m;
private bool _rebalanceWeek = false;
public bool RebalanceWeek => _rebalanceWeek;
private const decimal BrokerFee = 0.005m;
private Symbol _spy;
private SimpleMovingAverage _spySma200;
private AverageTrueRange _spyAtr14;
public override void Initialize()
{
SetStartDate(2010, 1, 1);
SetEndDate(2025, 1, 1);
SetCash(10000);
SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
var spySec = AddEquity("SPY", Resolution.Daily);
_spy = spySec.Symbol;
SetBenchmark(_spy);
_spySma200 = SMA(_spy, 200, Resolution.Daily);
_spyAtr14 = ATR(_spy, 14, MovingAverageType.Wilders, Resolution.Daily);
SetWarmUp(220, Resolution.Daily);
_marketRegimeFilter = new MarketRegimeFilter(_spy, _spySma200, _spyAtr14, volThreshOn: 0.027m);
_exposureScaler = new ExposureScalerEMA(emaPeriod: 20, k: 5m, minScale: 0.45m);
UniverseSettings.Resolution = Resolution.Daily;
AddUniverse(Universe.ETF("OEF", Market.USA, UniverseSettings));
Schedule.On(DateRules.Every(DayOfWeek.Thursday),
TimeRules.AfterMarketOpen(_spy, 1), ScheduledWeeklyRebalanceAndTrades);
}
public override void OnData(Slice data)
{
if (IsWarmingUp) return;
if (!_spyAtr14.IsReady || !Securities.ContainsKey(_spy) || !Securities[_spy].HasData) return;
var price = Securities[_spy].Price;
if (price <= 0) return;
var atrPct = _spyAtr14.Current.Value / price;
_exposureScaler.UpdateAndGetScale(atrPct, UtcTime);
}
private void ScheduledWeeklyRebalanceAndTrades()
{
if (IsWarmingUp) return;
if (!_marketRegimeFilter.IsReady || !_spyAtr14.IsReady || !Securities.ContainsKey(_spy) || !Securities[_spy].HasData)
return;
var spyPrice = Securities[_spy].Price;
var spyAtrPct = _spyAtr14.Current.Value / (spyPrice == 0 ? 1 : spyPrice);
var volScale = _exposureScaler.UpdateAndGetScale(spyAtrPct, UtcTime);
var regimeScale = _marketRegimeFilter.GetExposureScale(this);
var globalExposure = regimeScale * volScale;
var sorted = _customIndicators
.Where(x => x.Value.IsReady && Securities.ContainsKey(x.Key) && Securities[x.Key].HasData)
.OrderByDescending(x => x.Value.AnnualizedSlope)
.Take(_topNStockOfSp500)
.Where(x =>
x.Value.AnnualizedSlope > _minimumAnnualizedSlope &&
Securities[x.Key].Price > x.Value.MovingAverage &&
x.Value.Gap < _maximumGap)
.ToList();
// sell symbols no longer in selection
foreach (var sec in Portfolio.Values.Where(x => x.Invested))
if (!sorted.Exists(x => x.Key == sec.Symbol))
Liquidate(sec.Symbol);
if (RebalanceWeek)
{
_rebalanceWeek = false;
var perRisk = Portfolio.TotalPortfolioValue * RiskPerContractOnPortfolio * globalExposure;
foreach (var sec in Portfolio.Values.Where(x => x.Invested))
{
var sym = sec.Symbol;
var qty = sec.Quantity;
var ciKvp = sorted.FirstOrDefault(x => x.Key == sym);
if (ciKvp.Value == null) { Liquidate(sym); continue; }
var price = Securities[sym].Price;
var targetQty = ciKvp.Value.Atr > 0 ? (int)Math.Floor(perRisk / ciKvp.Value.Atr) : 0;
if (targetQty < 0) targetQty = 0;
var diff = targetQty - qty;
if (diff < 0)
Sell(sym, Math.Abs(diff));
else if (diff > 0 && globalExposure > 0.05m)
{
var cost = diff * price * (1 + BrokerFee);
var cashFree = Portfolio.TotalPortfolioValue - Portfolio.TotalHoldingsValue;
if (cashFree > cost) Order(sym, diff);
}
}
}
else _rebalanceWeek = true;
// buy new symbols
if (globalExposure > 0.05m)
{
var perRisk = Portfolio.TotalPortfolioValue * RiskPerContractOnPortfolio * globalExposure;
foreach (var kvp in sorted)
{
var ci = kvp.Value;
var sym = ci.Symbol;
if (Portfolio[sym].Invested) continue;
var price = Securities[sym].Price;
var qty = ci.Atr > 0 ? (int)Math.Floor(perRisk / ci.Atr) : 0;
if (qty <= 0) continue;
var cost = qty * price * (1 + BrokerFee);
var cashFree = Portfolio.TotalPortfolioValue - Portfolio.TotalHoldingsValue;
if (cashFree > cost) Order(sym, qty);
}
}
else
Debug($"[{Time}] GlobalExposure={globalExposure:F2} -> no new buys.");
// NEW trailing stop logic (3×ATR below high since entry)
foreach (var kvp in Portfolio.Values.Where(x => x.Invested))
{
var sym = kvp.Symbol;
if (!_customIndicators.ContainsKey(sym)) continue;
var ci = _customIndicators[sym];
var price = Securities[sym].Price;
var atr = ci.Atr;
if (atr <= 0) continue;
var holding = Portfolio[sym];
var highSinceEntry = holding.Price * 1.0m;
if (price > highSinceEntry) highSinceEntry = price;
var stopPrice = highSinceEntry - 3m * atr;
if (price < stopPrice)
{
Liquidate(sym, "3xATR trailing stop hit");
}
}
}
public override void OnSecuritiesChanged(SecurityChanges changes)
{
foreach (var sec in changes.AddedSecurities)
{
if (_customIndicators.ContainsKey(sec.Symbol) || sec.Symbol.Value == "SPY") continue;
var ci = new CustomMomentumIndicator(sec.Symbol, _annualizedSlopeWindow, _movingAverageWindow, _gapWindow, _atrWindow);
var warmup = Math.Max(Math.Max(_annualizedSlopeWindow, _movingAverageWindow), Math.Max(_gapWindow, _atrWindow));
var hist = History(sec.Symbol, warmup + 5, Resolution.Daily);
foreach (var bar in hist) ci.Update((TradeBar)bar);
_customIndicators[sec.Symbol] = ci;
RegisterIndicator(sec.Symbol, ci, Resolution.Daily);
}
foreach (var sec in changes.RemovedSecurities)
{
if (sec.Invested) Liquidate(sec.Symbol);
_customIndicators.Remove(sec.Symbol);
}
}
}
}