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