// ============================================================================
// P1FastCore.cs — pure C# port of pine/Winners/p1_fast_lucidpro_eval_25k.pine
// (P1 FAST METHOD: ORB or5 + rsi_gate=notOB + stop_pad 0.25 ATR + M1be exits,
//  grade AB take rule, one signal per direction per day, limit-at-OR-level
//  entry valid 30 minutes, hard flat 15:55 ET / 12:55 on CME early closes)
// PLUS the funded reBreak re-arm variant (Rearm = ReBreak, 2026-08-17; pine
// pine/Winners/Archieve/p1_fast_lucidpro_funded_rebreak.pine).
//
// This file has NO NinjaTrader dependencies. It is compiled into BOTH
// NinjaTrader 8 strategies (P1FastLucidEval25k.cs, P1FastLucidFunded25k.cs)
// AND into the macOS parity harness that verifies it trade-for-trade against
// the anchored engine reference books:
//   * eval   (Rearm Off,     $800/cap20): parity_refs_2026-08-02/
//            lucidpro_25k_eval.json  n=222, net $32,106.10
//   * funded (Rearm ReBreak, $325/cap20): DB variant 14767
//            lucidpro_r_ladder_books_full2019 / fund_rebreak|r325
//            n=722, net $4,578.94 (2019-05-10..2026-08-07)
// DO NOT EDIT trading logic here without re-running BOTH parity gates — this
// port follows the same referee discipline as the TV scripts (AGENTS.md §6).
//
// Port conventions (kept from the Pine source, line refs are to the .pine):
//  * One call to OnBar() == one closed 1m chart bar, evaluated at its CLOSE
//    (Pine executes the whole script at bar close).  The host feeds bars in
//    time order and never feeds a bar twice.
//  * Times are naive US/Eastern wall clock. Tmin = minutes since midnight ET
//    of the bar OPEN; ctMin = Tmin + 1 = bar close; DayId = yyyymmdd;
//    AbsMin = minutes since 0001-01-01 of the bar OPEN (Pine's `time` in ms
//    is only ever used for ordering / +N-minute arithmetic, so minutes lose
//    nothing).
//  * Synthetic HTF frames binned by ET wall clock, midnight anchor
//    (pine binOf, line ~237): bin = dayId*1440 + tmin - tmin % tf.  Pure int
//    ops only — the 2026-07-04 bin-degeneration bug note travels with the
//    code: never reintroduce division here.
//  * "na" is double.NaN; nz(x, y) is IsNaN(x) ? y : x.
//  * The core owns SIGNALS + market state only.  Order plumbing (limit entry,
//    SL/TP tickets, the BE move, EOD flatten) lives in the host, driven by
//    the shared BracketPlan state machine at the bottom of this file so the
//    NT8 wrapper and the parity harness cannot drift apart on management.
// ============================================================================

using System;
using System.Collections.Generic;

namespace P1Fast
{
    public enum RsiGateMode { Off, NotOB, Both }

    // ORB re-arm rule (engine cfg.orb_rearm / pine rearmMode):
    //   Off     — one signal per direction per day (eval preset, legacy)
    //   ReBreak — a consumed direction re-arms when a COMPLETED 5m close is
    //             strictly back inside the OR level (engine strat_ORB lines
    //             232-243; the re-arm bar itself never triggers). Unlimited
    //             re-arms (engine orb_max_per_dir = 0). Funded preset.
    public enum RearmMode { Off, ReBreak }

    public sealed class P1FastParams
    {
        // instrument
        public double TickSize = 0.25;        // MES
        public double PointValue = 5.0;       // MES $/pt per contract
        // method inputs (pine `inputs` block; defaults = shipped preset)
        public double RiskUsd = 800.0;        // 1R nominal (parity: flat $800)
        public RsiGateMode RsiGate = RsiGateMode.NotOB;
        public double PadAtr = 0.25;          // stop pad (x ATR 5m)
        public double DailyStopR = 2.0;       // 0 = off. Engine ref book ran 0.
        public int CapQty = 20;               // 0 = uncapped. PART OF THE METHOD.
        public RearmMode Rearm = RearmMode.Off;   // Off = eval legacy (bit-identical)
        // one-winner day cap: no NEW entries once realized day PnL >= this
        // (0 = off; the funded reference book 14767 has NO cap — pine
        // f_guardOK dayCapUsd is a live overlay, engine-unmodelled)
        public double DayCapUsd = 0.0;
        // Confluence TAKE rule (engine take_policy / conf_count_bits, added
        // 2026-08-17 for the R_minus_c2/c3_count3 presets):
        //   TakeCount = 0  -> legacy grade rule: take iff nb >= 4 (grade A/B)
        //   TakeCount = N  -> take iff at least N of the bits named in
        //                     ConfCountBits ("c1,c3,c4,c5,c6") are set;
        //                     the grade is still computed for display only.
        public int TakeCount = 0;
        public string ConfCountBits = "c1,c2,c3,c4,c5,c6";
        // LIVE EVAL sizing (taper ÷1.7 + desperado). OFF for any parity run.
        public bool LiveMode = false;
        public double LiveNeeded = 1250.0;
        public double LiveRoom = 1000.0;
        public double LiveDiv = 1.7;
        // trade window (bar OPEN AbsMin bounds, inclusive — pine tradeFrom/To)
        public long TradeFromAbsMin = long.MinValue;
        public long TradeToAbsMin = long.MaxValue;
        // session emit bound: 5m decision close must be <= this ET minute.
        // 930 (15:30) == pine emitMax with sessAM=false == engine _emit_ok_time
        // for session in ("AMPM","RTH").
        public int EmitMaxMin = 930;
    }

    public struct BarInput
    {
        public long AbsMin;   // bar OPEN, minutes since 0001-01-01, ET wall clock
        public int DayId;     // yyyymmdd (ET)
        public int Tmin;      // bar OPEN minutes since midnight ET
        public double O, H, L, C, V;
    }

    // What the core needs to know about the host's order/position state at
    // this bar's close (pine reads strategy.* for the same three facts).
    public struct HostState
    {
        public bool InPosition;      // strategy.position_size != 0
        public bool PendingEntry;    // working entry limit not yet filled/cancelled
        public double RealizedDayPnl; // net $ realized since today's first bar (commissions included)
        // realized day PnL on the basis the DAY CAP is declared on (the
        // engine's daycap overlay counts $0.50/side); NaN = use RealizedDayPnl.
        // NT8 passes the same account figure for both.
        public double RealizedDayPnlForCap;
    }

    public sealed class SignalEvent
    {
        public bool IsLong;
        public double Level;       // limit price = OR high / OR low
        public double Stop;        // padded structural stop
        public double SlDist;      // |level - stop| (sizes the trade, sets 1R/2R)
        public double Tp1;         // +1R (BE trigger)
        public double Tp2;         // +2R (all-out limit target)
        public int Qty;            // contracts (only when Taken)
        public int Nb;             // confluence bits set (0..6)
        public string Bits = "";   // "c1c2c3c4c5c6"
        public string Grade = "";  // A / B / C
        public bool Taken;         // passed every gate; host should place the order
        public string SkipReason = ""; // sl-tight | gradeC | in-pos | guardrail | window | size
        public long DeadlineAbsMin;    // last bar OPEN that may fill the limit
                                       // (= signal bar AbsMin + 31; pine pendDeadline)
    }

    public sealed class BarOutput
    {
        public bool NewDay;
        public bool FlatNow;       // bar OPEN >= 15:55 ET (12:55 on early closes)
        public bool IsEarlyClose;
        public SignalEvent Signal; // null when no direction fired this bar
    }

    public sealed class P1FastCore
    {
        readonly P1FastParams P;
        double Tick => P.TickSize;
        double Pv => P.PointValue;

        public P1FastCore(P1FastParams p) { P = p; }

        // ------------------------------------------------------------ frames
        sealed class Frame
        {
            public readonly int Tf; readonly int Cap;
            public long BinId = long.MinValue; public bool Live;
            public double O, H, L, C, V; public long Ot;
            public int Done;                     // completed bars ever
            public readonly List<double> Hh = new List<double>(), Hl = new List<double>(), Hc = new List<double>(), Hv = new List<double>();
            public readonly List<long> Ht = new List<long>();  // completed-bar OPEN AbsMin
            public Frame(int tf, int cap) { Tf = tf; Cap = cap; }

            // pine binOf: pure int ops ONLY (see 2026-07-04 bug note above)
            public long BinOf(int dayId, int tmin) => (long)dayId * 1440 + tmin - tmin % Tf;

            public bool Finalize()
            {
                if (!Live) return false;
                Hh.Add(H); Hl.Add(L); Hc.Add(C); Hv.Add(V); Ht.Add(Ot);
                if (Hh.Count > Cap)
                { Hh.RemoveAt(0); Hl.RemoveAt(0); Hc.RemoveAt(0); Hv.RemoveAt(0); Ht.RemoveAt(0); }
                Done++; Live = false;
                return true;
            }

            public void Accum(BarInput b)
            {
                long bin = BinOf(b.DayId, b.Tmin);
                if (!Live || BinId != bin)
                { BinId = bin; Live = true; O = b.O; H = b.H; L = b.L; C = b.C; V = b.V; Ot = b.AbsMin; }
                else
                { H = Math.Max(H, b.H); L = Math.Min(L, b.L); C = b.C; V += b.V; }
            }

            public bool EndsNow(int tmin) => tmin % Tf == Tf - 1;
        }

        readonly Frame f5 = new Frame(5, 40), f15 = new Frame(15, 30), f30 = new Frame(30, 30),
                       f60 = new Frame(60, 30), f240 = new Frame(240, 20);

        // -------------------------------------------------- frame-derived state
        double atr5 = double.NaN;              // Wilder 14 on continuous 5m
        double rsiAvgUp = double.NaN, rsiAvgDn = double.NaN, rsi5 = double.NaN;
        readonly List<double> vol5 = new List<double>();    // completed 5m volumes (cap 21)
        readonly List<double> p5loV = new List<double>(); readonly List<long> p5loT = new List<long>();
        readonly List<double> p5hiV = new List<double>(); readonly List<long> p5hiT = new List<long>();
        readonly List<double> p15loV = new List<double>(); readonly List<long> p15loT = new List<long>();
        readonly List<double> p15hiV = new List<double>(); readonly List<long> p15hiT = new List<long>();
        readonly List<double> sw30hi = new List<double>(), sw30lo = new List<double>();
        readonly List<double> sw60hi = new List<double>(), sw60lo = new List<double>();
        double ema20h1 = double.NaN, ema50h1 = double.NaN;
        readonly List<double> fvg60lo = new List<double>(), fvg60hi = new List<double>(); readonly List<int> fvg60d = new List<int>();
        readonly List<double> fvg240lo = new List<double>(), fvg240hi = new List<double>(); readonly List<int> fvg240d = new List<int>();

        // ------------------------------------------------------- day context
        double pdh = double.NaN, pdl = double.NaN;
        double onh = double.NaN, onl = double.NaN;
        double lonH = double.NaN, lonL = double.NaN;
        double curDayHi = double.NaN, curDayLo = double.NaN;
        double onAccHi = double.NaN, onAccLo = double.NaN;
        double orHi = double.NaN, orLo = double.NaN;
        bool orDone;
        double cumV, cumVP, cumVP2;            // 09:30-anchored session VWAP
        readonly List<double> eqh = new List<double>(), eql = new List<double>();
        readonly List<double> swLvlL = new List<double>(); readonly List<int> swStL = new List<int>(), swRdL = new List<int>();
        readonly List<double> swLvlS = new List<double>(); readonly List<int> swStS = new List<int>(), swRdS = new List<int>();
        int rthIdx = -1;
        int eqDay = -1;
        readonly List<long> sessOpenT = new List<long>(); // 09:30 bar-open stamps of past sessions
        // per-direction detector state (pine longArmed/shortArmed, stored
        // inverted): true = direction consumed. Off mode: consumed for the
        // day. ReBreak mode: re-arms on a completed 5m close back inside.
        bool longDone, shortDone;

        int prevDayId = -1;
        int prevTmin;                          // pine nz(tmin[1], 0)

        // ------------------------------------------------------- public views
        public double OrHi => orHi; public double OrLo => orLo; public bool OrDone => orDone;
        public double Atr5 => atr5; public double Rsi5 => rsi5;
        public double Pdh => pdh; public double Pdl => pdl;
        public double Onh => onh; public double Onl => onl;
        public bool LongDone => longDone; public bool ShortDone => shortDone;
        public bool LongArmed => !longDone; public bool ShortArmed => !shortDone;
        public double VwapNow => cumV > 0 ? cumVP / cumV : double.NaN;
        public double SdNow => cumV > 0
            ? Math.Sqrt(Math.Max(cumVP2 / cumV - (cumVP / cumV) * (cumVP / cumV), 0.0)) : double.NaN;

        // CME 13:00-ET early closes (pine EARLY_CLOSE, verbatim) + forward
        // MAINTENANCE additions beyond the pine data window (post-2026-06,
        // marked): keep this list current for live trading — a missed date
        // means the strategy holds until 15:55 into a closed market.
        static readonly HashSet<int> EarlyClose = new HashSet<int>
        {
            20230703, 20230704, 20230904, 20231123, 20231124,
            20240115, 20240219, 20240527, 20240619, 20240703, 20240704, 20240902,
            20241128, 20241129, 20241224,
            20250120, 20250217, 20250526, 20250619, 20250703, 20250704, 20250901,
            20251127, 20251128, 20251224,
            20260119, 20260216, 20260525, 20260619,
            // ---- post-pine additions (not part of the parity window) ----
            20260703, 20260907, 20261126, 20261127, 20261224,
            20270118, 20270215, 20270531, 20270618, 20270705,
        };

        // ==================================================================
        // main per-bar step — mirrors the pine script's top-to-bottom order
        // ==================================================================
        public BarOutput OnBar(BarInput bar, HostState host)
        {
            var outp = new BarOutput();
            int tmin = bar.Tmin;
            int ctMin = tmin + 1;
            int dayId = bar.DayId;
            bool newDay = dayId != prevDayId;
            outp.NewDay = newDay;

            bool inRTH = tmin >= 570 && tmin < 960;
            bool inON = tmin >= 1080 || tmin < 570;
            bool inLON = tmin >= 180 && tmin < 480;
            bool inOR = tmin >= 570 && tmin < 575;
            bool isEarly = EarlyClose.Contains(dayId);
            int flatMin = isEarly ? 775 : 955;
            bool flatNow = tmin >= flatMin;
            outp.FlatNow = flatNow;
            outp.IsEarlyClose = isEarly;
            bool inWindow = bar.AbsMin >= P.TradeFromAbsMin && bar.AbsMin <= P.TradeToAbsMin;

            // ---- day roll (pine 480-498): pdh/pdl advance only when the
            // finished day had an RTH session (weekends/holidays keep prior)
            if (newDay)
            {
                if (!double.IsNaN(curDayHi)) { pdh = curDayHi; pdl = curDayLo; }
                curDayHi = double.NaN; curDayLo = double.NaN;
                lonH = double.NaN; lonL = double.NaN;
                orHi = double.NaN; orLo = double.NaN;
                orDone = false;
                longDone = false; shortDone = false;
                rthIdx = -1;
            }
            // 18:00 ET: fresh overnight window (pine 499-501)
            if (tmin >= 1080 && prevTmin < 1080) { onAccHi = double.NaN; onAccLo = double.NaN; }

            if (inRTH)
            {
                curDayHi = Math.Max(Nz(curDayHi, bar.H), bar.H);
                curDayLo = Math.Min(Nz(curDayLo, bar.L), bar.L);
            }
            if (inON)
            {
                onAccHi = Math.Max(Nz(onAccHi, bar.H), bar.H);
                onAccLo = Math.Min(Nz(onAccLo, bar.L), bar.L);
            }
            if (inLON)
            {
                lonH = Math.Max(Nz(lonH, bar.H), bar.H);
                lonL = Math.Min(Nz(lonL, bar.L), bar.L);
            }
            if (inOR)
            {
                orHi = Math.Max(Nz(orHi, bar.H), bar.H);
                orLo = Math.Min(Nz(orLo, bar.L), bar.L);
            }
            if (!orDone && tmin >= 575) orDone = true;

            // ---- 09:30 snapshot (pine 551-596): freeze ON, EQ clusters, sweeps
            if (inRTH && dayId != eqDay)
            {
                eqDay = dayId;
                onh = double.IsNaN(onAccHi) ? pdh : onAccHi;   // engine: pdh/pdl if ON empty
                onl = double.IsNaN(onAccLo) ? pdl : onAccLo;
                sessOpenT.Add(bar.AbsMin);
                if (sessOpenT.Count > 10) sessOpenT.RemoveAt(0);
                int ns = sessOpenT.Count;
                long winStart = ns >= 3 ? sessOpenT[ns - 3] : long.MinValue;
                var hV = new List<double>(); var lV = new List<double>();
                if (winStart != long.MinValue)
                {
                    for (int k = 0; k < p5hiV.Count; k++)
                        if (p5hiT[k] >= winStart + 10) hV.Add(p5hiV[k]);
                    for (int k = 0; k < p5loV.Count; k++)
                        if (p5loT[k] >= winStart + 10) lV.Add(p5loV[k]);
                }
                Cluster(hV, eqh);
                Cluster(lV, eql);
                var longLvls = new List<double> { pdl, onl };
                if (!double.IsNaN(lonL)) longLvls.Add(lonL);
                longLvls.AddRange(eql);
                var shortLvls = new List<double> { pdh, onh };
                if (!double.IsNaN(lonH)) shortLvls.Add(lonH);
                shortLvls.AddRange(eqh);
                ArmSweeps(longLvls, swLvlL, swStL, swRdL);
                ArmSweeps(shortLvls, swLvlS, swStS, swRdS);
                cumV = 0.0; cumVP = 0.0; cumVP2 = 0.0;
            }

            // ---- lazy bin finalization (pine 598-624): bins that ended before
            // this bar opened (missing boundary minutes).  Never fires signals.
            if (f5.Live && f5.BinId != f5.BinOf(dayId, tmin))
                if (f5.Finalize())
                {
                    Post5(); Rsi5Update();
                    // Lazy-finalized 5m bins never fire signals but DO
                    // re-arm (pine 756-763): the engine detector sees every
                    // completed decision bar. Only re-arming here, never
                    // consumption.
                    if (P.Rearm == RearmMode.ReBreak && orDone && !double.IsNaN(orHi))
                    {
                        double c5Lz = f5.Hc[f5.Hc.Count - 1];
                        if (longDone && c5Lz < orHi) longDone = false;
                        if (shortDone && c5Lz > orLo) shortDone = false;
                    }
                }
            if (f15.Live && f15.BinId != f15.BinOf(dayId, tmin))
                if (f15.Finalize()) Post15();
            if (f30.Live && f30.BinId != f30.BinOf(dayId, tmin))
                if (f30.Finalize()) Post30();
            if (f60.Live && f60.BinId != f60.BinOf(dayId, tmin))
                if (f60.Finalize()) Post60();
            if (f240.Live && f240.BinId != f240.BinOf(dayId, tmin))
                if (f240.Finalize()) Post240();

            // (pine 626-749 is trade management — host-side, see BracketPlan)

            // ---- accumulate current 1m bar into every frame (pine 752-756)
            f5.Accum(bar); f15.Accum(bar); f30.Accum(bar); f60.Accum(bar); f240.Accum(bar);

            // ---- session VWAP + sweep state machines (pine 758-792)
            if (inRTH)
            {
                rthIdx++;
                double tp = (bar.H + bar.L + bar.C) / 3.0;
                double v = Math.Max(bar.V, 1e-9);
                cumV += v; cumVP += v * tp; cumVP2 += v * tp * tp;
                UpdateSweeps(swLvlL, swStL, swRdL, true, bar);   // long side
                UpdateSweeps(swLvlS, swStS, swRdS, false, bar);  // short side
            }

            // ---- inline bin finalization (pine 799-816): signals fire ONLY
            // from the 5m bin ending at THIS bar's close.
            bool sig5 = false;
            if (f5.EndsNow(tmin) && f5.Finalize()) { sig5 = true; Post5(); Rsi5Update(); }
            if (f15.EndsNow(tmin) && f15.Finalize()) Post15();
            if (f30.EndsNow(tmin) && f30.Finalize()) Post30();
            if (f60.EndsNow(tmin) && f60.Finalize()) Post60();
            if (f240.EndsNow(tmin) && f240.Finalize()) Post240();

            // ---- signal scan (pine 1027-1129)
            bool ctxReady = orDone && !double.IsNaN(orHi) && !double.IsNaN(pdh)
                            && !double.IsNaN(pdl) && !double.IsNaN(atr5) && atr5 > 0;
            int sigDir = 0;
            if (sig5 && ctxReady && ctMin > 575 && ctMin <= P.EmitMaxMin && !flatNow)
            {
                double c5m = f5.Hc[f5.Hc.Count - 1];       // completed 5m close
                // Each direction is scanned INDEPENDENTLY on every completed
                // decision bar (engine strat_ORB / pine 1183-1215): one close
                // can trigger one side and re-arm the other. The two triggers
                // are mutually exclusive (a close cannot be above OR-high and
                // below OR-low), so a single sigDir suffices.
                // ---- LONG side ----
                if (longDone)
                {
                    // ReBreak re-arm: completed close STRICTLY back below
                    // OR-high; the re-arm bar itself never triggers.
                    if (P.Rearm == RearmMode.ReBreak && c5m < orHi) longDone = false;
                }
                else if (c5m > orHi)
                {
                    if (RsiOK(true)) { longDone = true; sigDir = 1; }
                    // gated bar: direction NOT consumed; scan continues next 5m
                }
                // ---- SHORT side ----
                if (shortDone)
                {
                    if (P.Rearm == RearmMode.ReBreak && c5m > orLo) shortDone = false;
                }
                else if (c5m < orLo)
                {
                    if (RsiOK(false)) { shortDone = true; sigDir = -1; }
                }
            }

            if (sigDir != 0)
            {
                bool lng = sigDir > 0;
                SigParts sp = BuildSignal(lng);
                double level = sp.Level, slRaw = sp.SlRaw, slD = sp.SlD;
                int nb = sp.Nb;
                var ev = new SignalEvent
                {
                    IsLong = lng,
                    Level = level,
                    Stop = slRaw,
                    SlDist = slD,
                    Tp1 = level + (lng ? slD : -slD),
                    Tp2 = level + (lng ? 2 * slD : -2 * slD),
                    Nb = nb,
                    Bits = sp.Bits,
                    Grade = nb >= 5 ? "A" : nb >= 4 ? "B" : "C",
                    DeadlineAbsMin = bar.AbsMin + 31,      // signal close + 30 min
                };
                if (slD < Tick) ev.SkipReason = "sl-tight";
                else if (!TakeOK(sp.Bits, nb)) ev.SkipReason = P.TakeCount > 0 ? "conf-count" : "gradeC";
                else if (host.InPosition || host.PendingEntry) ev.SkipReason = "in-pos";
                else if (P.DailyStopR > 0 && host.RealizedDayPnl <= -P.DailyStopR * P.RiskUsd)
                    ev.SkipReason = "guardrail";
                else if (P.DayCapUsd > 0 && (double.IsNaN(host.RealizedDayPnlForCap)
                         ? host.RealizedDayPnl : host.RealizedDayPnlForCap) >= P.DayCapUsd)
                    ev.SkipReason = "guardrail";        // one-winner day cap (pine f_guardOK)
                else if (!inWindow) ev.SkipReason = "window";
                else
                {
                    int q = QtyFor(slD);
                    if (q < 1) ev.SkipReason = "size";     // engine skipped-size
                    else { ev.Taken = true; ev.Qty = q; }
                }
                outp.Signal = ev;
            }

            prevDayId = dayId;
            prevTmin = tmin;
            return outp;
        }

        // ------------------------------------------------------------ helpers
        static double Nz(double x, double y) => double.IsNaN(x) ? y : x;

        // engine swing_flags: strict fractal k=2, pivot bar = n-3, confirmed
        // when bar n-1 (newest completed) closes (pine f_pivots)
        static void PivotsUpdate(Frame f, List<double> loV, List<long> loT,
                                 List<double> hiV, List<long> hiT, int cap)
        {
            int n = f.Hh.Count;
            if (n < 5) return;
            double hp = f.Hh[n - 3];
            if (hp > Math.Max(f.Hh[n - 5], f.Hh[n - 4]) && hp > Math.Max(f.Hh[n - 2], f.Hh[n - 1]))
            {
                hiV.Add(hp); hiT.Add(f.Ht[n - 3]);
                if (hiV.Count > cap) { hiV.RemoveAt(0); hiT.RemoveAt(0); }
            }
            double lp = f.Hl[n - 3];
            if (lp < Math.Min(f.Hl[n - 5], f.Hl[n - 4]) && lp < Math.Min(f.Hl[n - 2], f.Hl[n - 1]))
            {
                loV.Add(lp); loT.Add(f.Ht[n - 3]);
                if (loV.Count > cap) { loV.RemoveAt(0); loT.RemoveAt(0); }
            }
        }

        static void Swings3Update(Frame f, List<double> his, List<double> los)
        {
            int n = f.Hh.Count;
            if (n < 5) return;
            double hp = f.Hh[n - 3];
            if (hp > Math.Max(f.Hh[n - 5], f.Hh[n - 4]) && hp > Math.Max(f.Hh[n - 2], f.Hh[n - 1]))
            { his.Add(hp); if (his.Count > 3) his.RemoveAt(0); }
            double lp = f.Hl[n - 3];
            if (lp < Math.Min(f.Hl[n - 5], f.Hl[n - 4]) && lp < Math.Min(f.Hl[n - 2], f.Hl[n - 1]))
            { los.Add(lp); if (los.Count > 3) los.RemoveAt(0); }
        }

        // FVG update on the newest completed bar (pine f_fvg): form vs bar n-3,
        // then close any open gap the new close fully traded through
        static void FvgUpdate(Frame f, List<double> lo, List<double> hi, List<int> dir)
        {
            int n = f.Hh.Count;
            if (n < 3) return;
            double h2 = f.Hh[n - 3], l2 = f.Hl[n - 3];
            double h0 = f.Hh[n - 1], l0 = f.Hl[n - 1];
            if (h2 < l0) { lo.Add(h2); hi.Add(l0); dir.Add(1); }
            else if (l2 > h0) { lo.Add(h0); hi.Add(l2); dir.Add(-1); }
            double c0 = f.Hc[n - 1];
            int k = 0;
            while (k < dir.Count)
            {
                int d = dir[k];
                if ((d > 0 && c0 < lo[k]) || (d < 0 && c0 > hi[k]))
                { lo.RemoveAt(k); hi.RemoveAt(k); dir.RemoveAt(k); }
                else k++;
            }
            if (dir.Count > 200) { lo.RemoveAt(0); hi.RemoveAt(0); dir.RemoveAt(0); }
        }

        void Post5()
        {
            int n = f5.Hh.Count;
            double hh = f5.Hh[n - 1], ll = f5.Hl[n - 1];
            double tr = hh - ll;
            if (n >= 2)
            {
                double pc = f5.Hc[n - 2];
                tr = Math.Max(hh - ll, Math.Max(Math.Abs(hh - pc), Math.Abs(ll - pc)));
            }
            vol5.Add(f5.Hv[n - 1]);
            if (vol5.Count > 21) vol5.RemoveAt(0);
            PivotsUpdate(f5, p5loV, p5loT, p5hiV, p5hiT, 400);
            atr5 = double.IsNaN(atr5) ? tr : (13.0 * atr5 + tr) / 14.0;
        }

        // Wilder RSI(14) on completed continuous 5m closes; rsi = 50 while
        // avg-down is 0/undefined (engine's .replace(0,nan) -> .fillna(50))
        void Rsi5Update()
        {
            int n = f5.Hc.Count;
            if (n < 2) return;
            double d = f5.Hc[n - 1] - f5.Hc[n - 2];
            double up = Math.Max(d, 0.0), dn = Math.Max(-d, 0.0);
            rsiAvgUp = double.IsNaN(rsiAvgUp) ? up : (13.0 * rsiAvgUp + up) / 14.0;
            rsiAvgDn = double.IsNaN(rsiAvgDn) ? dn : (13.0 * rsiAvgDn + dn) / 14.0;
            rsi5 = rsiAvgDn > 1e-12 ? 100.0 - 100.0 / (1.0 + rsiAvgUp / rsiAvgDn) : 50.0;
        }

        void Post15() => PivotsUpdate(f15, p15loV, p15loT, p15hiV, p15hiT, 100);
        void Post30() => Swings3Update(f30, sw30hi, sw30lo);

        void Post60()
        {
            int n = f60.Hc.Count;
            double c0 = f60.Hc[n - 1];
            Swings3Update(f60, sw60hi, sw60lo);
            FvgUpdate(f60, fvg60lo, fvg60hi, fvg60d);
            ema20h1 = double.IsNaN(ema20h1) ? c0 : (2.0 / 21.0) * c0 + (19.0 / 21.0) * ema20h1;
            ema50h1 = double.IsNaN(ema50h1) ? c0 : (2.0 / 51.0) * c0 + (49.0 / 51.0) * ema50h1;
        }

        void Post240() => FvgUpdate(f240, fvg240lo, fvg240hi, fvg240d);

        // EQ clusters (pine f_cluster): sort asc, group within 5 ticks, mean of
        // groups with >= 2 members
        void Cluster(List<double> vals, List<double> dst)
        {
            dst.Clear();
            int m = vals.Count;
            if (m < 2) return;
            vals.Sort();
            double tol = 5 * Tick;
            int i = 0;
            while (i < m)
            {
                double g0 = vals[i];
                int j = i + 1, cnt = 1;
                double s = g0;
                while (j < m && vals[j] - g0 <= tol) { s += vals[j]; cnt++; j++; }
                if (cnt >= 2) dst.Add(s / cnt);
                i = j;
            }
        }

        static void ArmSweeps(List<double> lvls, List<double> dstLvl, List<int> dstSt, List<int> dstRd)
        {
            dstLvl.Clear(); dstSt.Clear(); dstRd.Clear();
            foreach (double v in lvls)
                if (!double.IsNaN(v)) { dstLvl.Add(v); dstSt.Add(0); dstRd.Add(-1); }
        }

        // engine detect_sweep parity (pine 767-792): first raid per level (4
        // ticks beyond), reclaim close within 3 RTH bars after the raid bar,
        // else the level is dead. 0 armed / 1 pending / 2 swept / 3 dead.
        void UpdateSweeps(List<double> lvl, List<int> st, List<int> rd, bool longSide, BarInput bar)
        {
            for (int k = 0; k < lvl.Count; k++)
            {
                int s = st[k];
                double lv = lvl[k];
                bool raided = longSide ? bar.L <= lv - 4 * Tick : bar.H >= lv + 4 * Tick;
                if (s == 0 && raided) { s = 1; rd[k] = rthIdx; }
                if (s == 1)
                {
                    bool reclaimed = longSide ? bar.C > lv : bar.C < lv;
                    if (reclaimed) s = 2;
                    else if (rthIdx >= rd[k] + 3) s = 3;
                }
                st[k] = s;
            }
        }

        // ------------------------------------------------------- confluences
        bool C1(bool lng)
        {
            int n = f60.Hc.Count;
            if (n == 0 || double.IsNaN(ema50h1)) return false;
            double c0 = f60.Hc[n - 1];
            return lng ? c0 > ema20h1 && ema20h1 > ema50h1
                       : c0 < ema20h1 && ema20h1 < ema50h1;
        }

        bool C2(bool lng, double px)
        {
            double vw = VwapNow;
            if (double.IsNaN(vw)) return false;
            double sd = SdNow;
            return lng ? px > vw + sd : px < vw - sd;    // vwapBand = band-trend
        }

        bool C3()
        {
            int n = vol5.Count;
            if (n < 2) return false;
            int cnt = Math.Min(n - 1, 20);
            double s = 0.0;
            for (int k = n - 1 - cnt; k <= n - 2; k++) s += vol5[k];
            double avg = s / cnt;
            return avg > 0 && vol5[n - 1] > 1.5 * avg;
        }

        bool C4(bool lng)
        {
            var st = lng ? swStL : swStS;
            for (int k = 0; k < st.Count; k++) if (st[k] == 2) return true;
            return false;
        }

        static bool FvgHit(bool lng, double px, List<double> lo, List<double> hi, List<int> dir)
        {
            int want = lng ? 1 : -1;
            for (int k = 0; k < dir.Count; k++)
                if (dir[k] == want && px >= lo[k] && px <= hi[k]) return true;
            return false;
        }

        // _htf_pattern on one frame (pine f_pattern): 13-bar window edge or
        // break-retest of the last 3 confirmed swings; tol = max(6 ticks,
        // 10% window range); needs >= 7 completed bars ever
        bool Pattern(Frame f, List<double> shi, List<double> slo, bool lng, double px)
        {
            int n = f.Hh.Count;
            if (f.Done < 7 || n < 1) return false;
            int w = Math.Min(n, 13);
            double hi = f.Hh[n - 1], lo = f.Hl[n - 1];
            for (int k = n - w; k <= n - 1; k++)
            { hi = Math.Max(hi, f.Hh[k]); lo = Math.Min(lo, f.Hl[k]); }
            double tol = Math.Max(6 * Tick, 0.1 * (hi - lo));
            if (lng && Math.Abs(px - lo) <= tol) return true;
            if (!lng && Math.Abs(px - hi) <= tol) return true;
            double cl0 = f.Hc[n - 1];
            var arr = lng ? shi : slo;
            for (int k = 0; k < arr.Count; k++)
            {
                double lv = arr[k];
                if ((lng ? cl0 > lv : cl0 < lv) && Math.Abs(px - lv) <= tol) return true;
            }
            return false;
        }

        bool C5(bool lng, double px) =>
            FvgHit(lng, px, fvg60lo, fvg60hi, fvg60d)
            || FvgHit(lng, px, fvg240lo, fvg240hi, fvg240d)
            || Pattern(f60, sw60hi, sw60lo, lng, px)
            || Pattern(f30, sw30hi, sw30lo, lng, px);

        // room to 2R: nearest opposing PDH/PDL/ONH/ONL, any open 1H/4H FVG
        // edge, or the next round-100 level (engine round_room = 100)
        bool C6(bool lng, double px, double slD)
        {
            var lv = new List<double> { pdh, pdl, onh, onl };
            for (int k = 0; k < fvg60d.Count; k++) { lv.Add(fvg60lo[k]); lv.Add(fvg60hi[k]); }
            for (int k = 0; k < fvg240d.Count; k++) { lv.Add(fvg240lo[k]); lv.Add(fvg240hi[k]); }
            lv.Add(lng ? (Math.Floor(px / 100.0) + 1) * 100.0 : Math.Floor(px / 100.0) * 100.0);
            double nearest = 1.0e18;
            foreach (double x in lv)
            {
                if (double.IsNaN(x)) continue;
                if (lng && x > px + 1e-9) nearest = Math.Min(nearest, x - px);
                if (!lng && x < px - 1e-9) nearest = Math.Min(nearest, px - x);
            }
            return nearest >= 2.0 * slD;
        }

        // RSI(14) 5m gate on the breakout decision bar; missing RSI counts as
        // 50 (passes). notOB blocks rsi > 70 long / < 30 short.
        bool RsiOK(bool lng)
        {
            double r = double.IsNaN(rsi5) ? 50.0 : rsi5;
            bool ok = true;
            if (P.RsiGate == RsiGateMode.NotOB || P.RsiGate == RsiGateMode.Both)
                ok = lng ? r <= 70.0 : r >= 30.0;
            if (ok && P.RsiGate == RsiGateMode.Both)
                ok = lng ? r > 50.0 : r < 50.0;
            return ok;
        }

        // pure signal builder (pine f_signal). stop order of operations =
        // engine: raw structural -> 0.5*ATR floor -> stop_pad widening; the
        // PADDED distance feeds sizing, 1R/2R and c6 room.
        struct SigParts
        {
            public double Level, SlRaw, SlD;
            public int Nb;
            public string Bits;
        }

        SigParts BuildSignal(bool lng)
        {
            double level = lng ? orHi : orLo;
            double orMid = 0.5 * (orHi + orLo);
            double slRaw = lng ? Math.Max(orMid, level - 1.2 * atr5)
                               : Math.Min(orMid, level + 1.2 * atr5);
            if (Math.Abs(level - slRaw) < 0.5 * atr5)              // min_stop floor
                slRaw = lng ? level - 0.5 * atr5 : level + 0.5 * atr5;
            if (P.PadAtr > 0)                                       // stop pad
                slRaw = lng ? slRaw - P.PadAtr * atr5 : slRaw + P.PadAtr * atr5;
            double slD = Math.Abs(level - slRaw);
            bool c1 = C1(lng), c2 = C2(lng, level), c3 = C3(), c4 = C4(lng),
                 c5 = C5(lng, level), c6 = C6(lng, level, slD);
            int nb = (c1 ? 1 : 0) + (c2 ? 1 : 0) + (c3 ? 1 : 0)
                   + (c4 ? 1 : 0) + (c5 ? 1 : 0) + (c6 ? 1 : 0);
            string bits = $"{(c1 ? 1 : 0)}{(c2 ? 1 : 0)}{(c3 ? 1 : 0)}{(c4 ? 1 : 0)}{(c5 ? 1 : 0)}{(c6 ? 1 : 0)}";
            return new SigParts { Level = level, SlRaw = slRaw, SlD = slD, Nb = nb, Bits = bits };
        }

        // take rule: legacy grade A/B (nb >= 4) or "count:N over selected bits"
        // (engine strategies_p1 take_policy="count:N", conf_count_bits)
        bool TakeOK(string bits, int nb)
        {
            if (P.TakeCount <= 0) return nb >= 4;
            int cnt = 0;
            string sel = "," + (P.ConfCountBits ?? "").Replace(" ", "") + ",";
            for (int i = 0; i < 6 && i < bits.Length; i++)
                if (bits[i] == '1' && sel.Contains(",c" + (i + 1) + ",")) cnt++;
            return cnt >= P.TakeCount;
        }

        // one sizing rule for backtest AND live (pine f_qtyFor):
        // parity = floor(risk/stop$) capped; live = min(needed ÷ div, base,
        // cap) with the DESPERADO full-cap override when room < base.
        int QtyFor(double slD)
        {
            double rpc = slD * Pv;
            int q = (int)Math.Floor(P.RiskUsd / rpc);
            if (P.LiveMode)
            {
                int baseC = P.LiveRoom < P.RiskUsd && P.CapQty > 0 ? P.CapQty : q;
                int taperC = (int)Math.Ceiling(P.LiveNeeded / (P.LiveDiv * rpc));
                q = Math.Max(1, Math.Min(baseC, taperC));
            }
            if (P.CapQty > 0) q = Math.Min(q, P.CapQty);
            return q;
        }
    }

    // ======================================================================
    // BracketPlan — the M1be trade-management state machine, shared by the
    // NT8 wrapper and the parity harness so their management cannot drift.
    // Mirrors pine 626-749 + 1132-1156 for mgmtM1:
    //   * full position, fixed LIMIT target at +2R
    //   * once +1R prints (bar HIGH/LOW touch on a CLOSED bar strictly after
    //     the fill bar), stop moves to breakeven — effective from the NEXT
    //     bar (orders are (re)submitted at bar close, same as pine/engine)
    //   * EOD flat at the first bar opening >= 15:55 (12:55 early close)
    // The host owns actual order submission; it calls OnBarClosed() once per
    // closed 1m bar while the position is open and then (re)submits its
    // stop at StopPrice and its target at TargetPrice.
    // ======================================================================
    public sealed class BracketPlan
    {
        public bool IsLong { get; private set; }
        public double EntryPx { get; private set; }   // the OR-level limit price
        public double Tp1 { get; private set; }
        public double Tp2 { get; private set; }
        public double StopPrice { get; private set; }
        public bool BeMoved { get; private set; }
        public int FillBarIdx { get; private set; } = int.MinValue;

        public double TargetPrice => Tp2;

        public static BracketPlan FromSignal(SignalEvent s) => new BracketPlan
        {
            IsLong = s.IsLong,
            EntryPx = s.Level,
            Tp1 = s.Tp1,
            Tp2 = s.Tp2,
            StopPrice = s.Stop,
        };

        public void MarkFilled(int barIdx) { FillBarIdx = barIdx; }

        /// <summary>Returns true when this bar armed the BE move (host should
        /// amend its working stop to StopPrice, effective next bar).</summary>
        public bool OnBarClosed(int barIdx, double high, double low)
        {
            if (FillBarIdx == int.MinValue || barIdx <= FillBarIdx) return false;
            if (BeMoved) return false;
            bool touched = IsLong ? high >= Tp1 : low <= Tp1;
            if (!touched) return false;
            StopPrice = EntryPx;
            BeMoved = true;
            return true;
        }
    }
}
