// ============================================================================
// P1FastLucidFunded25kRminusC3.cs — NinjaTrader 8 strategy wrapper around P1FastCore.cs
// P1 FAST R_minus_c3_count3 M1be $400 — LucidPro 25K FUNDED port (account LTF02582991480003).
// GENERATED by nt8/make_funded_variants.py from P1FastLucidFunded25k.cs (do not
// hand-edit; the order plumbing is byte-identical to the verified base). Replaces the TV -> webhook -> PickMyTrade -> Tradovate
// chain with native NT8 automation on your Lucid Tradovate connection.
//
// METHOD (engine DB variant 14690, lucid25k_protocol_grid_full2019 /
// R_minus_c3_count3 — the "R_minus_c3_count3" cell on
// armansportal.com/trading/firms/lucid/funded-pro-25k; the battery records it as
// FAILED KILL BAR 1 — Arman asked for it for live account 003 anyway):
//   * ORB 5-minute opening range (09:30-09:35 ET), breakout = completed 5m
//     close beyond the OR level, RSI(14) 5m notOB gate (long blocked > 70,
//     short blocked < 30; a gated bar is skipped, not consumed)
//   * confluence grade A/B only (>= 4 of 6 bits: 1H bias, VWAP band, 5m
//     volume, sweep reclaim, HTF level, room to 2R)
//   * entry = LIMIT at the OR level, good 30 minutes after the signal close
//   * stop = padded structural stop (max(OR mid, level - 1.2 ATR), 0.5 ATR
//     floor, + 0.25 ATR pad); M1be management: full size, fixed limit target
//     at +2R, stop to breakeven after a CLOSED 1m bar (after the fill bar)
//     touches +1R
//   * reBreak RE-ARM: a used direction re-arms when a completed 5m close is
//     strictly back inside the OR level (unlimited re-arms per day) — this is
//     what distinguishes it from the eval port (one signal per direction/day)
//   * TAKE RULE: >= 3 of the five bits c1,c2,c4,c5,c6 (c3 5m volume NOT counted;
//     engine take_policy count:3) — instead of grade A/B
//   * flat $400 risk, cap 20, reBreak re-arm, 2R daily stop, no day cap
//   * daily stop: no NEW entries after realized day PnL <= -2R (-$800)
//   * hard flat 15:55 ET (12:55 on CME early closes)
//
// INSTALL: copy BOTH files (P1FastCore.cs + this file) into
//   Documents\NinjaTrader 8\bin\Custom\Strategies\
// (P1FastCore.cs is SHARED with P1FastLucidEval25k.cs — keep exactly ONE copy
// of it under bin\Custom, and use the 2026-08-17 or newer version, which
// carries the reBreak knob) then NT8: New > NinjaScript Editor > Compile (F5).
//
// CHART / BACKTEST REQUIREMENTS (see nt8/NT8-GUIDE.md §6 for the protocol):
//   * Instrument MES, data series = 1 MINUTE, 24h (ETH) session template.
//     The strategy refuses to trade on any other bar type/size/instrument.
//   * Bars arrive in NT8's configured time zone (default = PC local); the
//     strategy converts Local -> US/Eastern. If NT8's time zone setting is
//     NOT your PC zone, put that zone's .NET id in "Bars time zone id".
//   * Protective exits are ALSO submitted immediately on the entry execution
//     (OnExecutionUpdate), so live you are never naked for the fill minute.
//
// VERIFICATION (2026-08-17, nt8/harness): core reproduces engine variant 14690
// 1233/1233 matched trades fully identical over 2019-05..2026-08 (+1 classified
// structural engine-only re-entry). Full-window book: n=1234 net -$4,406 raw
// (2023-08+: n=531 +$19,835 raw / +$13,601 tick-rounded at $0.74/side; 2019-2022
// negative). Tick-grid probe: full-window rounded -$9,780. NOT TV-verified.
//
// CAVEATS THAT TRAVEL WITH EVERY NUMBER (AGENTS.md §2/§5): optimistic fill
// model (touch fills, zero net slippage), no holdout, MES-only, recent-regime
// edge, sim != live. The 2019-2022 years of this book are net NEGATIVE; the
// edge is 2024+. This is a simulated ceiling, not an expected live result.
// ============================================================================

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Windows;
using System.Windows.Media;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.Tools;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.DrawingTools;
using P1Fast;
#endregion

namespace NinjaTrader.NinjaScript.Strategies
{
    public class P1FastLucidFunded25kRminusC3 : Strategy
    {
        private P1FastCore core;
        private BracketPlan plan;          // null when no signal is being worked
        private Order entryOrder;          // working entry limit (null otherwise)
        private long pendingDeadline;      // last bar-OPEN AbsMin that may fill
        private int planQty;
        private int filledQty;             // accumulated entry-execution quantity
        private int exitFilledQty;         // accumulated TP/SL/EOD execution quantity
        private bool eodActive;            // last closed bar was in the 15:55/12:55 flat window
        private bool wasInPos;
        private int lastDayId = -1;
        private double realizedDayBase;
        private bool badSeries;
        private TimeZoneInfo easternTz, sourceTz;
        private int sigSeq;                // unique drawing tags
        private int orDayId = -1;          // day whose OR lines are on the chart
        private int orStartBar = -1;       // CurrentBar of the 09:35 bar (OR complete)
        private int tradesToday, winsToday;
        private double lastCumProfit;

        private const string SigLong = "P1L", SigShort = "P1S";
        // ---- display-only styling (TV-style boxed labels); nothing here feeds logic
        private SimpleFont fontLabel, fontSmall, fontPanel;
        private Brush brLongBg, brShortBg, brSkipBg, brBeBg, brPanelBg, brOrBg;

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Name = "P1FastLucidFunded25kRminusC3";
                Description = "R_minus_c3_count3 — Fast reBreak M1be $400, take iff >=3 of c1,c2,c4,c5,c6 (5m volume bit not counted) — LucidPro 25K funded 003; engine variant 14690";
                Calculate = Calculate.OnBarClose;      // pine executes on bar close
                EntriesPerDirection = 1;               // per OPEN position; re-entries after flat are allowed
                EntryHandling = EntryHandling.AllEntries;
                IsExitOnSessionCloseStrategy = true;   // backstop only; the method flattens itself 15:55
                ExitOnSessionCloseSeconds = 30;
                Slippage = 0;
                StartBehavior = StartBehavior.WaitUntilFlat;
                TimeInForce = TimeInForce.Gtc;
                TraceOrders = false;
                RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
                BarsRequiredToTrade = 0;               // core gates itself (needs prior-day levels)
                IsInstantiatedOnEachOptimizationIteration = true;

                RiskUsd = 400;
                CapQty = 20;
                Rearm = RearmMode.ReBreak;
                DailyStopR = 2.0;
                DayCapUsd = 0;
                TakeCount = 3;
                ConfCountBits = "c1,c2,c4,c5,c6";
                RsiGate = RsiGateMode.NotOB;
                PadAtr = 0.25;
                AccountLabel = "LTF02582991480003";
                TradeFrom = new DateTime(2000, 1, 1);
                TradeTo = new DateTime(2099, 1, 1);
                BarsTimeZoneId = "";
                LogSignals = true;
                ShowLabels = true;
                ShowLevels = true;
                ShowStatus = true;
            }
            else if (State == State.Configure)
            {
                // nothing extra — single 1m series only
            }
            else if (State == State.Realtime)
            {
                // MANDATORY historical -> realtime order-reference transition
                // (NT8 advanced order handling): cancelling/amending a stale
                // historical Order object can disable the strategy.
                if (entryOrder != null)
                    entryOrder = GetRealtimeOrder(entryOrder);
            }
            else if (State == State.DataLoaded)
            {
                badSeries = BarsPeriod.BarsPeriodType != BarsPeriodType.Minute
                            || BarsPeriod.Value != 1;
                if (badSeries)
                {
                    Log("P1FastLucidFunded25kRminusC3 requires a 1-MINUTE data series; strategy is disabled on this chart.", LogLevel.Error);
                    Print("P1FastLucidFunded25kRminusC3: wrong series (need 1 minute). No trades will be taken.");
                }
                // hard MES guard: the method is built, sized and verified on
                // MES only (tick $1.25, point $5). Refuse anything else.
                if (!badSeries && (Instrument.MasterInstrument.Name != "MES"
                    || Math.Abs(Instrument.MasterInstrument.PointValue - 5.0) > 1e-9))
                {
                    badSeries = true;
                    Log($"P1FastLucidFunded25kRminusC3 is MES-only (got {Instrument.MasterInstrument.Name}, point value {Instrument.MasterInstrument.PointValue}); strategy is disabled.", LogLevel.Error);
                    Print("P1FastLucidFunded25kRminusC3: not MES — no trades will be taken.");
                }

                // take-rule input hygiene: the counted-bits list must be a comma
                // list drawn from c1..c6 (engine parse_conf_count_bits rejects
                // anything else); a typo must disable the strategy, not silently
                // count fewer bits.
                if (!badSeries && TakeCount > 0)
                {
                    string[] toks = (ConfCountBits ?? "").Replace(" ", "").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                    var seen = new HashSet<string>();
                    foreach (string tk in toks)
                        if (!(tk.Length == 2 && tk[0] == 'c' && tk[1] >= '1' && tk[1] <= '6') || !seen.Add(tk))
                        { badSeries = true; break; }
                    if (toks.Length == 0 || TakeCount > toks.Length) badSeries = true;
                    if (badSeries)
                    {
                        Log($"P1FastLucidFunded25kRminusC3: invalid take rule (min {TakeCount} over '{ConfCountBits}'); strategy is disabled.", LogLevel.Error);
                        Print("P1FastLucidFunded25kRminusC3: invalid 'Take rule: counted bits' — use a comma list from c1..c6 with no repeats, min-count <= list length. No trades will be taken.");
                    }
                }

                fontLabel = new SimpleFont("Segoe UI", 12) { Bold = true };
                fontSmall = new SimpleFont("Segoe UI", 10);
                fontPanel = new SimpleFont("Consolas", 12);
                brLongBg = MakeBrush(27, 94, 32);      // #1b5e20 (pine long label)
                brShortBg = MakeBrush(183, 28, 28);    // #b71c1c (pine short label)
                brSkipBg = MakeBrush(66, 66, 66);
                brBeBg = MakeBrush(255, 179, 0);
                brPanelBg = MakeBrush(13, 17, 23);
                brOrBg = MakeBrush(21, 101, 192);

                easternTz = FindTz("America/New_York", "Eastern Standard Time");
                sourceTz = string.IsNullOrWhiteSpace(BarsTimeZoneId)
                    ? TimeZoneInfo.Local
                    : TimeZoneInfo.FindSystemTimeZoneById(BarsTimeZoneId);

                var p = new P1FastParams
                {
                    TickSize = Instrument.MasterInstrument.TickSize,
                    PointValue = Instrument.MasterInstrument.PointValue,
                    RiskUsd = RiskUsd,
                    RsiGate = RsiGate,
                    PadAtr = PadAtr,
                    DailyStopR = DailyStopR,
                    CapQty = CapQty,
                    Rearm = Rearm,
                    DayCapUsd = DayCapUsd,
                    TakeCount = TakeCount,
                    ConfCountBits = ConfCountBits,
                    LiveMode = false,                  // funded = flat risk, no eval taper
                    TradeFromAbsMin = AbsMin(TradeFrom),
                    TradeToAbsMin = AbsMin(TradeTo),
                };
                core = new P1FastCore(p);
                Print($"P1FastLucidFunded25kRminusC3: account {AccountLabel} · risk ${RiskUsd} flat · cap {CapQty} · rearm {Rearm} · daily stop {DailyStopR}R · day cap ${DayCapUsd} · take {(TakeCount > 0 ? "count:" + TakeCount + " over " + ConfCountBits : "grade A/B")}");
            }
        }

        protected override void OnBarUpdate()
        {
            if (badSeries || core == null || BarsInProgress != 0) return;

            // NT8 stamps bars with their CLOSE time; the core wants OPEN time ET.
            DateTime closeLocal = Time[0];
            DateTime openEt = ToEastern(closeLocal).AddMinutes(-1);
            var bar = new BarInput
            {
                AbsMin = openEt.Ticks / TimeSpan.TicksPerMinute,
                DayId = openEt.Year * 10000 + openEt.Month * 100 + openEt.Day,
                Tmin = openEt.Hour * 60 + openEt.Minute,
                O = Open[0], H = High[0], L = Low[0], C = Close[0], V = Volume[0],
            };

            // ---- day PnL baseline (pine dayStartNP resets at ET midnight)
            double cum = SystemPerformance.AllTrades.TradesPerformance.Currency.CumProfit;
            if (bar.DayId != lastDayId) { realizedDayBase = cum; tradesToday = 0; winsToday = 0; }

            // ---- host management (pine 626-749): fill / close detection + BE
            bool inPosNow = Position.MarketPosition != MarketPosition.Flat;
            if (inPosNow && !wasInPos && plan != null)
                plan.MarkFilled(CurrentBar);
            if (!inPosNow && wasInPos)
            {
                // trade closed since last bar
                tradesToday++;
                if (cum > lastCumProfit) winsToday++;
                plan = null; filledQty = 0; exitFilledQty = 0;
            }
            lastCumProfit = cum;
            if (inPosNow && plan != null)
            {
                if (plan.OnBarClosed(CurrentBar, High[0], Low[0]))
                {
                    if (LogSignals)
                        Print($"{openEt:yyyy-MM-dd HH:mm} +1R touched -> SL to breakeven {plan.StopPrice}");
                    if (ShowLabels)
                        Draw.Text(this, "be" + sigSeq, false, "SL → BE " + Fp(plan.StopPrice), 0,
                            plan.IsLong ? Low[0] : High[0], plan.IsLong ? 22 : -22,
                            Brushes.Black, fontSmall, TextAlignment.Center, brBeBg, brBeBg, 90);
                }
            }

            bool pendingEntry = entryOrder != null && IsWorking(entryOrder);
            var host = new HostState
            {
                InPosition = inPosNow,
                PendingEntry = pendingEntry,
                RealizedDayPnl = cum - realizedDayBase,
                RealizedDayPnlForCap = cum - realizedDayBase,   // NT8: one account figure for both guards
            };

            var o = core.OnBar(bar, host);
            eodActive = o.FlatNow;      // read by OnExecutionUpdate for sneak fills

            if (o.FlatNow)
            {
                // pine 1147-1156: cancel everything, flatten, reset state.
                // NOTE: `plan` is NOT cleared on the cancel request — a limit
                // can still fill while the cancel is pending (NT8 CancelOrder
                // doc); plan is cleared only when the order reaches a terminal
                // state with the position flat (OnOrderUpdate) or the position
                // closes. A sneak fill here gets exits from OnExecutionUpdate
                // and is flattened on the next (still FlatNow) bar.
                if (pendingEntry) CancelOrder(entryOrder);
                if (inPosNow)
                {
                    if (Position.MarketPosition == MarketPosition.Long) ExitLong("EOD", SigLong);
                    else ExitShort("EOD", SigShort);
                }
            }
            else
            {
                // limit expired unfilled (pine 1124-1129): next bar opens past
                // deadline. Same cancel-race rule: plan survives the request.
                if (pendingEntry && !inPosNow && bar.AbsMin + 1 > pendingDeadline)
                {
                    CancelOrder(entryOrder);
                    if (LogSignals) Print($"{openEt:yyyy-MM-dd HH:mm} limit expired unfilled (30 min) — cancelled");
                }

                // ---- new signal (pine 1050-1093)
                var s = o.Signal;
                if (s != null && !s.Taken)
                {
                    if (LogSignals)
                        Print($"{openEt:yyyy-MM-dd HH:mm} signal skipped ({s.SkipReason}) {(s.IsLong ? "long" : "short")} grade {s.Grade} bits {s.Bits}");
                    if (ShowLabels)
                    {
                        sigSeq++;
                        // compact grey tag (pine "no trade" label); details in the Output log
                        Draw.Text(this, "skip" + sigSeq, false, (s.IsLong ? "▲ " : "▼ ") + "no trade · " + SkipText(s.SkipReason),
                            0, s.IsLong ? Low[0] : High[0], s.IsLong ? 18 : -18,
                            Brushes.WhiteSmoke, fontSmall, TextAlignment.Center, brSkipBg, brSkipBg, 70);
                    }
                }
                if (s != null && s.Taken && entryOrder == null && !inPosNow)
                {
                    plan = BracketPlan.FromSignal(s);
                    planQty = s.Qty;
                    filledQty = 0;
                    exitFilledQty = 0;
                    pendingDeadline = s.DeadlineAbsMin;
                    double lim = Instrument.MasterInstrument.RoundToTickSize(s.Level);
                    Order ret = s.IsLong
                        ? EnterLongLimit(0, true, s.Qty, lim, SigLong)
                        : EnterShortLimit(0, true, s.Qty, lim, SigShort);
                    // Order updates can fire SYNCHRONOUSLY inside the submit
                    // call (NT8 advanced order handling) — OnOrderUpdate's
                    // name-capture is authoritative; only adopt the returned
                    // object when it is still alive, so a synchronous
                    // rejection cannot wedge a dead order into entryOrder.
                    if (ret != null && IsWorking(ret)) entryOrder = ret;
                    if (LogSignals)
                        Print($"{openEt:yyyy-MM-dd HH:mm} {(s.IsLong ? "LONG" : "SHORT")} {s.Qty} lim {s.Level} SL {s.Stop} TP {s.Tp2} (2R all-out, BE at +1R {s.Tp1}) grade {s.Grade} bits {s.Bits}");
                    if (ShowLabels)
                    {
                        sigSeq++;
                        // TV-style boxed label (pine label.new style label_up/down):
                        // filled background, white bold text, hung below the bar for
                        // longs / above for shorts, plus an arrow at the bar itself
                        string txt = (s.IsLong ? "LONG " : "SHORT ") + s.Qty + " @ " + Fp(s.Level)
                            + "\nSL " + Fp(s.Stop) + "   TP " + Fp(s.Tp2) + " (2R, all out)"
                            + "\nSL→BE at +1R (" + Fp(s.Tp1) + ")"
                            + "\n✓ " + Why(s.Bits)
                            + "\ngrade " + s.Grade + " · limit good 30 min";
                        Draw.Text(this, "sig" + sigSeq, false, txt, 0,
                            s.IsLong ? Low[0] : High[0], s.IsLong ? 60 : -60,
                            Brushes.White, fontLabel, TextAlignment.Left,
                            s.IsLong ? brLongBg : brShortBg, s.IsLong ? brLongBg : brShortBg, 88);
                        if (s.IsLong) Draw.ArrowUp(this, "arr" + sigSeq, false, 0, Low[0] - 2 * TickSize, Brushes.LimeGreen);
                        else Draw.ArrowDown(this, "arr" + sigSeq, false, 0, High[0] + 2 * TickSize, Brushes.OrangeRed);
                    }
                }

                // ---- exits (pine 1132-1134): (re)submit stop + 2R target at
                // every bar close while in position; a BE move re-prices the stop
                if (inPosNow && plan != null)
                    SubmitExits();
            }

            DrawLevelsAndStatus(bar, o, openEt, inPosNow, pendingEntry, cum - realizedDayBase);

            wasInPos = inPosNow;
            lastDayId = bar.DayId;
        }

        protected override void OnExecutionUpdate(Execution execution, string executionId,
            double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)
        {
            if (execution.Order == null) return;
            string name = execution.Order.Name;
            if (name == "TP" || name == "SL" || name == "EOD")
            {
                exitFilledQty += quantity;
                return;
            }
            if (name != SigLong && name != SigShort) return;
            // An entry EXECUTION is a fill no matter what the order's final
            // state says — NT8 documents Cancelled-with-Filled>0 for a limit
            // that filled while its cancel was pending. Never filter on state.
            filledQty += quantity;
            if (plan == null)
            {
                // defense-in-depth: an entry fill with no plan must never ride
                // unprotected — flatten immediately and say so loudly.
                Print("P1FastLucidFunded25kRminusC3: entry execution arrived with no active plan — emergency flatten.");
                if (marketPosition == MarketPosition.Long) ExitLong("EOD", SigLong);
                else if (marketPosition == MarketPosition.Short) ExitShort("EOD", SigShort);
                return;
            }
            if (eodActive)
            {
                // sneak fill during the 15:55/12:55 cancel: hard-flat NOW —
                // waiting for another bar could hold through the session break
                if (plan.IsLong) ExitLong("EOD", SigLong);
                else ExitShort("EOD", SigShort);
                return;
            }
            // attach protective exits the moment the entry fills so a live
            // position is never naked during the fill minute (the bar-close
            // resubmission in OnBarUpdate keeps them current afterwards)
            SubmitExits();
        }

        protected override void OnOrderUpdate(Order order, double limitPrice, double stopPrice,
            int quantity, int filled, double averageFillPrice, OrderState orderState,
            DateTime time, ErrorCode error, string comment)
        {
            // capture the entry order BY NAME — updates can arrive before the
            // submission call returns (NT8 advanced order handling)
            if (order.Name == SigLong || order.Name == SigShort)
            {
                if (orderState == OrderState.Cancelled || orderState == OrderState.Rejected
                    || orderState == OrderState.Filled)
                {
                    entryOrder = null;
                    // cancel-race resolution: discard the plan ONLY when the
                    // order itself reports zero filled quantity (order.Filled
                    // is authoritative here; our filledQty / Position can lag
                    // the execution callback) and we are flat.
                    if ((orderState == OrderState.Cancelled || orderState == OrderState.Rejected)
                        && order.Filled == 0 && filledQty == 0
                        && Position.MarketPosition == MarketPosition.Flat)
                        plan = null;
                }
                else
                    entryOrder = order;
            }
            if (orderState == OrderState.Rejected)
                Print($"P1FastLucidFunded25kRminusC3: ORDER REJECTED {order.Name} — check account/connection. {comment}");
        }

        private void SubmitExits()
        {
            if (plan == null) return;
            // remaining = entry fills minus exit fills (execution-accumulated,
            // per NT8 partial-fill guidance); Position.Quantity only as a
            // fallback when execution accounting is not available yet
            int qty = filledQty - exitFilledQty;
            if (qty <= 0) qty = Position.Quantity;
            if (qty <= 0) return;
            // NT8/exchange orders must sit on the tick grid; the method's
            // ATR-derived stop and 2R target are off-grid by construction.
            // Nearest-tick rounding here is the divergence class the tick-grid
            // probe measured (raw $18,563 -> rounded $15,328 on 2023-08..2026-07).
            double stop = Instrument.MasterInstrument.RoundToTickSize(plan.StopPrice);
            double target = Instrument.MasterInstrument.RoundToTickSize(plan.TargetPrice);
            if (plan.IsLong)
            {
                ExitLongLimit(0, true, qty, target, "TP", SigLong);
                ExitLongStopMarket(0, true, qty, stop, "SL", SigLong);
            }
            else
            {
                ExitShortLimit(0, true, qty, target, "TP", SigShort);
                ExitShortStopMarket(0, true, qty, stop, "SL", SigShort);
            }
        }

        // ---------------------------------------------------------- drawings
        // Display only — nothing here feeds signals, sizing or exits.
        private void DrawLevelsAndStatus(BarInput bar, BarOutput o, DateTime openEt,
            bool inPosNow, bool pendingEntry, double dayPnl)
        {
            if (ShowLevels && core.OrDone && !double.IsNaN(core.OrHi))
            {
                if (orDayId != bar.DayId) { orDayId = bar.DayId; orStartBar = CurrentBar; }
                int span = CurrentBar - orStartBar;
                if (span >= 0 && bar.Tmin < 960)
                {
                    // one line pair per day, re-drawn (same tag) to extend to the current bar
                    Draw.Line(this, "orh" + bar.DayId, false, span, core.OrHi, 0, core.OrHi,
                        Brushes.DodgerBlue, DashStyleHelper.Solid, 1);
                    Draw.Line(this, "orl" + bar.DayId, false, span, core.OrLo, 0, core.OrLo,
                        Brushes.DodgerBlue, DashStyleHelper.Solid, 1);
                    if (span == 0)
                        Draw.Text(this, "ort" + bar.DayId, false, "OR " + Fp(core.OrHi) + " / " + Fp(core.OrLo),
                            0, core.OrHi, -14, Brushes.White, fontSmall, TextAlignment.Center, brOrBg, brOrBg, 85);
                }
            }
            if (ShowLevels && !double.IsNaN(core.Pdh) && bar.Tmin == 570)
            {
                // one marker per day (at the 09:30 bar) for the prior-day / overnight levels
                Draw.Dot(this, "pdh" + bar.DayId, false, 0, core.Pdh, Brushes.Silver);
                Draw.Dot(this, "pdl" + bar.DayId, false, 0, core.Pdl, Brushes.Silver);
                if (!double.IsNaN(core.Onh))
                {
                    Draw.Dot(this, "onh" + bar.DayId, false, 0, core.Onh, Brushes.SlateGray);
                    Draw.Dot(this, "onl" + bar.DayId, false, 0, core.Onl, Brushes.SlateGray);
                }
            }
            if (!ShowStatus) return;
            string state;
            if (o.FlatNow) state = "FLAT WINDOW (15:55 / 12:55 early close) — no entries";
            else if (inPosNow) state = "IN TRADE — SL " + (plan != null ? Fp(plan.StopPrice) : "?") + (plan != null && plan.BeMoved ? " (BE)" : "")
                                       + " · TP " + (plan != null ? Fp(plan.TargetPrice) : "?");
            else if (pendingEntry) state = "LIMIT WORKING @ " + (plan != null ? Fp(plan.EntryPx) : "?") + " · SL " + (plan != null ? Fp(plan.StopPrice) : "?") + " · TP " + (plan != null ? Fp(plan.TargetPrice) : "?");
            else if (!core.OrDone) state = "opening range builds 09:30–09:35 ET";
            else state = "WAITING — ▲ " + (core.LongArmed ? "armed @ " + Fp(core.OrHi) : "used, re-arms on 5m close < " + Fp(core.OrHi))
                       + "  ▼ " + (core.ShortArmed ? "armed @ " + Fp(core.OrLo) : "used, re-arms on 5m close > " + Fp(core.OrLo));
            bool dayStopHit = DailyStopR > 0 && dayPnl <= -DailyStopR * RiskUsd;
            bool dayCapHit = DayCapUsd > 0 && dayPnl >= DayCapUsd;
            string txt = "P1 FAST R_minus_c3_count3 M1be $400 · $" + RiskUsd + " · cap " + CapQty + " · " + AccountLabel
                + "\n" + openEt.ToString("yyyy-MM-dd HH:mm") + " ET  ·  " + state
                + "\nday PnL $" + dayPnl.ToString("0") + " · trades " + tradesToday + " (W " + winsToday + ")"
                + (dayStopHit ? "  ·  DAILY STOP HIT — done for today" : "")
                + (dayCapHit ? "  ·  DAY CAP BANKED — done for today" : "")
                + "\nATR5 " + (double.IsNaN(core.Atr5) ? "—" : core.Atr5.ToString("0.00"))
                + " · RSI5 " + (double.IsNaN(core.Rsi5) ? "—" : core.Rsi5.ToString("0.0"))
                + " · PDH/PDL " + Fp(core.Pdh) + "/" + Fp(core.Pdl)
                + "\nsim ≠ live · fill model optimistic · not TV-verified";
            Draw.TextFixed(this, "p1status", txt, TextPosition.TopLeft, Brushes.WhiteSmoke, fontPanel,
                Brushes.DimGray, brPanelBg, 82);
        }

        private string Fp(double px) => double.IsNaN(px) ? "—" : px.ToString("0.00");

        // frozen solid brushes are required for drawing objects rendered on
        // NT8's UI thread from the strategy thread
        private static Brush MakeBrush(byte r, byte g, byte b)
        {
            var br = new SolidColorBrush(Color.FromRgb(r, g, b));
            br.Freeze();
            return br;
        }

        private static string Why(string bits)
        {
            var parts = new List<string>();
            if (bits.Length >= 6)
            {
                if (bits[0] == '1') parts.Add("1H bias");
                if (bits[1] == '1') parts.Add("VWAP band");
                if (bits[2] == '1') parts.Add("volume");
                if (bits[3] == '1') parts.Add("sweep");
                if (bits[4] == '1') parts.Add("HTF level");
                if (bits[5] == '1') parts.Add("room");
            }
            return string.Join(" · ", parts);
        }

        private static string SkipText(string reason)
        {
            switch (reason)
            {
                case "gradeC": return "setup too weak (grade C)";
                case "conf-count": return "too few counted confluences";
                case "in-pos": return "already in a trade / limit working";
                case "guardrail": return "daily stop / day cap hit — done for today";
                case "window": return "outside trade window (check input)";
                case "size": return "stop too wide for $ risk";
                case "sl-tight": return "stop too tight";
                default: return reason;
            }
        }

        private static bool IsWorking(Order o) =>
            o.OrderState == OrderState.Working || o.OrderState == OrderState.Accepted
            || o.OrderState == OrderState.Submitted || o.OrderState == OrderState.ChangePending
            || o.OrderState == OrderState.ChangeSubmitted || o.OrderState == OrderState.PartFilled
            || o.OrderState == OrderState.CancelPending || o.OrderState == OrderState.CancelSubmitted
            || o.OrderState == OrderState.TriggerPending;

        private DateTime ToEastern(DateTime t)
        {
            if (easternTz == null) return t;
            var src = sourceTz ?? TimeZoneInfo.Local;
            if (src.Id == easternTz.Id) return t;
            return TimeZoneInfo.ConvertTime(DateTime.SpecifyKind(t, DateTimeKind.Unspecified), src, easternTz);
        }

        private long AbsMin(DateTime etWallClock) => etWallClock.Ticks / TimeSpan.TicksPerMinute;

        private static TimeZoneInfo FindTz(params string[] ids)
        {
            foreach (var id in ids)
                try { return TimeZoneInfo.FindSystemTimeZoneById(id); } catch { }
            return null;
        }

        #region Properties
        [NinjaScriptProperty, Range(25, double.MaxValue)]
        [Display(Name = "Risk $ per trade (1R, flat)", GroupName = "1. Method", Order = 1,
            Description = "P1 Fast reBreak M1be $325 = the battery cell on funded-pro-25k. Flat every trade; qty = floor(risk / stop$) capped below. Do not change without a new parity run.")]
        public double RiskUsd { get; set; }

        [NinjaScriptProperty, Range(0, 1000)]
        [Display(Name = "Max contracts cap (0 = uncapped)", GroupName = "1. Method", Order = 2,
            Description = "LucidPro 25K hard maximum = 20 micros. PART OF THE METHOD (the cap truncates tight-stop trades).")]
        public int CapQty { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Re-arm rule", GroupName = "1. Method", Order = 3,
            Description = "ReBreak (method): a used direction re-arms when a completed 5m close is strictly back inside the OR level; unlimited re-arms per day. Off = one signal per direction per day (the eval port).")]
        public RearmMode Rearm { get; set; }

        [NinjaScriptProperty, Range(0, 100)]
        [Display(Name = "Daily stop (R, 0 = off)", GroupName = "1. Method", Order = 4,
            Description = "No NEW entries after realized day PnL <= -N x R (method: 2.0 = -$650). The engine reference book 14767 ran 2.0.")]
        public double DailyStopR { get; set; }

        [NinjaScriptProperty, Range(0, double.MaxValue)]
        [Display(Name = "One-winner day cap $ (0 = off)", GroupName = "1. Method", Order = 5,
            Description = "No NEW entries once realized day PnL >= this. The reference book has NO cap (0) — leave 0 for any parity/backtest run. Lucid's 40% consistency rule is a payout-eligibility gate, not a breach; a cap of 40% of your cycle target keeps you eligible (e.g. $440 for the $1,000 first payout at 25,000 start) at the cost of upside.")]
        public double DayCapUsd { get; set; }

        [NinjaScriptProperty, Range(0, 6)]
        [Display(Name = "Take rule: min counted bits (0 = grade A/B)", GroupName = "1. Method", Order = 8,
            Description = "0 (this preset): take iff grade A/B (>= 4 of the 6 confluence bits). N > 0: take iff at least N of the bits named below are set (engine take_policy = count:N) — used by the R_minus_c2/c3_count3 presets.")]
        public int TakeCount { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Take rule: counted bits", GroupName = "1. Method", Order = 9,
            Description = "Comma list of the bits the count rule counts (c1 1H bias, c2 VWAP band, c3 5m volume, c4 sweep, c5 HTF level, c6 room). Ignored when the min-count above is 0.")]
        public string ConfCountBits { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "RSI(14) 5m gate", GroupName = "1. Method", Order = 6,
            Description = "NotOB (method): block longs when 5m RSI14 > 70 / shorts when < 30 on the breakout bar; direction is NOT consumed by a gated bar.")]
        public RsiGateMode RsiGate { get; set; }

        [NinjaScriptProperty, Range(0, 5)]
        [Display(Name = "Stop pad (x ATR 5m)", GroupName = "1. Method", Order = 7)]
        public double PadAtr { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Account label (display only)", GroupName = "2. Account", Order = 1,
            Description = "Shown in the status box and log. Does NOT route orders — the account is chosen in the strategy dialog / chart trader. This preset: LTF02582991480003.")]
        public string AccountLabel { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Trade window start (ET)", GroupName = "3. Window", Order = 1)]
        public DateTime TradeFrom { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Trade window end (ET)", GroupName = "3. Window", Order = 2)]
        public DateTime TradeTo { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Bars time zone id (blank = PC local)", GroupName = "4. Plumbing", Order = 1,
            Description = ".NET time zone id of the bar timestamps, i.e. the zone in NT8 Tools > Options > General. Leave blank when that setting matches your PC clock.")]
        public string BarsTimeZoneId { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Log signals to output", GroupName = "4. Plumbing", Order = 2)]
        public bool LogSignals { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Big plain-English trade labels", GroupName = "5. Display", Order = 1)]
        public bool ShowLabels { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Plot OR / PDH / PDL / ONH / ONL levels", GroupName = "5. Display", Order = 2)]
        public bool ShowLevels { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Status box (what to do now)", GroupName = "5. Display", Order = 3)]
        public bool ShowStatus { get; set; }
        #endregion
    }
}
