Page 3 of 3

Re: The NY "Silver Bullet": How Smart Money Exploits the 10 AM Window

Posted: Mon Sep 21, 2026 10:59 pm
by PTScalper
Ctrader code version 2.0

Code: Select all

using System;
using System.IO;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.EasternStandardTime, AccessRights = AccessRights.File)]
    public class ProSilverBullet : Robot
    {
        #region Parameters (Configuration)
        
        [Parameter("Session Start (EST)", Group = "Session Constraints", DefaultValue = "10:00")]
        public string SessionStartStr { get; set; }

        [Parameter("Session End (EST)", Group = "Session Constraints", DefaultValue = "11:00")]
        public string SessionEndStr { get; set; }

        [Parameter("Max Spread (Pips)", Group = "Microstructure Filters", DefaultValue = 1.5, MinValue = 0.1, Step = 0.1)]
        public double MaxSpreadPips { get; set; }

        [Parameter("Risk / Reward Ratio", Group = "Execution Parameters", DefaultValue = 2.0, MinValue = 1.0, Step = 0.1)]
        public double RiskReward { get; set; }

        [Parameter("Risk % Per Trade", Group = "Execution Parameters", DefaultValue = 2.0, MinValue = 0.1, Step = 0.1)]
        public double RiskPercent { get; set; }

        [Parameter("Sweep Lookback (Bars)", Group = "Execution Parameters", DefaultValue = 10, MinValue = 3)]
        public int SwingLength { get; set; }

        [Parameter("Enable HTF Bias", Group = "Macro Filter", DefaultValue = true)]
        public bool UseHtfFilter { get; set; }

        [Parameter("HTF Timeframe", Group = "Macro Filter", DefaultValue = "Hour4")]
        public TimeFrame HtfTimeframe { get; set; }

        [Parameter("HTF EMA Length", Group = "Macro Filter", DefaultValue = 20, MinValue = 1)]
        public int HtfEmaLength { get; set; }
        
        #endregion

        #region Private State & Services

        private TimeSpan _sessionStart;
        private TimeSpan _sessionEnd;
        private SessionState _currentState;
        private readonly string _botLabel = "PRO_SILVER_BULLET";

        private Bars _htfBars;
        private ExponentialMovingAverage _htfEma;
        
        private enum SessionState
        {
            Offline,
            Scanning,
            TradeExecuted
        }

        #endregion

        #region Initialization

        protected override void OnStart()
        {
            if (!TimeSpan.TryParse(SessionStartStr, out _sessionStart) || !TimeSpan.TryParse(SessionEndStr, out _sessionEnd))
            {
                Print("🚨 CRITICAL: Invalid session time format. Use HH:MM.");
                Stop();
                return;
            }

            if (UseHtfFilter)
            {
                _htfBars = MarketData.GetBars(HtfTimeframe);
                _htfEma = Indicators.ExponentialMovingAverage(_htfBars.ClosePrices, HtfEmaLength);
            }

            _currentState = SessionState.Offline;
        }

        #endregion

        #region Core Evaluation Loop

        protected override void OnBar()
        {
            UpdateSessionState();

            // Guard clauses keep the execution thread clean
            if (_currentState != SessionState.Scanning || IsPositionActive())
                return;

            if (IsSpreadTooWide())
                return;

            EvaluateSetup();
        }

        private void UpdateSessionState()
        {
            TimeSpan currentTime = Server.Time.TimeOfDay;
            bool isInsideWindow = currentTime >= _sessionStart && currentTime < _sessionEnd;

            if (isInsideWindow && _currentState == SessionState.Offline)
            {
                _currentState = SessionState.Scanning;
            }
            else if (!isInsideWindow && _currentState != SessionState.Offline)
            {
                _currentState = SessionState.Offline;
                CancelPendingOrders();
            }
        }

        #endregion

        #region Market Structure Engine

        private void EvaluateSetup()
        {
            if (Bars.Count < SwingLength + 3) return;

            // 1. Evaluate Bias Provider
            bool isBullishBias = true;
            bool isBearishBias = true;

            if (UseHtfFilter)
            {
                int htfIndex = _htfBars.ClosePrices.Count - 2; // Hard-lock to the last closed H4 candle
                if (htfIndex >= 0)
                {
                    double htfClose = _htfBars.ClosePrices[htfIndex];
                    double htfEmaValue = _htfEma.Result[htfIndex];
                    
                    isBullishBias = htfClose > htfEmaValue;
                    isBearishBias = htfClose < htfEmaValue;
                }
            }

            // 2. Liquidity Sweep Detection
            double recentHigh = GetRecentHigh(SwingLength, 3);
            double recentLow  = GetRecentLow(SwingLength, 3);

            bool sweptHigh = Bars.HighPrices.Last(1) >= recentHigh || Bars.HighPrices.Last(0) >= recentHigh;
            bool sweptLow  = Bars.LowPrices.Last(1) <= recentLow || Bars.LowPrices.Last(0) <= recentLow;

            // 3. FVG Imbalance Detection
            bool isBullishFvg = Bars.LowPrices.Last(0) > Bars.HighPrices.Last(2) && Bars.ClosePrices.Last(1) > Bars.OpenPrices.Last(1);
            bool isBearishFvg = Bars.HighPrices.Last(0) < Bars.LowPrices.Last(2) && Bars.ClosePrices.Last(1) < Bars.OpenPrices.Last(1);

            // 4. Signal Routing
            if (sweptLow && isBullishFvg && isBullishBias)
            {
                double entryLimit = Bars.HighPrices.Last(2);
                double stopLoss = Math.Min(Bars.LowPrices.Last(0), Math.Min(Bars.LowPrices.Last(1), Bars.LowPrices.Last(2)));
                ExecuteOrder(TradeType.Buy, entryLimit, stopLoss);
            }
            else if (sweptHigh && isBearishFvg && isBearishBias)
            {
                double entryLimit = Bars.LowPrices.Last(2);
                double stopLoss = Math.Max(Bars.HighPrices.Last(0), Math.Max(Bars.HighPrices.Last(1), Bars.HighPrices.Last(2)));
                ExecuteOrder(TradeType.Sell, entryLimit, stopLoss);
            }
        }

        private double GetRecentHigh(int lookback, int startIndex)
        {
            double max = double.MinValue;
            for (int i = startIndex; i < startIndex + lookback; i++)
                max = Math.Max(max, Bars.HighPrices.Last(i));
            return max;
        }

        private double GetRecentLow(int lookback, int startIndex)
        {
            double min = double.MaxValue;
            for (int i = startIndex; i < startIndex + lookback; i++)
                min = Math.Min(min, Bars.LowPrices.Last(i));
            return min;
        }

        #endregion

        #region Execution & Risk Engine

        private void ExecuteOrder(TradeType tradeType, double entryLimit, double stopLoss)
        {
            double riskInPrice = Math.Abs(entryLimit - stopLoss);
            if (riskInPrice <= 0) return;

            double riskInPips = riskInPrice / Symbol.PipSize;
            double takeProfit = tradeType == TradeType.Buy 
                ? entryLimit + (riskInPrice * RiskReward) 
                : entryLimit - (riskInPrice * RiskReward);
            
            double tpInPips = Math.Abs(entryLimit - takeProfit) / Symbol.PipSize;

            double volume = CalculateRiskAdjustedVolume(riskInPips);
            if (volume <= 0) return;

            // Server payload normalization
            entryLimit = Symbol.NormalizePrice(entryLimit);
            
            TradeResult result = PlaceLimitOrder(tradeType, SymbolName, volume, entryLimit, _botLabel, riskInPips, tpInPips);
            
            if (result.IsSuccessful)
            {
                _currentState = SessionState.TradeExecuted;
                Print($"✅ Setup secured. Type: {tradeType} | Vol: {volume} | Risk: {riskInPips:F1} pips");
            }
            else
            {
                LogRejectionTelemetry(tradeType, entryLimit, stopLoss, takeProfit, riskInPips, result.Error);
            }
        }

        private double CalculateRiskAdjustedVolume(double riskInPips)
        {
            double riskAmount = Account.Balance * (RiskPercent / 100.0);
            double exactVolume = (riskAmount / (riskInPips * Symbol.PipValue)) * Symbol.VolumeInUnitsMin;
            
            return Symbol.NormalizeVolumeInUnits(exactVolume, RoundingMode.Down);
        }

        private bool IsSpreadTooWide()
        {
            double currentSpread = Symbol.Spread / Symbol.PipSize;
            if (currentSpread > MaxSpreadPips)
            {
                Print($"⚠️ Spread spike: {currentSpread:F1} pips exceeds threshold of {MaxSpreadPips}. Skipping evaluation.");
                return true;
            }
            return false;
        }

        #endregion

        #region Telemetry & Logging

        private void LogRejectionTelemetry(TradeType type, double entry, double sl, double tp, double slPips, ErrorCode? error)
        {
            if (error == null) return;

            string action = $"{type.ToString().ToUpper()}_LIMIT";
            double currentSpread = Symbol.Spread / Symbol.PipSize;

            string logMsg = string.Format(
                "[{0}] SYM: {1} | ACT: {2} | ENTRY: {3:F5} | SL: {4:F5} ({5:F1} pips) | TP: {6:F5} | SPREAD: {7:F1} pips | ERR: {8}",
                Server.Time.ToString("yyyy-MM-dd HH:mm:ss"), SymbolName, action, 
                entry, sl, slPips, tp, currentSpread, error.ToString()
            );

            // cTrader log severity routing
            PrintError($"❌ REJECTION: {logMsg}");

            try
            {
                string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "cAlgo", "SilverBullet_Telemetry.csv");
                Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                File.AppendAllText(filePath, logMsg + Environment.NewLine);
            }
            catch (IOException ex)
            {
                PrintWarning($"Disk write failed: {ex.Message}");
            }
        }

        #endregion

        #region Active State Utilities

        private bool IsPositionActive()
        {
            return Positions.Count(p => p.SymbolName == SymbolName && p.Label == _botLabel) > 0 ||
                   PendingOrders.Count(p => p.SymbolName == SymbolName && p.Label == _botLabel) > 0;
        }

        private void CancelPendingOrders()
        {
            foreach (var order in PendingOrders.Where(p => p.SymbolName == SymbolName && p.Label == _botLabel))
            {
                CancelPendingOrder(order);
            }
        }

        #endregion
    }
}