← rule50ai.com — every rule the machine trades under, synced from the live system · updated 2026-11-08
Every trade needs a written thesis with entry, stop, target, and an invalidation condition before the order goes in.
framework design, 2026-07-07
Honor the journaled stop. Exiting late because "it might come back" is a rule violation even if it works.
framework design, 2026-07-07
Limit orders only (paper trading doesn't reliably support market/stop orders); place exits proactively rather than assuming a stop will fire.
moomoo API docs, 2026-07-07
Never average down a losing position. Adding to a loser is a new thesis pretending to be the old one. Not formally scoped to specific strategies in the docs — applies as general trading discipline whenever an add is being considered.
prior, 2026-07-07
Check the spread before entry: if (ask − bid) > 0.5% of price, skip or size down — paper fills won't show the real cost but live behavior is what we're training. Not formally scoped to specific strategies — general execution discipline.
prior, 2026-07-07
Know WHY it's up before buying the score: watchlist leaders are gap movers — earnings, FDA, buyouts. A +50%-style mover is halt-prone (LULD); halve size and widen the stop or skip. Binds news-catalyst strategies. Explicitly WAIVED for rumor-momo (R15) — the tape itself is the thesis there instead.
prior, 2026-07-07
Respect the stop-guard: it executes journaled stops/targets between sessions. Don’t cancel its exit orders without a written reason, and never leave a position without a journaled stop. A live position may use only its latest still-open direction-matching plan: filled close quantity is netted so partial exits retain protection, fully closed/reviewed episodes end the plan, and terminal zero-fill attempts are ignored. When the current plan is missing, fail closed with no-journaled-stop rather than reuse a completed episode’s stop. The append-only journal itself also fails closed: malformed canonical JSON is never treated as empty, and every save rebases under a cross-process lock so a stale writer cannot erase newer rows, lessons, or completed review fields.
framework design, 2026-07-07
Paper fills are optimistic (touch = fill, no slippage/queue). Treat marginal setups as losers-in-live; demand edge beyond a tick. Not formally scoped to specific strategies — general caution applied when judging any setup's quality.
prior, 2026-07-07
Momentum entry orders don't rest. If a breakout/momo buy isn't filled while the trigger is live, cancel it — a resting buy limit above a stalled tape fills exactly when momentum breaks (adverse selection). Re-enter on a fresh trigger instead. Every playbook binds this in addition to the safety core.
journal trade 7a24c484 (PENG, 2026-07-08)
Breakout entries need demand evidence, not just a level: require a confirmation hold above the trigger (≥5 min) or a successful retest, AND volume pace ≥2x — defined as 2x GREATER THAN NORMAL FOR THIS TIME OF DAY (time-adjusted pace vs a full-day average). First-touch buys of a breakout level are banned. Mid-day breaks (after ~10:30 CT) on <2x volume are skips. Binds only the breakout playbooks — the signal playbooks trade their indicator without a volume gate. Explicitly NOT waived for rumor-momo.
earned 2026-07-08: UMC, VG, BZH losses
A name with a REAL news catalyst (earnings, contract win, guidance raise, FDA, index add, M&A) being bought mechanically by institutions is tradeable as a continuation — signature is algo-style accumulation: price holds above VWAP essentially the whole session, the tape stair-steps with shallow pullbacks, and volume stays elevated for hours (not one opening spike). Entry does not need a fresh breakout level: buy the pullback-to-VWAP that holds, or the break of a tight upper consolidation. Stop below VWAP or the consolidation low. R12 (never rest the order) and R13 (volume evidence) still apply; what this waives is only the fresh-trigger-level requirement. One re-entry allowed per amended R8 if the FULL pattern re-qualifies from scratch.
human directive, 2026-07-09
An unexplained mover MAY be bought without a confirmed catalyst — the tape itself is the thesis. Constraints: technicals must still qualify (above VWAP, R13 demand evidence NOT waived); HALF normal size, max 4% equity risk (raised from 0.5%, 2026-07-15); mechanical entry — break of the post-OR15 high with hold/retest, skip if already >1.5× ATR(5m) above that level; stop = tighter of VWAP or OR15 low, wick-buffered, never widened; target ≥2R; exit immediately on a denial headline or a >10-min VWAP loss. Waives only R9's know-why requirement. Journal with strategy tag rumor-momo.
human directive, 2026-07-09; mechanized 2026-07-15
The stop-guard enforces exactly one thing between sessions: last price ≤ stop (or ≥ target). It has no VWAP, indicator, or elapsed-time awareness. Any invalidation phrased as "loses VWAP for >10 min" or similar will never fire on its own — convert the invalidation into the single price that expresses it, and journal THAT as the stop. If you genuinely need a wider stop, the invalidation is wrong, not the stop.
earned 2026-07-09: BBIO eff847ac/c12c137d
Stops are NOT percentage-based. Procedure: (1) identify the structure whose break kills the thesis — swing low, VWAP, OR level, prior-day high, SuperTrend line, or the playbook's other explicitly named structure; (2) place the stop a wick buffer below it — max(0.25×ATR5m, 0.15%); (3) choose that structural stop independently, then size to the smaller of R21's notional allocation and R32's flat-$100 full-stop cap. The one forbidden placement is BETWEEN levels — below the invalidation but above real support. pm-break (fixed 2026-08-06): PMH is its own named invalidation (entry_safety.r17_structural_stop applies this rule's buffer to it); a data/fetch failure skips the entry rather than inventing a stop. SUPERSEDED IN PART 2026-08-07 by R47: the PRIMARY stop basis is now entry -/+ 2.5 x ATR(14) on the 5m trading timeframe, system-wide including the pm-break daemon. R17 is not struck -- it is the fallback when live ATR5m is unavailable, and its no-man's-land prohibition, R29 clearance and skip-rather-than-widen discipline remain operative on top of the R47 basis.
human directive 2026-07-09 + BBIO postmortem; R32 wording synchronized 2026-07-29; pm-break enforcement auto-applied 2026-07-30; stop basis superseded in part by R47, human directive 2026-08-07
A strategy or rule is only judged on a sample: minimum 15 journaled attempts (raised from 10) before it can be suspended, promoted, or declared failed. Every LOSS review must include a "what would have made this profitable" counterfactual. Failed rules/strategies are struck through but STAY listed with the evidence — never deleted, so the same idea is never re-implemented blind. One rule-adjustment per review cycle per strategy. Governs how every strategy in the roster gets evaluated. AMENDED 2026-08-11 (human directive): `testing` is a real, DERIVED status -- the dashboard's old `untracked` bucket is removed. strategy_lifecycle.json is a FAILURE registry, so a tag with no row has simply never failed; derive_status() resolves it from live episode count (testing below JUDGMENT_MIN_EPISODES=TRADE_BUDGET=15, active at/above). An existing record always wins; derivation never yields a terminal state. Refreshed each EOD into the sibling `derivedStatuses` key and never materialised as an FSM record -- storing it would make reopen_initial_failures() skip the tag and silently disable R18 demotion (measured: 1 transition without a stored record, 0 with one). EXTENDED 2026-08-11 (human directive): a fourth answer exists alongside active/testing/dead -- `non-entry`, the ABSENCE of a lifecycle status. NON_ENTRY_TAGS in tools/strategy_lifecycle.py: top10 (display tracker), chandelier (role:exit-overlay, an EXIT construct), unknown (R46 failed-attribution sentinel), unintended-oversell (moomoo_sync incident label for a SELL on a non-SHORT code). derive_status() short-circuits to non-entry for these ahead of any stored record, and reopen_initial_failures() skips them so an incident bucket cannot reach 15 episodes and be judged as a strategy. Distinct from an R45 alias: an alias IS a strategy pooling evidence into another's verdict; a non-entry tag has no verdict to pool. pm-break was deliberately classified as NEITHER -- it is role:entry with automation.enabled and fails R45's same-gate test against tj-pmh (different automation.kind, owner opening_bell.py vs stop_guard.py, entry window, and candidate source), so it derives as an ordinary testing strategy. ACTIVE-DECAY (2026-08-15): after the cycle that earned ACTIVE, later attempts are scored in complete non-overlapping 15-episode batches; two consecutive negative equal-weight batches reopen the R18/R43 state machine. R34-locked strategies are exempt. STRADDLE AMENDMENT (2026-08-16, human directive): a batch spanning the moment a strategy-targeted fix went live is mixed evidence and does not count toward escalation - it is dropped and escalation waits for wholly post-fix batches, which delays escalation but never blocks it. Code fixes are invisible to journal-derived improvement detection, so they must stamp fixEffectiveAt via strategy_lifecycle.mark_fix_effective(); the marker is cleared on any decay transition. FIRST-CYCLE ARTIFACT/TAIL REPAIR (2026-09-12, auto-applied): a record-less tag whose 15+ sample is carried by one best episode remains derived TESTING under sibling `provisionalInitialVerdicts` until ex-top evidence is non-negative or a later episode makes the full normalized sample negative. Provisional tags are excluded from the scanner's already-judged ACTIVE set but never materialized as FSM records, preserving the ordinary failure/retest transition.
human directive 2026-07-09
High watchlist scores are themselves trade triggers. Tier 1 (score ≥90, FIRST 30 MINUTES ONLY — window added 2026-08-07 by human directive): any stock reaching score ≥90 inside minutes 0-30 of the regular session is taken; outside that window Tier 1 does not trade at all. Stop WIDER than standard (1.0×ATR5m buffer), half-standard 2.5% notional allocation, tag score90. The 0-30 window REPLACES R31's time-of-day windows for this tag (tool-enforced at stop_guard.SCORE90_OPEN_WINDOW inside r31_time_pm_gate; skip reason `r31-score90-open-window`); R31's premarket 2-8% trap-zone veto still binds it. The watchlist that computes the score rebuilds every 5 minutes 08:33-09:00 CT ("Kitty Watchlist Open5min") so a mid-window crossing is actually sampled. Tier 2 (score >80 AND volume ≥2x time-adjusted pace): take the trade, standard R17 stop and standard 5% notional allocation, tag score80 — unchanged, still R31-timed, no 30-minute window. Both allocations are reduced when R32's flat $100 max-loss cap binds. Gap fast-track: a news-coupled gapper up >5% with volume trending above normal pace = take immediately, tag gap-go.
human directives, 2026-07-09/10; fast-track threshold lowered 10%→5% by human directive 2026-07-26; sizing wording synchronized with R32 2026-07-29; Tier 1 restricted to the first 30 minutes by human directive 2026-08-07 (same change dropped a stale "R5's first-5-min ban still holds" clause — R5 was removed 2026-07-23)
The cap is on the SYMBOL'S COMBINED position, never per-order. A flat symbol may open an initial position up to 7.5% of equity (the 5% base strategy allocation x R44's maximum 1.5x multiplier); the combined position may then be grown by pyramiding to at most 10% of equity (R28, re-enabled 2026-08-07). Smaller strategy allocations remain smaller. The initial cap sits strictly BELOW the combined cap on purpose: maximum exposure to one name can only be reached by ADDING to a position that is already winning, never expressed in one shot on an unproven trade. sizing.symbol_headroom_notional/cap_adjusted_buy_qty enforce it in both stop_guard.py and moomoo_trade.py.
human directive 2026-07-10; amended (hard rule) 2026-07-14; cap reduced 10%→5% by human directive 2026-07-23
OPERATIVE AGAIN 2026-08-07 with R28's pyramiding reversal: every authorized LONG add journals one stop for the entire symbol at the wider of the existing core stop and the new tranche stop. The live-profit, non-growing-tranche, pending-add, and 10% combined-position gates run before broker submission; a missing core stop fails closed.
earned 2026-07-10: RXT 4f0fce75/4326fafd; reactivated with R28 by human directive 2026-08-07; incomplete rollout repaired by recurring Codex action-log review 2026-08-11
No sell may exceed unreserved held LONG qty and no cover may exceed unreserved open SHORT qty; working closes reserve their full quantity until terminal. A BUY that reduces/closes an existing short is exit management, not a new entry, and bypasses halt/entry-cutoff/universe checks only while an open SHORT exists; a declared cover with no SHORT to close would create new LONG exposure and is refused as nothing-to-cover. ENFORCED 2026-10-29 (note-376): a BUY submitted as a NEW ENTRY against a live SHORT is refused as undeclared-cover before headroom, so live position state alone cannot grant the risk-reducing bypass. stop_guard.guarded_cover() and audited programmatic exits declare strategy_entry=False; declared covers retain the unchanged bypass notice and overcover precedence. flatten covers both directions under one cross-process lock and rate-limit batches close orders. If a second authorized flattener times out on that lock, it refreshes broker state before stale-close review; only a broker-confirmed terminal-state cancel race is benign, while working-state or transport cancel failures remain actionable. Exit-pending masking is bounded for EVERY working close, not just the daemon's own (ENFORCED 2026-10-12). A working close parks its position in exit-pending, which check_cycle() skips in BOTH the fixed-stop pre-pass and the rich exit pass, so while it rests that position's journaled stop is not compared to any price. PROTECTIVE_EXIT_STALE_SECONDS = 60 bounds that window only for orders journaled strategy stop-guard, and stale_eod_close_orders() only for eod-flatten; every other close (risk-repair, close-carry, fault-recovery, any model- or hand-placed exit) masks its position for as long as it rests, with no bound at all. The rule: no open position may be skipped by both stop passes beyond a bounded window, and when a working close is what masks it, that window is bounded and the order is then CANCELLED, never re-priced, so the journaled stop re-arms against a live price on the next cycle. eod-flatten stays out (its own 90s owner coordinates the flatten lock), an order with no parseable broker create_time is still never cancelled, and a position masked past the bound logs its own event instead of appearing only as a substring inside a heartbeat summary. Evidence: US.AXTI 2026-10-12 SHORT -71 @ 64.92 with a journaled cover-stop of 66.63 reported US.AXTI:exit-pending from 13:39:51Z while every other position in the same heartbeat reported a live stop comparison; the masking order (785814, fault-recovery, BUY 71 @ 65.02, placed by the opening session during a wrongful R24 fault) was an ordinary away-from-market limit 0.83 below a 65.85 market -- corrected 2026-10-12: it was NOT permanently unfillable as first recorded, it reached FILLED_ALL 71/71 and the episode closed at -1.77. The hazard is that the mask is ASYMMETRIC: a resting away-from-market cover can only fill on a favourable move, while the journaled adverse-side stop is compared to no price at all, was structurally ineligible for cancellation, and its full-quantity R24 reservation (canSellQty 0.0) blocked any second exit. Same failure mode as BFLY 2026-09-22 (347 consecutive exit-pending cycles, -9.89R / -$365.66 instead of -1R) arriving through the one door that fix left open. ENFORCED 2026-10-12: MANAGEMENT_EXIT_STALE_SECONDS = 300 gives non-guard/non-eod closes 20 normal poll cycles (five times the daemon's own 60-second bound) before cancellation-only recovery. stop-guard remains exclusively on PROTECTIVE_EXIT_STALE_SECONDS = 60; eod-flatten remains on its coordinated 90-second owner; an unparseable broker create_time never causes a cancel. stale-management-exit-cancelled records the position, order id, journaled stop, live price, age and consecutive pendingCycles, and the following broker snapshot re-arms normal stop evaluation. Regression: tools/tests/test_stale_protective_exits.py covers SHORT/BUY and LONG/SELL mirrors, eod/guard exclusions, timestamp refusal, the verbatim AXTI check_cycle path, and a neutered-bound old-behavior control. Enforcement verified live 2026-10-12 13:11 CT: Stop Guard reloaded via scripts/reload_stop_guard_supervised.ps1, new instance ecfb598f89044a359523a27faf26d0bb healthy with a matching owner lock. See workflows/trading_safety_core.md (R24) and workflows/trading_lessons.md.
earned 2026-07-13: VG naked short; undeclared-cover enforcement auto-applied by FIX session 2026-10-29, note-376
A hit fixed target converts to a tight trailing stop (2%) instead of exiting flat, when the tape still shows momentum, symmetric across both directions since 2026-08-06 — mechanical proxies in stop_guard.py (not LLM judgment): a LONG extends only when price hasn't faded off today's high (within 2%) AND is still net-higher than 5 bars ago; a SHORT extends only when price hasn't bounced off today's low (within 2%) AND is still net-lower than 5 bars ago. The target becomes a tightening stop trigger, not a guaranteed fill floor: quote movement or execution slippage can still produce a fill short of it. Only fires on a FIXED target with no trail already active. ENFORCED 2026-10-06 (overnight review of the 2026-10-05 day): the extension's `target = None` was not durable. moomoo_lib._merge_journal_history() enforces "null never means erase" and restores any field a candidate nulled from the on-disk row unless the caller opts in via the private _clearFields marker, and extend_with_trailing_stop() never did -- so every extension republished the already-hit target. check_cycle() rebuilds its plan straight off the journal row ("target": tr.get("target")) and the LONG/SHORT target branches gate extension on `not plan.get("trailPct")`, so a position already carrying the 2% trail fell through to the FLAT guarded_sell/guarded_cover "target-hit" arm: R25 decided to let a winner run and the next cycle closed it at target anyway. Measured on trade_log.json: 43 of 55 lifetime target-hit-extended events had a non-null target on disk afterwards -- every one from 2026-08-13 on, i.e. everything after the merge rule landed -- and 8 of those 43 were then closed by the FLAT target-hit-exit / short-target-hit-cover arm within 30-65s of their own extension: HL f31181a0 (30s), SIMO 49906966 (32s), KOS f530c6d5 (34s), DKNG ab81d9d4 (39s), METC 1e9c89d2 (53s), CRWV 56a15595 (53s), PURR f593df5a (55s), XNDU 330ccef3 (65s) -- the first four being the ones that also survive the clean-cohort filters used for scoring below. Zero of the 12 older extensions still carrying target: null were closed that way -- perfect separation. OPERATIVE: extend_with_trailing_stop() now sets _clearFields for `target`, so the null persists and the trail genuinely governs. This is the ONLY site in tools/ that nulls a journal trade-row field, so one marker closes the class; _merge_journal_history() carries a comment naming the hazard for the next caller. After an extension the entry row legitimately reads stop = the locked floor and target = null; that is R25 working, not an R24 violation and not a missing target. EVIDENCE-BASIS CORRECTION, no retune: re-derived from closedEpisodes rather than journal lesson prose, the 43 single-lot, same-day, episode-matched extensions are +$146.10 total, mean +$3.40, median +$1.90 (IQR -$2.21..+$9.04, min -$52.02, max +$66.29), 25 helped / 17 hurt / 1 flat -- 34 where the trail actually governed (+$93.84, 18W/15L), 4 reverted-and-flat-exited (+$21.31), 5 stopped/unresolved (+$30.95, OII 367300a2 +$66.29 against INIO dbaa1a31 -$52.02). Twelve are excluded: 8 multi-lot episodes where the row's realizedPnl is the whole episode while the counterfactual is one lot (PURR f593df5a reads +$287.22 purely from that mismatch), and 4 multi-day carries. The 2026-09-26 refresh's +$538.97/51 is dominated by two excluded rows; METC 1e9c89d2, already flagged there as 55.4% of the total, opened 2026-08-31 11:10 and closed 2026-09-03 12:32, so its target branch fired three days late at 12.035 against a 13.44 locked floor -- that +$298.56 measures an R24 flatten failure, not a trail decision, and must not be credited to R25. TIGHT_TRAIL_PCT stays 0.02. HONEST LIMITS: the fix changes future behaviour for ~9% of extensions (4 of 43) and bar-derived MFE cannot simulate what the 2% trail would have done on those four paths, so NO P&L improvement is claimed -- the change rests on the mechanism plus the pre/post separation in the log. The clean cohort's mean is small against its spread, no multiple-comparison correction is applied, and per-episode P&L older than the last successful broker history pull inherits R57's provisional caveat. Safety-core rule: trading_safety_core.md's R25 bullet updated in the same change. Regression: tools/tests/test_r25_target_clear_persists.py (7 cases, 4 driving the real save_journal write path; verified to fail 4/7 with the marker reverted).
human directive, 2026-07-13; TESTING
R18 attempts count unique flat-to-flat episodeId values, not fills. Historical pyramids and partial fills share one episode, one immutable primaryStrategy, and one outcome; overlapping signals are signalOverlays. Gap-go after 10:00 CT requires positive 30-minute trend and at least 1% above VWAP; all gap-go entries reject extension beyond 1.5 ATR from VWAP/trigger. VixFix requires the higher-low breakout. Portfolio exposure and loss-at-stop heat are reported each guard cycle.
multi-session review 2026-07-16: RXT, AXTI/TGB/TSEM, ABT/LCID/IOVA/SKWD/SMA
REVERSED 2026-08-07 (human directive: 'allow pyramiding again up to 10% of the portfolio for each name'). Adds are permitted again, subject to three gates, all tool-enforced: (1) the symbol's COMBINED position may not exceed 10% of equity; (2) the position must currently be IN PROFIT -- adding to a loser is averaging down, which is how FIG reached 125% of equity on 2026-07-16; (3) each add must be NO LARGER than the previous tranche, making the position a true decreasing pyramid rather than a top-heavy cost basis. An unknown unrealized P&L fails closed (no add). An add that overshoots the remaining headroom is trimmed to fit; an INITIAL entry that overshoots is refused outright, never silently resized. sizing.pyramid_add_allowed() + sizing.symbol_headroom_notional().
human directive 2026-07-16: remove all pyramiding; it is not effective; R21 ceiling reduced 10%→5% by human directive 2026-07-23
A stop must clear the LIVE last price by at least 0.5% (stop ≤ last × 0.995 for LONG) — never place a stop inside the marketable-limit band an entry buy is filled through (limit = last × 1.003, plus quote-staleness wiggle). A stop born inside that band means the position can be in breach the instant it fills, and the daemon flattens it next cycle for a guaranteed churn loss with zero thesis. If R17's structural stop sits inside the band, the setup is too tight to trade at a marketable limit — skip or wait for a pullback; don't widen the stop arbitrarily. Tool-enforced (`stop_guard.stop_clears_entry_band()`).
earned 2026-07-17: CAL 7af70880 — stopped out in 60 seconds on a born-in-breach stop
A stopped-out symbol may not re-enter off the SAME cached watchlist row — a freshness gate, not a re-entry cap (R8/R20's struck count-based limits are not reinstated). Blocks re-entry only until the watchlist has rebuilt at least once since the stop-out (`watchlist.json`'s `generatedAt` advances), then imposes no further limit on how many times the symbol may re-qualify. Tool-enforced (`stop_guard.py`'s `_stopped_out` cache / `is_stale_post_stopout()`, reconstructed from the durable journal on daemon restart so a restart cannot erase the block). Not formally scoped to specific strategies in the source doc — applies wherever the deterministic entry guard re-evaluates a symbol.
earned 2026-07-17: CAL, TRVI, SG same-day re-entries off a stale watchlist row
Skip the first 15 minutes after the open and the 60–120-minute late-morning lull; enter only in the 15–60 minute or 120-minute-plus windows. Separately, skip signals whose premarket move at signal time is in the 2–8% band — up a little without being the day's genuine headline mover; a confirmed 2–8% reading vetoes, missing premarket data does not (this rulebook's standing convention: missing data is inert, never a veto). Binds score90, score80, gap-go, rumor-momo, and inst-continuation (`stop_guard.R31_STRATEGIES`) — formerly shared with the now-removed R23, which reused the same roster for an unrelated reason. Tool-enforced (`stop_guard.r31_time_pm_gate`, checked in `guarded_entry()` before sizing). AMENDED 2026-08-07 (human directive): score90 is EXEMPT from the time-of-day half and uses its own R19 Tier 1 window (minutes 0-30 only, stop_guard.SCORE90_OPEN_WINDOW), which both opens the first 15 minutes to it and closes everything after minute 30; the premarket 2-8% half still binds it. Every other tag in the roster keeps the 15-60/120+ rule verbatim.
backtest evidence 2026-07-20 ([internal note]); implemented by human directive 2026-07-24; score90 exempted from the time-of-day half by human directive 2026-08-07
A symbol whose changePct today exceeds 30% may not be bought LONG -- it is too extended, short-only from here. The shared entry_safety.r33_extended_long_veto predicate is enforced by both LONG execution owners: stop_guard.guarded_entry() and the separate opening_bell.py pm-break daemon. Missing change data never vetoes (this rulebook's standing convention: missing data is inert, never a veto). SHORT entries are untouched: they still need their own qualifying bearish setup (e.g. short-gap-open-close) -- this rule only removes the LONG option, it does not auto-short.
human directive, 2026-07-28 ("do not purchase (long) any stock that is up over 30%... those should only be eligible for shorting"); opening-bell propagation auto-applied 2026-07-30
Extends R18. Once a strategy's most recent three completed, non-overlapping 15-trade batches (equal-weight normalized, same basis as R18) are each individually net-positive -- 45+ episodes -- its strategy_lifecycle.json status becomes locked: permanently exempt from promote_r18_failures's initial-judgment path and reassess_retests's retest cycles, though it keeps trading and its parameters may still be refined normally. The check is a trailing window (the most recent three completed batches, not necessarily the strategy's first three). Only a human/manual edit to strategy_lifecycle.json may move a locked strategy off that status. Separately, if a LOCKED strategy's lifetime (all-time) normalized P&L later turns negative, it is NOT auto-demoted or auto-killed -- flaggedForReview is set on its lifecycle record instead, surfaced on the dashboard, and never cleared automatically once set. AMENDED 2026-08-07 (humanLocked): R34's 'only a human moves it' guarantee existed only at status 'locked', which means locked permanently ACTIVE -- a human wanting a strategy retired AND frozen had no way to say so. A new humanLocked: true field on any lifecycle record now freezes it against every automated transition in tools/strategy_lifecycle.py whatever its status (checked first by reassess_retests, close_deadlocked_retests, advance_lock_status and flag_locked_strategies_if_negative; reopen_initial_failures is covered for free). Only a human editing strategy_lifecycle.json clears it. Separately, advance_lock_status() now also skips status 'inactive' on status alone -- it previously skipped only {locked, dead, dead-dead}, so a hand-retired strategy could be promoted back to 'locked' (permanently active) off its frozen history. REAFFIRMED 2026-08-11 (human directive): the lock threshold stays at 3 consecutive net-positive 15-trade batches (45 episodes). Reconsidered alongside the R18/R43 state-model clarification and deliberately left unchanged. AMENDED 2026-08-13 (human directive): for a LOCKED strategy the batch trigger fires on ONE completed negative 15-trade batch, not two consecutive (LOCKED_NEGATIVE_BATCHES_TO_FLAG=1, kept separate from the generic CONSECUTIVE_NEGATIVE_BATCHES_TO_FLAG=2). Locked is the only status no automated path can demote, kill or reopen, so flaggedForReview is its sole safety valve and still takes no action; at two batches it could not fire until 30 trades had passed. Lifetime-negative trigger unchanged as backup. The trigger is a STATE evaluation re-derived from full history every EOD, with no cursor of batches-since-the-change, so it is retroactive by construction - on the live book it flags max-loser-breakdown immediately on its existing -414.92 batch despite a +1135.76 lifetime cushion. A recovery clears it: the window is the last completed batch only. AMENDED 2026-08-28 (human directive): both triggers are evaluated over the tag's CURRENT configuration, not its entire history. For an R43-inverted tag the basis starts at the inversion boundary -- the first episode of the final contiguous same-direction run, derived by strategy_lifecycle.current_config_episodes() rather than persisted, so it needs no migration. The batch trigger re-batches from that boundary too, so no batch straddles the flip and nets negative on original-direction losses. Rationale: R43 inverts a strategy BECAUSE its original direction lost, so pooling those losses back into R34 re-judges a configuration that no longer exists. Measured on the live book 2026-08-28: etf-scalp -648.30 pooled vs +1049.51 current-config, orb-retest -464.02 vs +538.57. Not an amnesty -- the same pass newly surfaced a real negative post-inversion batch on kronos-momentum that pooled batching masked. No-op for non-inverted tags (adx-di unchanged at -1177.53), asserted as test controls in tools/tests/test_r34_inversion_basis.py. AMENDED 2026-09-23 (human directive): nothing is flagged for human review any more -- flaggedForReview is retired. decide_locked_strategies() judges each locked strategy every EOD on its current configuration: latest batch negative with lifetime positive -> WATCH (stays locked, keeps trading, r34Watch self-clears on recovery); last two batches negative, or lifetime negative with the latest batch negative -> RELEASE through the shared R18/R43 transition (fresh retest on a new loss-review improvement, else R43 inversion, else dead-dead subject to R43's session floor). humanLocked is unchanged.
human directive, 2026-07-31 ("after a strategy has strung together 3 consecutive positive 15-trade cycles in a row... mark it as permanently active/green... if it later turns net negative over its all-time/lifetime trade history... flagged for human review"); amended by human directive 2026-09-23 ("allow them to continue to trade and allow the system to make its own determination rather than flag for human review")
Fades the regular-session opening gap back toward yesterday's close, calibrated from a public 10-year NQ gap-fade study. A gap UP is faded SHORT (org-gap-fade-short); a gap DOWN is faded LONG (org-gap-fade-long). Only Tiny/Small/Medium gaps (|gap| < 0.50x daily ATR14) are eligible -- Large/Huge behave like breakaway gaps and the study's fill-rate edge is gone by then. Entry window 8:35-10:30 CT only (the fill-rate edge decays fast after late morning). Primary target is the 50% gap-fill midpoint (prior close + gap/2), reached far more reliably per the source study than a full 100% fill; the LONG side may extend past it via R25's existing generic momentum-gated extension (LONG-only in this codebase), the SHORT side always takes the 50% target flat. Structural stop beyond the gap extreme (today's high/low or the open, whichever is more extreme), buffered by the larger of 0.25x daily ATR14 or 0.15% of price. Day-of-week (the study found Tue/Wed strongest) is NOT a hard filter -- logged for a future R18/weekly-rollup evaluation of whether this system's own sample shows the same pattern, not gated on day one with no in-system evidence.
human directive, 2026-08-01, referencing a public 10-year NQ opening-range-gap study and a public TradingView indicator ("ICT Opening Range Gap" by n00btraders) as the closest available reference for the described (DM-only) original
Kronos-mini forecasts a symbol's near-term OHLCV path from its own live 1-minute bar history (3 independent samples -> direction/magnitude/confidence, confidence = fraction of samples agreeing). Exit side (universal, every open position): check_cycle() tightens (never loosens) the effective stop to 2% of price -- the same tight-trail convention R25 already uses -- on a confident (>=0.67) forecast reversal against the held direction; a soft signal, never a hard exit trigger. Optional entry-confirmation side (kronos_gate on guarded_entry/guarded_short_entry, default False): refuses an independently-qualified entry only on a confident forecast disagreement and fails open on missing data or dependencies. Standalone kronos-momentum side: because Kronos is the entire incremental signal, kronos_require_signal=True fails closed unless the model confidently forecasts UP (>=0.67); missing, flat, low-confidence, error, and opposing outcomes intentionally skip. No already-scored strategy is retroactively changed. Every real forecast is logged for a hit-rate evaluation. Standalone fail-closed repair auto-applied by recurring Codex action-log repair session 2026-08-02 from the rollout contradiction documented in [internal note] DEMOTED TO OBSERVATION-ONLY 2026-08-19 (human directive): Kronos still runs and logs every forecast and judged outcome but no longer vetoes entries or tightens stops (KRONOS_ADVISORY_ONLY in stop_guard.py). Evidence: 7,486 self-judged forecasts, 43.0% directional hit rate (worse than chance); confidence uninformative (1.00 -> 47.5% n=2632 vs 0.67 -> 47.0% n=4179). Not deleted: the 0.33 bucket hits 0.4% on n=675, which looks like a sign/labelling defect worth diagnosing. require_signal=True still fails CLOSED because kronos-momentum has no signal without it - failing open would make it enter unconditionally. Stops can now only be later than under R37, never earlier.
human directive, 2026-08-02; Yu Shi, Zongliang Fu, Shuo Chen, Bohan Zhao, Wei Xu, Changshui Zhang, Jian Li, "Kronos: A Foundation Model for the Language of Financial Markets," arXiv:2508.02739 (AAAI 2026); model: https://github.com/shiyu-coder/Kronos (MIT)
Aggregate dollar risk across every simultaneously open position -- the sum of each position's qty x distance-to-its-effective-stop, i.e. what a correlated gap-down hitting every stop at once would actually cost, not raw notional exposure -- may not exceed 2.5% of equity (PORTFOLIO_HEAT_CAP_PCT). This gates NEW entries only: sizing.portfolio_heat_capped_qty() is one more term in the existing min() cap chain alongside R21/R32/R36, so it can only shrink or refuse a trade, never enlarge one. check_cycle() already computes the aggregate every cycle (heat_risk); entry_cycle() threads it into every guarded_entry()/guarded_short_entry() call. A None value (e.g. check_cycle() itself errored this cycle) skips the gate entirely for that cycle -- fail open, matching R36/R37's own convention. Existing open positions are never force-closed or resized by this rule; it only blocks further new entries until enough positions close to bring the aggregate back under the cap. 2.5% is the midpoint of a requested 2-3% range, cross-checked against R32's flat $100 max-loss cap (implying roughly 25-50 concurrent full-stop-sized positions on $100k-$200k equity before this cap binds). Explicitly reverses the 2026-07-13 rulebook-review decision that added no portfolio heat cap, using the new evidence (the 2026-08-02 tail-risk audit) that decision said would be needed to revisit it.
human directive, 2026-08-02, the third and final fix from [internal note] (Finding #1: no portfolio-level position-count or aggregate-risk ceiling existed); reverses 2026-07-13 rulebook-review Decision 3. See [internal note]
Every EOD pass recomputes which realized losses are IRS wash sales under IRC Section 1091 (loss disallowed if substantially identical stock is acquired within 30 calendar days before or after the loss), the disallowed loss amount, and the adjusted cost basis on the replacement shares. This system trades the same tickers repeatedly, so wash sales are EXPECTED to trigger often -- that is correct bookkeeping, not a bug. detect_wash_sales() is a pure function over trading.json's closedTrades FIFO lot ledger: for a LONG loss the window is centered on the sale date and the trigger is an ACQUISITION (new LONG purchase or SHORT-covering buy); for a SHORT loss the window is centered on the covering (closing) trade date and the trigger is a DISPOSITION (new SHORT sale or LONG sale) -- mirroring the IRS's own short-sale wash-sale guidance, the reverse of the ordinary LONG case. Replacement shares are matched FIFO with proportional disallowance for partial rebuys. This rule never gates, sizes, or otherwise changes any live trading decision -- it is purely a downstream informational report, not part of the operative safety-core rule set. Known limitations: 'substantially identical' is approximated as same ticker symbol (a documented IRS gray area, more so for options/derivatives than the plain shares this system trades); cross-direction replacement basis adjustments are flagged as a less-established case; adjusted basis is not chained across wash-sale generations. Good-faith informational bookkeeping, not a substitute for professional tax software or a CPA.
human directive, 2026-08-02 ("build wash-sale tracking for accurate tax reporting — not avoidance logic, just correct bookkeeping"). See [internal note]
TOGGLEABLE since 2026-08-12 (human directive), currently OFF via DAILY_DEPLOYMENT_CAP=false. R40 is NOT removed -- the rule and all its machinery stay in the codebase so the switch can be flipped back on. WHAT CHANGED: R40 now has an off position. ROTH_ACCOUNT never provided one -- it only selected WHICH ceiling applied (50% of net assets when true, 100%/broker buyingPower when false), so ROTH_ACCOUNT=false still capped, at $162,323 on this account, and that margin branch is what squeezed entries to a single share on 2026-08-12 (SMCI: R21 allowed 133 shares, R38 allowed 6517, R40 allowed 1 on $64 of remaining budget). strategy_lifecycle.daily_deployment_cap_enabled() reads DAILY_DEPLOYMENT_CAP (default false, case-insensitive literal 'true' to enable); entry_cycle() computes deployed_today/daily_cap_dollars only when it is on and passes None otherwise. Switched off after confirming R40 is not load-bearing for compliance here: moomoo_trade.py is hard-locked to TrdEnv.SIMULATE (no real broker to incur a good-faith violation against), ROTH_ACCOUNT=false so the T+1 settlement branch was never live, and buyingPower $162,323 vs $100,196 net assets = 1.62x leverage, i.e. margin, which has no good-faith-violation concept. SET DAILY_DEPLOYMENT_CAP=true TO RE-ARM IT IF THE ACCOUNT EVER NEEDS SETTLEMENT PROTECTION. Historical text: Total NOTIONAL capital (qty x entry price, NOT stop-distance risk -- that's R38) committed to NEW entries in a single calendar day may not exceed 50% of net assets for a Roth/cash account (T+1 settlement rotation -- capital used to buy today isn't available to re-spend until the next business day, so deploying 100% on day one risks a good-faith violation), or 100% of net assets / the broker's own margin buyingPower if larger for a margin account (the default). Driven by the ROTH_ACCOUNT env flag (true/false, case-insensitive, default false). Independent of and simultaneous with R38 -- either can be the binding constraint regardless of the other; this caps capital deployed, R38 caps at-risk dollars from stop distance, a different dimension entirely. entry_cycle() computes deployed_today (gross sum of today's journaled BUY/SHORT notional, not netted against same-day closes) and daily_cap_dollars once per cycle, threaded into every guarded_entry()/guarded_short_entry() call and into sizing.sized_qty()/sized_short_qty() as one more shrink-only min() term. Never touches already-open positions; the budget refills at the next calendar day. Does not disable SHORT selling under Roth mode -- governs sizing only, not direction eligibility.
human directive, 2026-08-04 ("add an account-type-aware daily capital deployment cap, driven by an env flag"). See [internal note]
REINSTATED and AMENDED 2026-08-12 (human directive). Minimum-notional FLOOR: a trade whose fully-capped size falls below 0.5% of equity is SKIPPED entirely, never sized UP. Removed 2026-08-07 alongside R32 on the reasoning that with R32's squeezing gone the floor would reject only deliberately small positions; the data refuted that. Over the five sessions after removal, sub-$500 orders went from 0.6% of all orders (10/1551) to 8.1% (31/384) and 1-share orders from 0.3% to 4.4%. The 33 sub-$500 round-trips ever closed total +$23.21 lifetime (median -$0.01); the 15 closed since removal total -$35.55 - spread and commission drag, not edge. Trigger was a 1-share $37.51 SMCI ticket. The floor must REFUSE and never size up: every other term (R21 notional, R38 heat) is an upper bound, so raising qty to meet a floor would override whichever was binding; refusing is monotonically risk-reducing. Reinstated at its ORIGINAL purely-proportional 0.5% form: a flat-dollar alternative term was drafted and dropped on the human's instruction ('just 0.5% of account value'). At ~$100k equity the floor is ~$501. Mechanics: sizing.below_min_notional_floor()/min_notional_floor_dollars(), checked in guarded_entry()/guarded_short_entry() right after the qty<1 sized-to-zero check so a zeroed trade keeps its own label; binding_caps() names the cause and the entry-skipped/below-min-notional note reports notional, floor and binding cap. Measured cost: would have refused 31 of 384 post-removal orders (8.1%), forfeiting the +$23 they produced. Historical text: RETIRED alongside R32, whose shrinking it existed to clean up after. R41 skipped trades that R32's $100 max-loss cap had squeezed to an uneconomic size; with R32 gone that squeezing no longer happens, so the floor was rejecting nothing but genuinely small deliberate positions. A tiny position is now simply placed. sizing.below_min_notional_floor() and MIN_NOTIONAL_PCT survive for historical log analysis and because R43's paradigm-shift probe is still expressed relative to the floor. Removed in the same decision as R32; write-up: [internal note]
human directive, 2026-08-06 ("no position sizing below 0.5% of portfolio value... the correct behavior is to SKIP the trade entirely, not to force the size up"). See [internal note] RETIRED 2026-08-07 by human directive; removal write-up cited in the rule text above.
The buy universe includes the pinned ETF list (SPY, QQQ, IWM, DIA, SMH, XBI, XLE, XLF, XLK, XRT, GLD, SLV, TLT, TQQQ, SQQQ, SOXL, SOXS) in addition to the day's watchlist. ETFs are liquid, halt-free, rumor-free — good vehicles for playbook testing. Leveraged-ETF half-sizing was removed 2026-07-28 by human directive: every ETF, 1x/2x/3x alike, may use the full R21 5% notional like any other symbol (R32's flat $100 max-loss cap still applies on top).
human directive, 2026-07-09; leverage half-sizing removed by human directive 2026-07-28
Judgment-tier sessions may implement an evidence-backed profit/risk improvement -- altering rules, code, or strategies -- without waiting for per-instance human sign-off (the standing authorization in [internal note], in force since 2026-07-14). This rule adds one concrete, tool-enforced requirement: the change must be documented where the EOD Reports dashboard tab actually shows it, not only in a git commit or an internal reviews/*.md file nobody reads day-to-day. Every autonomous trade_log.py note must start with the literal prefix SELF-IMPROVEMENT: (case-insensitive) so the dashboard's isSelfImprovementNote() detector picks it up. Every autonomous strategy_lifecycle.json status transition (locked, retesting, active, dead, dead-dead, paradigm-shift, flagged-for-review) is written to trade_log.json by eod_review.py's log_lifecycle_transitions(), not merely printed to a log file. The EOD Reports tab is a nested, collapsed-by-default accordion (restructured 2026-08-06) -- day-level and section-level summary badges keep self-improvement/paradigm-shift/dead-dead counts visible without expanding anything. Track A (strategy/rule adjustments, gated on R18's 15-attempt sample-size bar) and Track B (bug/reliability fixes, no sample-size gate) both remain in force unchanged; a genuinely new mechanism still needs a checkable external citation; safety-core changes still require explicit downside analysis and a synchronized trading_safety_core.md update.
human directive, 2026-08-06 ("the system has standing permission to actually implement the change... AS LONG AS the change is clearly documented... shows up in the EOD status/report... not just buried in a git commit or an internal reviews/*.md file"). See [internal note]
A strategy that fails a 15-trade cycle (initial or retest) with no further actionable improvement gets ONE additional chance -- every LONG signal it would have taken instead goes SHORT, and every SHORT signal instead goes LONG, everything else about the strategy unchanged -- before being declared dead. invertedDirection and paradigmShiftAttempted are set once, permanently, the first time this fires. A paradigm-shift cycle can retry on its own actionable improvements exactly like a normal retest (status stays paradigm-shift, never silently relabeled). A profitable paradigm-shift cycle becomes active and keeps trading its earned inverted direction indefinitely -- invertedDirection is never cleared, and the strategy can still lock under R34. A paradigm-shift cycle that ALSO loses with no further actionable improvement (or Kelly-deadlocks under R36) becomes dead-dead: a stricter, permanent terminal state -- tradeBudget 0, excluded from entry routing exactly like dead, and unlike dead, no automated path in strategy_lifecycle.py may ever reverse it. Execution: strategy_direction_inverted() is checked as the LAST step in guarded_entry()/guarded_short_entry(), after every other gate (R33/R31/R29/R21/R32/R36/R38/R40/R41/R37) has already validated the signal for its native direction; the stop and target are mirrored around the same entry price and the order is placed through the opposite function via a hard stop_override/target_override, never blended with that function's own live signal data. AMENDED 2026-08-07 (human directive): a paradigm-shift cycle is NEVER retired by the R36 Kelly deadlock closer. quarter-Kelly is derived from the tag's history under its ORIGINAL direction, and every strategy worth inverting is a consistent loser whose Kelly is always a confirmed zero -- so the inverted cycle was being retired as dead-dead without placing one inverted trade. It now probes at stop_guard.PARADIGM_SHIFT_PROBE_PCT (1.25x R41 MIN_NOTIONAL_PCT = 0.625% of equity, the smallest position the rulebook permits) and is judged only on real inverted results by reassess_retests(). Its 15-trade budget is now actually enforced (CYCLE_STATUSES); previously retest_budget_remaining() returned None for paradigm-shift and stop_guard read that as no budget at all. A deadlocked 'retesting' tag still gets its paradigm-shift chance exactly as before. IMPLEMENTATION DEFECT, FIXED 2026-08-07: R43 placed ZERO inverted trades between 2026-08-04 and 08-07. moomoo_trade.strategy_direction() re-checked the order against the strategy's watchlist-declared direction, and build_watchlist.mjs knows nothing about lifecycle state, so an inverted tag still declared LONG there -- every inverted order arrived declared LONG and executing SHORT and was refused direction-mismatch (163 refusals against exactly 163 paradigm-shift-inverted attempts, a 100% failure rate; ema-stack, etf-scalp, pdh-break and gap-go accrued nothing against their budgets while appearing to trade). strategy_direction() now mirrors the declared direction for any invertedDirection tag, applied last so watchlist-declared and code-declared tags invert identically; an ambiguous declaration stays ambiguous. Consequence for judgment: no paradigm-shift cycle has any real inverted episodes before 2026-08-07. CLARIFIED 2026-08-11 (human directive): plain `dead` is TRANSIENT, not a resting state -- no FSM path assigns it (pinned by test), and the skull icon is reserved for `dead-dead`, the only terminal state. Icon rule: dead-dead 💀, paradigm-shift ⇄, legacy dead 🗿 'DEAD (pre-R43)'. reconcile_pre_r43_deads() (idempotent, each EOD) reconciled the 9 grandfathered `dead` records: 7 promoted to paradigm-shift (cross-session-strength, inst-continuation, mfi-hhhl, orb-retest, rsi-cross-reversal, vixfix, watchlist-momo -- all killed by the R18/R36 Kelly deadlock this rule's 2026-08-07 amendment identifies as a bug, same precedent as the 4 retroactive shifts that day), 1 dropped (barchart-ma50-100, an R45 alias), 1 left dead (vwap-bounce -- 'Explicitly removed; no supported redesign remains' is a human judgment, not a deadlock artifact). humanLocked and existing dead-dead records are never touched. IMPLEMENTATION DEFECT FIXED 2026-08-12: when a losing paradigm-shift cycle earns another 15 attempts from a recorded actionable improvement, stop_guard.retry_lesson_gate() now enforces that directional VWAP/30-minute lesson on the executed inverted direction. Previously the gate recognized only status=retesting, so paradigm-shift improvements were persisted but ignored. Initial paradigm-shift cycles with improvement=null remain direction-only. AMENDED 2026-10-10 (stalled-cycle bound; auto-applied by the weekly rollup, commit e0bc663): this rule's "bounded" evaluation had no bound in TIME. reassess_retests() sliced a cycle out of a tag's episodes and did `if len(cycle) < budget: continue` unconditionally, so a cycle that accrues ZERO episodes never reached any verdict branch -- not active, not a further retest, not dead-dead. Six inverted tags sat frozen 22-41 trading sessions with the full 15-trade budget remaining, absent from inactive_tags(), publishing ten candidates a day and placing nothing: ema-stack 41, pdh-break 40, watchlist-momo 36, cross-session-strength 33, rsi-cross-reversal 33, tj-pmh 22. Cause is the composition with the 2026-08-12 improvement gate above: all six carry the identical loss-review sentence ("wait for renewed rejection below VWAP with negative 30m trend before entry, or skip"), which retry_lesson_gate() enforces on the EXECUTED inverted SHORT side as vsVwapPct < 0 AND trend30mPct < 0 -- the opposite of the bullish tape every one of these LONG-native strength/momentum screens selects for, so signal AND gate is empty by construction (same shape as backlog task-61). A zero-accrual cycle now gets exactly ONE gate clearance once strategy_lifecycle.CYCLE_STALL_SESSIONS = 20 trading sessions of the whole book have passed since cycleStartedAt: improvement -> null (preserved as clearedImprovement), cycleGateCleared stamped so a nightly re-clear is impossible, and the cycle is then judged on direction alone exactly as an initial inversion is. Deliberately NOT dead-dead: retiring these permanently would condemn the inverted strategy for a gate conflict in this module, the precise error the 2026-08-07 amendment above exists to prevent -- and a tag that still cannot trade direction-only stays visibly stalled, which is the signal the gate was never the blocker. Sizing is untouched (a paradigm-shift probe still enters at stop_guard.PARADIGM_SHIFT_PROBE_PCT = 0.625% of equity), as are the trade budget and every other rail. Fires on zero accrual only: accruing-but-slow cycles were deliberately left alone (rsi-dip 44 sessions at 1/15, vixfix 40 at 7/15, 3bar-rev 24 at 8/15). See workflows/trading_lessons.md and the R43 stalled-cycle bullet in workflows/trading_safety_core.md.
human directive, 2026-08-06 ("invert its direction... gets a fresh 15-trade evaluation cycle... if a strategy fails its normal run, AND then fails its paradigm-shift/inverted run with no other actionable changes applied on top of the inversion, it becomes permanently locked as dead"). See [internal note]
Each strategy's R21 notional allocation is SCALED by a multiplier derived from Monte Carlo simulation of its own equal-weight-normalized episode history. Two properties are the whole point of the rule and are test-enforced. (1) It can RAISE size as well as lower it -- R36's Kelly cap was one more term in a min() and could only ever shrink; R44 multiplies, so a strategy with good simulated statistics sizes UP toward the cap. (2) It can NEVER reach zero: the multiplier is floored at 0.5x, so a strategy the simulation dislikes trades SMALLER and keeps generating the evidence that would change the verdict. That floor is the direct fix for R36's deadlock chain, and blocking a strategy remains R18/R34/R43's job alone. Below 15 closed episodes (the same bar R18 uses) the multiplier is exactly 1.0 -- NEUTRAL, never a penalty: absence of evidence must not masquerade as evidence of absence. Ceiling 1.5x, and R21's per-symbol cap still bounds the result underneath, so a boosted entry can never exceed 7.5% initial / 10% combined. Simulations (trade bootstrap, daily block bootstrap keeping same-day correlated trades together, and an execution-stress variant with 5-15% missed fills plus a slippage haircut; 10,000 paths, seeded for reproducibility) run NIGHTLY in tools/allocation_lab.py, which writes src/data/allocation_multipliers.json. stop_guard.py READS that file -- it never imports the research module, keeping slow simulation work out of the entry loop and a research-layer fault from taking the trading daemon down. A missing, malformed, or stale (>7 days) file reads as neutral 1.0, and every value is clamped into [0.5, 1.5] independently on both write and read.
human directive 2026-08-07; [internal note]
R18 judges a strategy after 15 episodes under its own tag, so two tags running the same entry gate split one strategy's evidence and neither ever reaches a verdict. Tags that share a gate are therefore merged: an alias is judged, sized, retired and routed as its CANONICAL tag, and its episodes pool into the canonical's evidence. Evidence 2026-08-07: all ten barchart-* tags declare automation.kind = watchlist-momo in build_watchlist.mjs -- the same live gate watchlist-momo uses -- and the twelve Barchart Opinion studies produce near-identical top-10 slates (moving-average studies over one universe are almost perfectly correlated; nine were symbol-for-symbol identical). watchlist-momo was already dead under R18 (-617.23 normalized over 28 episodes) and its ten aliases kept trading anyway, because eight had never individually reached 15 episodes. Pooled: 124 episodes, -1753.02 normalized. Separately or-break and orb-retest have byte-identical gate bodies in stop_guard.indicator_signal; pooled 29 episodes, -1002.59 normalized. Mechanics: strategy_lifecycle.canonical_tag() maps alias to canonical; barchart- matches by PREFIX, not enumeration, because the watchlist takes a rotating top ten of twelve studies. Applied in _tagged_episodes (pools evidence for every consumer), inactive_tags (a retired canonical retires every alias), retest_budget_remaining (an alias spends the canonical's budget), entry_cycle._add (options register and journal under the canonical tag -- the load-bearing block, since a prefix alias usually has no lifecycle record), advance_lock_status (no alias earns its own R34 lock) and is_direction_inverted (no alias trades the opposite side of its canonical). History is never rewritten: closedEpisodes and journal rows keep the sourcing tag, canonicalization happens on read, and the merge is reversible by editing one map. The merge is on GATE identity, never slate overlap -- macd-st shares the barchart slate but has its own gate and stays separate. Immediate effect: twelve tags stopped trading. Adding a tag whose automation.kind duplicates an existing tag's requires adding it to the alias map.
human directive, 2026-08-07 ("deduplicate the strategies and combine them"). See [internal note]
A closed episode with no strategy contributes its P&L to nothing: R18 cycle verdicts, R34 batches and R44 multipliers all count episodes BY TAG, so an unattributed trade is invisible to every judgment that decides whether a strategy lives, and each one silently shrinks the sample those verdicts use. Evidence 2026-08-07: a mid-session read of trading.json showed 7 of that day's 28 episodes attributed to 'unknown', most closed in one 11:19 batch; all seven self-healed on the overnight resync when annotate_closed_trades re-ran the entry-journal link after reconcile_journal copied the fills in. The self-healing IS the problem -- a failure that repairs itself silently is indistinguishable from one that does not, and nothing reported either. The order-time guarantee already existed but was never verified: moomoo_trade requires --strategy on buy/short, journals an orderId per fill, and R24 refuses a SELL beyond held quantity, so attribution runs at 100% (1266 of 1266 closed lots, all via the exact order-<orderId> link; the fuzzy price/time fallback is unused in the entire history). Mechanics: rebuild_trade_episodes.UNATTRIBUTED is the placeholder written when no strategy resolves -- not a strategy, never to be read as one. It is still written (dropping the episode would hide a real trade with real P&L), but unattributed_episodes() reports every occurrence, moomoo_sync logs an episode-unattributed event and a stderr warning as it writes, and task_health.trade_attribution_health() puts the standing count on the health surface where allOk gates the dashboard banner. Two invariant tests assert the property over the live book at both episode and lot level. unintended-oversell is NOT an attribution failure: it is a deliberate safety marker for a SELL beyond held quantity (R24) and must never be re-credited to the long strategy it oversold, which would launder a safety violation into that strategy's record. Nothing repairs attribution automatically -- deciding which strategy owned an orphaned fill is a judgment call over the journal's order-id chain. AMENDED 2026-09-30 (note-review, Track A): a REFUSED entry may never be resubmitted under a different strategy tag. The tag names the signal that produced the entry, not a routing slot to be swapped until an order is accepted. The watchlist carries BOTH a human-facing strategyLists[].name and the machine strategyLists[].strategyTag, and --strategy accepts ONLY the strategyTag: moomoo_trade.strategy_direction_detail() matches on strategyTag alone, so passing a name resolves to no declaration and refuses with direction-mismatch / 'has no unambiguous executed direction ... until its direction metadata is repaired', which reads like broken metadata and is not (name and strategyTag differ on 40 of the 56 lists in the 2026-09-29 15:08 CT build: news-gap-go -> gap-go, score80-volume -> score80, score90-conviction -> score90, next-day-high-confidence -> next-day, thomas-james-pmh -> tj-pmh, 'UT Bot Alerts' -> ut-bot; the session's third attempt, score80-conviction, matches no list name at all -- it is a blend of the two real names). Evidence 2026-09-29: an intraday session hit four such refusals on US.IOVA (14:21:54Z-14:54:07Z), logged them as a CODE BLOCKER requiring a fix session, stood down on the candidate for an hour, then placed BUY 300 US.IOVA under ut-bot -- a tag it had never evaluated -- with a thesis naming the real source ('qualified under ut-bot watchlist + news-gap-go fastTrack'). Journal 9fc7dbaf / order 772571 went CANCELLED_ALL at filledQty 0, so nothing reached ut-bot's record; that is luck, not a control. Had it filled, R18's ut-bot cycle, R34's batches and R44's multipliers would all have scored a gap-go signal as ut-bot -- attribution that is present and WRONG, which no unattributed_episodes() check can catch. The re-tag also routed around a live risk gate: the correct tag gap-go is R43 direction-inverted (declared LONG, executed SHORT), so the LONG would have been refused anyway, correctly. Operative: (a) --strategy takes a strategyTag, never a list name; (b) a direction-mismatch refusal is never grounds to retry under another tag -- trade the executed side the refusal names, or stand down; (c) a tag whose direction genuinely cannot be resolved is a stand-down and a session-note, not a substitution. Data-integrity control, not an edge claim: no stat block.
human directive, 2026-08-08 ("every trade needs a strategy attribution"). See [internal note] Amendment: auto-applied by note-review session (batch lane), 2026-09-30; evidence: four order-refused direction-mismatch events 2026-09-29 on US.IOVA, journal 9fc7dbaf / order 772571 tagged ut-bot, and watchlist.json (built 2026-09-29 15:08 CT) carrying 56 strategyTag values of which none is news-gap-go, score80-volume or score80-conviction
No order is submitted without a complete, fresh, risk-valid ExitPlan built BEFORE the entry from the symbol's own intraday regime (ADX/ATR-ratio/EMA-structure/Hurst vote with K=3 hysteresis and higher-timeframe reconciliation); an AMBIGUOUS regime refuses the entry outright. The stop basis is entry -/+ 2.5 x ATR(14) on the 5m trading timeframe for EVERY strategy, replacing R17's structure-minus-wick-buffer as the primary basis (R17 remains the fallback when live ATR5m is missing, and its no-man's-land / R29 / skip-rather-than-widen clauses still bind). Measured across real historical entries the R47 stop is tighter on 72% of them -- median 1.21% of price vs R17's 1.63% -- which means a smaller loss at the stop, NOT a larger position: per-trade size was measured unchanged on 308/308 because no per-trade sizing term has read the stop since R32 was removed. The tighter stop frees R38 aggregate heat, allowing more concurrent positions. Exits are three tiers at 40/35/25: fixed 2R, a 3xATR Chandelier trail that promotes to the higher timeframe at +2R, and a structure-exit runner gated off when the regime does not support one. Four invariants are enforced by TYPE rather than by assertion in trade_monitor.py: a stop has no assignment API so it can only tighten, a fixed target is immutable so it cannot extend, no method anywhere increases position size, and the defensive-adjustment budget is one permit with no reset. R37 is EXEMPT from that budget -- Kronos tightening may fire repeatedly; the one-shot counter governs regime-flip adjustments only, and both route through the same monotonic stop so risk can only shrink. Failure mode is fail-open: a classifier or planner fault logs and falls back to the pre-R47 rule chain rather than blocking the trade, matching R37/R38. Specified as 'R45'; R45 and R46 were claimed by concurrent work while this was in flight, so it landed as R47. PLAN GATE WIRED LIVE 2026-08-09 in SHADOW mode (R47_PLAN_GATE_ENFORCE=false default): it classifies and logs the verdict but does not block. Measured on 20 real symbols over 5 days of 5m bars, the live tape reads 70-80% AMBIGUOUS because MA_SLOPE_FLAT_PCT (0.05%/bar) is a daily-chart threshold demanding a ~4%/day trend against an observed ~1.2%/day median slope; enforcing would refuse ~95% of entries. FAULT (broken machinery) fails open; VERDICT (ambiguous/stale/risk refusal) is what fails closed once enforced. SHORT entries get the regime gate only and keep the flat 3% cover stop. Measured 2026-08-09 over all 228 closed SHORT trades across 161 symbols: 2.5xATR is tighter on 228/228 (median 0.91% of entry vs 3.00%), raises the stop-out rate from 75.0% to 95.2%, and would have cut short 24.3% of profitable shorts. Parity is rejected on evidence, not deferred. RECALIBRATED 2026-08-09: the earlier '95% refusal' figure was wrong (one bar, 20 untraded mega-caps). Re-measured over 3,851 moments on the real universe, raw AMBIGUOUS was 18.0%. ADX 25/20 and vol 1.1/0.9 measured healthy and were left alone; the EMA50 slope moved from a fixed percentage to 0.10 x ATR(20) per bar (AMBIGUOUS 18.0->17.1%, TRENDING 30.5->34.6%). Two bugs fixed: the RANGING band extreme came from the nearest support (a level inside the band) and is now the low/high of the last 24 5m bars; and RegimeSmoother counted calls not bars, so K=3 meant 45 seconds instead of 15 minutes. Enforcement REMAINS OFF: end-to-end over 11,538 consecutive bar-moments the shipped code refuses 44.1%, not 18%, because hysteresis initialised to AMBIGUOUS turns label churn into AMBIGUOUS-stickiness. Higher-timeframe reconciliation repaired 2026-08-09 (option C): it had NEVER run in production because the 1-day bar fetch yields ~26 fifteen-minute bars against EMA50's need for 50 (usable on 0 of 12 symbols); yahoo_bars_deep() now fetches 5 days cached for one bar period (usable 16/16). With it present the 5m and 15m labels agree only 40.6% of the time, so on_disagree is 'smaller' rather than 'ambiguous': the plan is marked half_size and guarded_entry applies a shrink-only 0.5x multiplier. A fault never resizes. The earlier AMBIGUOUS-stickiness hypothesis was wrong -- the smoother reduces ambiguity (16.9%->16.0%) and halves churn. Enforcement remains OFF pending a representative multi-moment measurement; the only post-fix figure is a 16-symbol snapshot at 31.2%. OPERATIVE STOP BASIS since 2026-08-11 (LIVE by default): width = max(0.5 x ATR(14,5m), 0.798% x entry). Replaces 2.5xATR and supersedes the never-live flat-0.50% plan. Study v2 coupled the target to the stop (the live target is entry + 3 x width, so v1's stop-only substitution overstated tight stops), computed ATR from pre-entry bars only, and treated R29-inadmissible stops as refused entries. Over 973 real LONG round-trips, among fully placeable rules: hybrid +$7,177/+$5,806/+$2,893 at 0%/8%/25% slippage; pure flat floor +$6,877/+$5,517/+$2,627; flat 1.00% +$4,846/+$3,297/+$6; 2.5xATR -$5,563/-$7,694/-$12,221 (worst tested). The ATR term fires on only 4.0% of trades (39/973) and its $300 edge over a pure flat floor is not statistically meaningful -- it is tail protection for volatile names (ACHR's 2.5xATR stop was 4.59% of entry), not a proven edge. The floor is derived from R29 + the entry premium so the rule can never be unplaceable. Caveats: the simulation's exit model is stop-or-3R only while live exits also use R25/Kronos/EOD-flatten, so the ranking carries but not the magnitudes; one ~4-week window; LONG only. Each entry logs its branch as R47 hybrid(atr)/hybrid(floor). Kill switch R47_HYBRID_STOP_ENABLED=false. AMENDED 2026-08-13 (human directive) - SHORT PARITY: shorts use the same hybrid as longs, max(0.5*ATR(14,5m), 0.798%*entry), replacing the flat 3%-of-entry default and reversing the 2026-08-09 hold. WHAT CHANGED IS THE POLICY, NOT THE EVIDENCE: the 2026-08-13 short-side study (415 shorts, real 5m bars, 7/23-8/13) did NOT clear a tightening - its sample is a +5.08% SPY uptrend, every width lost money, the ranking was non-monotonic, and the only positive rules died by 8% slippage; it recommended changing nothing, and the human directed parity anyway. Only the DEFAULT branch changes; explicit custom/structural stops and R43 mirrors are untouched. Affected since 2026-08-10: max-loser-failed-vwap (21), wsb-popularity-green-open (7), short-gap-open-close (5), max-loser-breakdown (5), short-gap-failed-10m (5), org-gap-fade-short (3), short-gap-5m-low (3), kronos-momentum (2), pdh-break (1) - 3.00% -> a MEDIAN 1.06% of entry (~2.8x tighter): CORRECTED 2026-08-13, the ATR term wins on 72% of real shorts vs ~4% on longs, so the all-floor 3.8x figure first published was wrong. A/B on the 220 affected shorts is NOT a clear win - at zero slippage the flat 3% beats the hybrid by $160 (-343 vs -503); the hybrid leads only under slippage assumptions that scale with stop distance. Stop-outs 62->126, win rate 70%->42%. Realized anchor: those same names/days actually closed +1419.05, while both sim arms are negative, so the simulation's magnitudes carry no weight. R29 ASYMMETRY FLAGGED NOT RESOLVED: R29 measures clearance from the live LAST for longs but from ENTRY for shorts, so a short stop at its minimum sits 0.198% from the tape vs a long's 0.5%; moot for the new default (the 0.798% floor clears it by construction) but still live for custom_stop strategies. Also 2026-08-13: max-loser-failed-vwap gets a widened SHORT trail (2%/1% arm) via SHORT_TRAIL_OVERRIDES, keyed by strategy tag not automation kind. STRUCK 2026-09-29 (overnight review, Track A): SHORT_TRAIL_OVERRIDES is now EMPTY and max-loser-failed-vwap takes the standard 1%/1% default. A trail can only lock in a gain if it is narrower than the move the strategy produces; this tag's median MFE is 1.63%, so a 2.00% give-back exited ~0.4% below entry. Trail exits that were a LOSS: 8.7% (n=23) in its own 1% era -> 62.1% (n=29) under the override, vs 20.2% (n=89) for sibling max-loser-breakdown on a 1% trail over the same window. Realized R on trail exits +0.014R (n=30) vs +0.317R (p=0.0055) and +0.279R (p=0.0193) for the two baselines. REVERTED SAME DAY 2026-08-13 (human directive) after measuring max adverse excursion: shorts keep the flat 3%. On the 220 real shorts that used that branch, 49% of eventual WINNERS ran >1.0% against entry first, 30% >1.5%, 20% >2.0% - a 1% stop would have cut 75 winners worth +$1920, a 2% stop 31 worth +$709. The MAE distribution is BINARY: a short that does not reach its stop wins 94-100% of the time regardless of how far it went against you, while losers' median MAE is 3.00% (essentially every loser is a stop-out), so adverse excursion below the stop predicts nothing. The A/B agreed - flat 3% beat the hybrid by $160 at ZERO slippage, the only unbiased assumption. The hybrid's real median short stop was 1.06% of entry, inside the band where winners were still being cut. NOT SETTLED: the width buys survivability worth ~$25-67 per surviving trade against ~$85 per stop-out (a payoff-ratio problem, not a stop-width one), and the whole sample is a +5.08% SPY rally where shorts would be expected to need more room than normal. RETEST ON DOWN-WEEK DATA. max-loser-failed-vwap's widened trail (2%/1% arm) was KEPT.
human directive, 2026-08-07 (regime-based exit planning specification + four follow-up decisions: BROAD stop-basis scope, full three-tier build, R37 exempt, fail-open); TESTING
The moomoo SIMULATE account charges a flat ~$1.96 per closed lot ($0.98/side, per ORDER) that a real US-resident Moomoo Financial Inc. account does not. Measured 2026-08-19: equity showed -$2,031 from a $100,000 start while trade-derived realized P&L showed +$1,657 gross, a -$3,867 gap over 1,969 closed lots, reconciling the whole book to within $8. PROOF IT IS PER-ORDER AND NOT REAL: (a) all six zero-trade days, weekends included, show a gap of exactly $0.00, ruling out borrow/financing/dividends/custody which accrue regardless of trading; (b) average shares-per-lot varies 6.5x (97->627) while gap/lot stays pinned near $1.96 and gap-per-SHARE swings 14x; (c) $0.98/side matches moomoo's NON-US-resident '$0.99 per order' unlock fee. Verified against moomoo's published US schedule: US residents pay $0 commission and $0 platform fee (promotional), with only regulatory pass-throughs (SEC $0.0000206*amount and FINRA TAF $0.000195/share on sells, CAT $0.000003/share both sides) totalling ~$0.10 per typical round trip, about 5% of the simulated charge. NOT DISABLEABLE: nothing in this repo sets it, and both deal_list_query and order_fee_query return 'Paper trading is not supported', so it is applied server-side with no ledger and no control -- do not go looking for a config. DIRECTION (easy to invert, so state both): against the PAPER ACCOUNT, gross strategy P&L reads ~$2/lot too OPTIMISTIC; against LIVE ECONOMICS, the paper EQUITY CURVE reads ~$2/lot too PESSIMISTIC. Operationally: gross P&L is live-accurate, equity is artificially depressed -- on 2026-08-19 the account showed $97,845 against a live-equivalent $101,513, i.e. up 1.5% rather than down 2.2%. DELIBERATELY NOT NETTED OUT (human directive, 2026-08-19): R18/R34/R36/R44 read gross episode P&L, which already equals commission-free live economics, so they were never distorted. The artifact is confined to the equity curve, where it biases R21 sizing and R38 heat slightly DOWNWARD -- the safe direction. Diagnosis: tools/equity_reconciliation.py (analysis only; a test bans live modules from importing it).
human directive, 2026-08-19 (keep paper economics matching commission-free live trading rather than building a fee-netting layer); investigation in internal notes (not committed; the repo has no [internal note] directory)
LIVE 2026-08-19, armed after the close on a flat book per human approval; registered as the `Kitty Guard Watchdog` daily task (08:30 +7h, every 1 min) and inventoried by task_health.py. Watchdog that relaunches the Stop Guard daemon when it dies mid-session. MOTIVATION: on 2026-08-19 the daemon died twice SILENTLY -- healthy at cycle #374 at 12:02:06 with every position quoting, gone 34 seconds later, no traceback, on AC power with 5.5GB free, so neither the Modern Standby cause diagnosed on 2026-08-18 nor OOM explains it. It stayed dead 22 minutes with 19 open positions and NOTHING in the system would have restarted it before the next morning's 08:31 trigger; only a human-run monitor noticing a stale heartbeat caught it. Cost: ARIS and MRK broke through their stops during the window (-$79.91 / -$58.56 on restart) and two profitable target-covers were missed and only collected late (MSTR +$13.47, BMNR +$64.35). The silent-termination root cause remains UNKNOWN; this does not fix it, it makes it survivable. THE CENTRAL SAFETY RULE IS NEGATIVE: never start a second daemon. Two guards enforcing the same stop can double-submit exits, and an unexplained second launch at 12:02:40 that day is the leading suspect for the first death. So the watchdog acts ONLY when stop_guard.lock_has_live_owner() is False (the recorded owner PID is genuinely gone). A daemon that is ALIVE but wedged is deliberately NOT restarted -- it is reported, because restarting that case is the one action that could turn a stall into a double exit, and a stalled guard still holds its stops. Further gates: weekday only; only inside 08:35-14:55 CT (the daemon is a scheduled one-shot that correctly exits after the close, so the worst misfire would be resurrecting it all night); heartbeat older than 180s; at most 3 restarts per day (a crash loop is a fault for a human, not something to feed); 300s cooldown. Relaunch goes through scripts/run_stop_guard.cmd -- the SAME wrapper the scheduled task uses -- so environment, the battery-below-10% gate and log destination all match the normal launch path. Decision logic is pure and tested (tools/guard_watchdog.py should_restart(); 13 tests); the spawn is isolated in main(). Registered as a daily trigger with PT1M repetition for seven hours; `task_health.py` inventories the deployed repetition and ExecutionTimeLimit against that declaration. See workflows/system_setup.md. CORRECTION 2026-09-23 (ENFORCED -- watchdog recovery is aggregate-bounded and killed runs are audible). Guard PID 21456 stopped publishing liveness at 09:56:51 CT (last completed cycle #134 at 09:54:55); scripts/run_stop_guard.cmd recorded `EXIT code=1` with NO traceback, NO `stopped` action-log event and NO CRITICAL line. Task Scheduler still counted the `Kitty Stop Guard` action tree as RUNNING until the watchdog's /End at 10:20:38, so `RestartCount 3` / `RestartInterval PT1M` never saw a failure and never restarted it -- RestartOnFailure is neither configured nor credited; the independent watchdog is the sole authoritative recovery path. The watchdog then fired ON SCHEDULE at 10:00:02, 10:05:02, 10:10:02, 10:15:02 and was TERMINATED by Task Scheduler at 10:02:02, 10:07:02, 10:12:02, 10:17:02 (Microsoft-Windows-TaskScheduler/Operational event 329) for exceeding the task's 2-minute ExecutionTimeLimit, and the 10:20 and 10:25 runs were terminated the same way. None of those runs wrote a single line to logs/guard_watchdog.log -- buffered stdout dies with the kill -- so the system's own log shows a 25-minute SILENCE instead of five killed recoveries; only the Windows event log held the evidence. replace_supervised()'s bounded waits sum to ~85s (schtasks /End 20s + taskkill /T 20s + taskkill /F 20s + 5s proof-of-death + schtasks /Run 20s) BEFORE interpreter startup and identity probes, so it is not bounded below its own 120s wall. Measured cost: guard-protection-gap 2071.8s (34m32s) with 14 open positions; MDB and AR both breached during the blind window and were covered within 50s/110s of protection returning, at 439.06 vs cover-stop 436.05 (4 sh) and 36.32 vs cover-stop 36.12 (48 sh) = ~$21.64 worse than the intended stops at the submitted cover prices. THREE OPERATIVE REQUIREMENTS, enforced 2026-09-23: (1) a recovery attempt must complete inside the supervisor's own execution wall -- the watchdog's total bounded work and its task ExecutionTimeLimit must be derived from one another with interpreter headroom, never set independently; (2) a watchdog run must leave a record even when killed (unbuffered/flushed output or an on-entry stamp), and consecutive runs that produce no completion record must escalate as a fault instead of reading as quiet; (3) a daemon exit must become a supervisor-visible failure, or recovery must not depend on the supervisor ever reporting one. DEPLOYMENT ENFORCED 2026-09-23: the canonical and live `Kitty Guard Watchdog` task repeat every PT1M for seven hours. Its single 120-second aggregate recovery budget is derived inside a 180-second ExecutionTimeLimit with interpreter headroom; each scheduler/taskkill sub-wait inherits the aggregate time remaining. `task_health.py` inventories both registered values against the declaration, so cadence or wall drift is unhealthy. DEPLOYMENT STATUS RESOLVED (2026-09-23 13:58 CT, critical note-review): the PT1M/180s cadence is now LIVE and externally verified. Set-ScheduledTask was applied to the live `Kitty Guard Watchdog` task; Get-ScheduledTask now reports Repetition.Interval=PT1M, Duration=PT7H, StopAtDurationEnd=False, ExecutionTimeLimit=PT3M, and task_health.watchdog_definition_health() returns ok:true against the declaration -- the same detector that emitted scheduled-task-error on every sweep from 2026-09-23T16:45Z through 18:50Z. The live recovery cadence is one minute and the live wall is 180s around the 120s aggregate recovery budget. Only the watchdog task was modified; `Kitty Stop Guard` was left Running and untouched, because re-registering the daemon task mid-session with 14 open positions could terminate live protection. The blocker was privilege, not code: queue task 15f581d250c44fe1 hit its timeout retry cap (exit 124, attempts 2/2) with the source change written but undeployed, and the rescoped follow-up 877fa0d48d1640eb (17:55-18:16Z) was DENIED access to the Task Scheduler CIM namespace and could not read or write the task at all. Deploying a scheduled-task definition is not fix-session work when the fix session is sandboxed out of the scheduler. Both protection tasks remain registered [scheduler config] with no stored credential, so no credential was needed or stripped -- the registration script's run-before-logon intent is still undeployed and is a separate open item. Raising the cadence 5x does not raise false-replacement risk: HEARTBEAT_STALE_SECONDS=180 and RESTART_COOLDOWN_SECONDS=300 bound replacement independently of trigger frequency, and the concurrent guard-cycle-stalled (cycle 265, 131.2s gap, checkCycle dominant) sits below the 180s staleness floor. PROCESS-STATE CORRECTION (2026-10-04, FIX session): an openable Windows process handle proves identity, not continued life. On 2026-10-01 the successor launched by the 14:24:45 CT restart exited at 14:25:11 with 10 positions open, but a retained handle kept OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) succeeding, so pid_is_alive() read the corpse as alive and process_start_token() returned its creation time while discarding the nonzero GetProcessTimes exit time. owner_is_verified_process() therefore verified a dead process, recovery_action() selected `replace` instead of the `restart` that had already worked at 14:24:45 and worked again at 14:35:08, and the terminal TerminateProcess returned ERROR_ACCESS_DENIED -- Windows' answer for an already-exited PID. That unguarded OSError escaped replace_supervised() before the proof-of-death loop, so no successor was launched in that tick, and because it was not VerifiedOwnerSurvivedError it also skipped escalate_unkillable_owner(): a total loss of stop protection was recorded as one non-CRITICAL watchdog-restart-error. The next tick restarted successfully 54s later; the full guard-protection-gap was 566.6s with 10 positions open. ENFORCED: a nonzero exit time is dead and yields no identity token, while OpenProcess itself failing with ERROR_ACCESS_DENIED and every ambiguous query still fail closed as alive, so a false 'dead' can never authorize a second daemon; and a failed terminal kill is preserved as audit evidence on the watchdog-restart event but always falls through to the bounded proof-of-death loop -- a gone PID starts its successor in the same tick, a surviving PID raises VerifiedOwnerSurvivedError and hard-escalates under R54. No threshold changed: HEARTBEAT_STALE_SECONDS, MAX_RESTARTS_PER_DAY, RESTART_COOLDOWN_SECONDS, PROOF_OF_DEATH_MAX_SECONDS and WATCHDOG_RECOVERY_BUDGET_SECONDS are all unchanged. Third occurrence of this shape -- 2026-09-28's 1899.8s gap and this one both had predecessors that died mid-cover (stop-out:US.ULCC:cover:before, stop-out:US.MAT:cover:before). Regression: test_exited_but_openable_windows_owner_is_dead_and_cannot_verify, test_exited_lock_owner_selects_restart_not_replace, test_failed_terminal_kill_proves_death_and_starts_successor_same_tick and test_failed_terminal_kill_still_escalates_a_surviving_owner. LATENCY CORRECTION ENFORCED 2026-10-06 -- an ownerless lock is proof of death, and the 180s staleness predicate was gating it anyway. HEARTBEAT_STALE_SECONDS = 180 answers 'is this live daemon merely slow or wedged?', which is moot when there is no owner at all: recovery_action() tested staleness BEFORE it ever looked at the lock owner, so a daemon that had already exited -- leaving an ownerless lock plus the fresh livenessAt stamp it published on its way out -- suppressed its own recovery for a full 180s, even though guard_watchdog.py's module docstring has claimed 'an ownerless stale lock is restarted immediately' since R49 was written. Proof it never fired early: all 9 watchdog restarts since 2026-09-28 carry a liveness age at or above the predicate (552s, 1885s, 241s, 220s, 195s, 263s, 551s, 252s, 187s -- the last being the floor, i.e. the predicate itself). 2026-10-06 decomposes the cost: daemon exits 14:20:56Z (R54 ProtectionCycleSurrender, shutdownKind uncaught-exception, 7 open positions), watchdog decides restart 14:24:04Z (188s later, ~180s of it the predicate wait), successor protecting 14:24:53Z (~49s genuine cold start) -- 188 of the 202.5s guard-protection-gap bought nothing and a correct recovery is ~50s. ENFORCED: HEARTBEAT_STALE_SECONDS now gates the LIVE-OWNER BRANCH ONLY; an ownerless lock restarts immediately. The live-owner replace path still requires 180s of frozen liveness plus cryptographic identity verification; 'ownerless' remains a proof because pid_is_alive() returns False only for an OS-proved exit or an absent lock record, and the 2026-10-04 process-state correction keeps OpenProcess access-denied and every ambiguous query failing closed as ALIVE (so those still wait); MAX_RESTARTS_PER_DAY (3) and RESTART_COOLDOWN_SECONDS (300) apply unchanged to the fast path and are now the ONLY brake on a surrender/restart loop, the 180s wait having been an accidental third one; the session window is unchanged, so a dead daemon after hours is still correct; and the no-two-daemons invariant is untouched -- the daemon's own startup still refuses to run while a live lock owner exists. A fresh guard-shutdown audit is deliberately NOT a precondition, because the unaudited deaths (abnormal-no-shutdown-audit, 2026-09-30 and 2026-10-01) must recover at least as fast. No threshold changed: HEARTBEAT_STALE_SECONDS itself, the 120s recovery budget, the 300s cooldown, PROTECTION_CYCLE_SURRENDER_AFTER and PROTECTION_CYCLE_SURRENDER_WITHHOLD_MAX are all unchanged. This completes R54 half six, which made the surrender conditional on the watchdog being ALIVE (it passed 2026-10-06 with maximum margin, 12.5s) but did nothing to make the recovery FAST. Regression: test_ownerless_lock_restarts_IMMEDIATELY_despite_a_fresh_stamp, test_an_ambiguous_owner_query_still_waits_out_the_predicate, test_the_fast_path_still_obeys_the_per_day_cap_and_the_cooldown, test_the_fast_path_is_still_inside_the_session_window_only, test_a_fresh_heartbeat_is_not_a_death_WHILE_AN_OWNER_IS_ALIVE.
human directive, 2026-08-19 (build auto-recovery rather than chase the silent-termination root cause); incident detail in internal notes
LIVE 2026-09-11. This number belongs to the audience of the Rule 50 AI channel: the system wrote its own rulebook; rule fifty is the humans watching it. Viewer suggestions (YouTube and X comments) enter a quarantined intake where comments are DATA, never instructions - nothing a comment says can execute code, change configuration, or place a trade. At most ONE suggestion is selected per night (the engagement-weighted 'Tonight's Pick'), with at most three concurrent test slots. Every suggestion faces the SAME evidence bar as an internal change: backtest, R18's 15-episode minimum, permutation testing with multiple-comparison correction, and block-permutation / out-of-sample checks where applicable - R23 and R52 are the standing demonstrations of ideas dying honestly under that bar. Adopted suggestions become numbered rules crediting the suggester's public handle; rejected ones are tombstoned with the evidence. THE OPERATOR EARNS NO CREDIT: the operator's own ideas go through the ordinary internal pipeline and never score leaderboard points - credit exists solely to measure outside input. Operator rule changes and edits are disclosed as operator changes. The operator ratifies every adoption and retains veto at every step; nothing enters this rulebook without a human directive. Full policy: autonomous-desk/community/selection-policy.md and input-security.md.
human directive, 2026-09-11 (Rule 50 AI channel: rule fifty is the audience; no credit to the operator)
LIVE 2026-09-03; UNRESPONSIVE-GATEWAY TRIGGER ENFORCED 2026-09-28. R51 restarts moomoo OpenD under either of two health failures: (1) its local broker gateway has been continuously unreachable for 90s, tracked across polls; or (2) the port accepts connections but current action-log telemetry contains a same-operation run of at least three broker-rpc-timeout events, the latest outageSeconds is at least 180s, and every confirming row proves timeoutPhase=rpc, requestSent=true, responseReceived=false. Startup/import and handshake timeouts NEVER authorize a restart: they are the self-inflicted cold-worker stall where killing OpenD is harmful. The evidence must be no more than five minutes old, so a prior outage cannot fire later. The process list is NEVER a health signal because OpenD can be running without serving; it is used only during recovery, where restart_opend() terminates every moomoo_OpenD.exe, waits for the list to empty, launches exactly one, and wait_for_port() verifies the listener or emits CRITICAL-opend-restart-failed. Both triggers share every refusal unchanged: US trading weekdays only, 08:00-15:30 CT, at most 4 attempts/day, and a 240s cooldown. The reason distinguishes unreachable from unresponsive-gateway. It NEVER places an order or touches a position. MOTIVATION: four gateway outages in the week of 2026-08-31 caused three skipped R24 flattens (17 positions carried), six failed exits, and a 966/966-error session while R49 correctly saw a live daemon. The missing listening-but-unresponsive case then occurred on 2026-09-25: after a 13:30:25 CT disconnect, 57 consecutive account-state broker RPC timeouts ran from 13:30:56 to 14:05:14 (34m18s, 10 live positions, zero stop evaluations) while every two-minute R51 poll stood down because the port accepted sockets. R51 acted only after connection refusal at 14:05:33 and restarted at 14:08:04, 37m39s after answers stopped. The 3/180s phase-gated trigger would have authorized at about 13:34 CT without misclassifying startup/handshake delay. The 2026-09-25 recovery correction remains: launch-only restarts had stacked five PIDs and spent the daily budget on no-ops; failed attempts remain charged so a logged-out gateway cannot enter a two-minute kill loop. Pure decision logic and recovery behavior are covered by tools/tests/test_opend_watchdog.py (48 tests). Driven by `Kitty OpenD Watchdog` every two minutes from 08:00 for 7h30m.
human directive, 2026-09-03 (build the OpenD watchdog); restart-verification correction auto-applied by interactive FIX session 2026-09-25 and reviewed 2026-09-26; unresponsive-gateway correction auto-applied by FIX session 2026-09-28 from the 2026-09-25 13:30:25-14:08:04 CT outage evidence
NULL FINDING + OBSERVATIONAL LOGGING, 2026-09-06. Do not re-investigate options-derived levels from scratch, and do not re-implement them as a gate. QUESTION: would call wall / put wall / zero-gamma flip point have informed entries or exits on real trades? ANSWER: no evidence, on the cleanest sample obtainable. PRIOR ART: R23 (options-wall awareness) ran target-cap / no-entry-into-wall / pin-caution / wall-break-trail gates live from 2026-07-11 and was REMOVED 2026-07-30; this study reaches the same verdict independently, from trade outcomes rather than rule performance. WHAT WAS TESTED: 197 lots with lookahead-free walls no more than 48h stale, 18 session days (2026-07-13..08-13), 84 symbols, reconstructed free from git-committed watchlist.json snapshots. Call-wall proximity is noise -- AUC 0.470 (all) and EXACTLY 0.500 for the largest call wall on LONGs. A put-wall threshold initially looked real (permutation p=0.0005, clearing Bonferroni at 0.0063, OOS holding sign) and DIED under block permutation: p=0.2694 blocked by symbol, because THREE symbols (CLSK, CIFR, ACHR) were 99% of the entire gain. A price-level confound was tested and refuted (corr +0.066, price-alone AUC 0.482). Diagnostic tell: snapshot staleness (AUC 0.404) out-separated every wall feature -- a data-collection artifact beating the hypothesis variables. WHAT COULD NOT BE TESTED, AND WHY: the zero-gamma FLIP LEVEL is not derivable from stored data. options_walls.mjs persists gexSharesPer1Pct, a SCALAR net gamma at the current spot -- locating a zero crossing needs a per-strike profile across hypothetical spots. The binary substitute (gamma sign) had 179 positive vs 18 negative, so it had no power either and was not run rather than produce a meaningless p-value. THE SIGN CONVENTION IS AN ASSUMPTION, NOT AN OBSERVATION: options_walls.mjs computes net gamma as calls-minus-puts on the stated premise that dealers are SHORT (see its own 'dealers short' comment). That is not verifiable from open interest, which shows contracts outstanding and not who holds which side. If dealers are net long a name, the wall/flip interpretation inverts -- so a null result here cannot distinguish 'walls do not matter' from 'the sign assumption is wrong on these names'. WHAT CHANGED INSTEAD: every order now journals `optionsContext` (callWall, callWallOI, putWall, putWallOI, pin, gex, asof, spot, totalOI, thin) via moomoo_trade.options_context()/watchlist_options_for(). ALL OBSERVATIONAL -- no gate reads any of it, and a test asserts no decision module even references it. RAW LEVELS are stored, never entry-relative distances, because distances are a modelling choice a later study should make for itself. `asof` is kept so staleness stays measurable. This exists because the study could only scrape 197 lots after the fact and 389 of 444 same-day matches were LOOKAHEAD (the chain is fetched post-close, modal asof 16:00 ET); stamping at order time removes that permanently and for free. Full historical coverage would need paid data (Polygon options ~$79/mo, ORATS ~$99/mo; Tradier's free tier is delayed/simulated) across 936 traded symbols -- not worth buying without a positive signal first, and there was none. REVISIT once the logged sample spans multiple regimes and enough symbols that a symbol-blocked permutation has real power.
human directive, 2026-09-06 (assess options levels; then log them observationally); supersedes nothing, complements struck R23
LIVE 2026-09-03. The R24 close-out silently failed on three consecutive sessions (2026-08-31, 09-01, 09-02): zero flatten-exit events, 17 positions accumulated overnight across days, last good flatten 2026-08-28. THE CLOSE-OUT CODE WAS CORRECT; its dependency failed. account_state() is the first statement inside the `mins >= EOD_FLATTEN_ET` branch, so a transient OpenD outage raises past the whole flatten. Evidence: on 09-01 and 09-02 the final cycle is at 14:40:5x, followed by repeating LOOP-CRASH 'OpenD is not reachable' until 'market day over - exiting'; the last working day (08-28) had 80 cycles inside the 14:41-15:05 window versus ZERO on the failing days. FIX 1: the crash backoff was a flat 300s, but the flatten window is only 24 minutes (15:41->16:05 ET), so each backoff burned a fifth of it - 4 attempts where 72 were possible, all landing while OpenD was still down. loop_error_backoff_seconds(mins) now returns 20s inside the window and keeps 300s during the session, where a crash loop must not hammer the broker. FIX 2: the daemon exited announcing only 'market day over', making an unflattened book indistinguishable from a clean one - which is why this ran three days unnoticed. Shutdown now logs CRITICAL-eod-flatten-incomplete with the open codes plus openPositionsAtExit on the stopped event, wrapped so an unreachable broker can never block the exit. STILL OPEN: OpenD itself has no monitor, watchdog or auto-restart. R49 watches the daemon; nothing watches the gateway the daemon cannot function without, and every failure this week traces to it.
human directive, 2026-09-03 ('trades aren't closing out'); write-up in internal notes
ACTIVE 2026-09-23. Instance PID 38012 completed protection cycle #50 at 12:51:51 CT and never completed another; it was killed at 13:45:48, leaving 53m57s of zero stop evaluation on 11 open positions. The three cycles before it had already stalled 295s, 155s and 1224s (#47/#49/#50, dominantPhase=checkCycle) and each still ran entry work (entryCycle 60.8s / 15.9s / 11.7s) while guard_health.json published status: healthy, entriesEnabled: true. HALF ONE - ENTRY PERMISSION: a material stall makes the completed-cycle verdict status: degraded, entriesEnabled: false, blocking only NEW exposure. If the protection phase has already crossed the freshness threshold, optional entry work is skipped on that same iteration; if entry work creates the overrun, the completed degraded verdict blocks the next iteration and the order-time guard immediately. Exits, stop-outs, covers, R24 flatten and every other risk-reducing action remain upstream of and independent from this gate. The first in-budget completed cycle restores permission with no cooldown. This is a safety invariant, not an expectancy claim - R18's 15-episode bar does not apply. HALF TWO - AGGREGATE DEADLINE: per-call budgets do not bound a cycle, so every cycle carries a 150-second aggregate budget (PROTECTION_CYCLE_DEADLINE_SECONDS in tools/stop_guard.py) - 3x the observed 25-50s normal upper range with 11 positions, and still ahead of R49's unchanged 180s watchdog liveness threshold. Known blocking broker, Yahoo and subprocess waits inherit the smaller of their own wall and the cycle's remaining budget; phase checkpoints abort accumulated overrun, publish degraded and skip entries. Two consecutive deadline misses make the daemon exit nonzero after publishing the degraded verdict and releasing its identity lock, for the independent watchdog to start fresh. This does not weaken R49's no-two-daemons invariant and does not change the 180s liveness threshold, the 3/day recovery cap, the 300s cooldown, or any stop/target/sizing logic. ENFORCED 2026-09-24 - HALF THREE, ORDER-TIME FRESHNESS PARITY. The R10/R22 order-time gate (stop_guard_supervision.entry_liveness; one production caller, moomoo_trade.entry_guard_status) derived its staleness ceiling as pollSeconds x ENTRY_HEARTBEAT_MULTIPLE = 15 x 4 = 60s, but applied it to updatedAt, which stop_guard.py writes once per COMPLETED protection cycle (_cycle_completed_at). A poll interval and a cycle interval are different clocks - a cycle contains many polls, and a cycle's normal publication period is its work PLUS the POLL_SECONDS sleep - so the gate held that stamp to a bound 2.5x tighter than half two's own 150s cycle budget and refused new exposure in states this rule certifies as healthy. EVIDENCE: 70 age-based stop-guard-unhealthy refusals across six trading days (2026-09-16/17/18/21/22/23); 59 of 70 cited an age at or under half two's 150s budget and 61 of 70 at or under R49's 180s liveness wall; 2026-09-23 alone refused 17 entries at ages 62-82s while the daemon completed every cycle and covered stops all day. THE CHANGE: the order-time ceiling is the same protection-cycle budget half two already defines (PROTECTION_CYCLE_DEADLINE_SECONDS, 150s) as a single shared source of truth, not a second literal, so the order boundary enforces the daemon's published freshness contract instead of a stricter independent one. UNCHANGED AND STILL FAIL-CLOSED: a live lock owner, matching lock/heartbeat instance and processStartToken identities, status healthy, entriesEnabled true, valid in-range pollSeconds, and a non-future stamp. A dead or replaced daemon is caught by the owner probe, not by the age ceiling, so raising the ceiling admits nothing under a dead guard; a stalling daemon is caught by half one, which publishes degraded/entriesEnabled false at exactly the same 150s, so the two gates coincide at the boundary by construction rather than by coincidence. This does NOT adopt livenessAt for entry permission - R49's invariant that a mid-cycle liveness stamp cannot grant entry permission stands. HONEST RESIDUAL: between 60s and 150s, entry permission now rests entirely on half one's degraded verdict; before 2026-09-23 the 60s ceiling was the only stall backstop, which is why the 2026-09-21 outage refusals were correct under the rules then in force and are not re-adjudicated here. Safety invariant, not an expectancy claim - R18's 15-episode bar does not apply and no stat block is offered because no edge is claimed. ENFORCED 2026-09-24 by the recurring Codex action-log repair session: PROTECTION_CYCLE_DEADLINE_SECONDS moved into tools/stop_guard_supervision.py, stop_guard.py imports that same object, entry_liveness compares updatedAt against it instead of pollSeconds x 4, and the dead ENTRY_HEARTBEAT_MULTIPLE constant was removed. Regression: tools/tests/test_stop_guard_supervision.py pins the 2026-09-23 62-82s refusals as now-allowed, 151s/1380s as still refused, the shared-object identity between daemon wall and order ceiling, and a dead/unverified owner at 5s as still refused. ENFORCED 2026-09-30 - HALF FIVE, PRECHECK IS INSIDE THE WALL AND MUST NOT SPEND CHECK_CYCLE'S SHARE. main() arms the cycle deadline at the top of every cycle, but the mins >= EOD_FLATTEN_ET block that follows it - the whole preCheck phase - contained no protection_cycle_checkpoint() at all, so the first checkpoint reachable from a cycle was check-cycle:start. EVIDENCE: 2026-09-29 cycle 718. account-state timed out at 20:03:36Z (rpc phase, 15.0s/15.0s); the branch's unbounded mt.cmd_flatten ran to 20:08:29Z; preCheck finished at 322.0s against the 150s wall; check_cycle() was entered, raised on its first line and returned held={} - zero stops evaluated for 324.9s (guard-cycle-stalled cycle 718, dominantPhase preCheck). The branch's trigger is a degraded OpenD and it is live from 14:41 CT with 19 minutes of session left, so the worse the broker behaved the longer the guard stopped polling stops. MEASURED ATTRIBUTION: order-refused was stamped at 20:03:52Z and the guard's next event at 20:08:29Z; the only code between them is trade_log.log_event's synchronous refresh_json_mirror() - all_entries() over a 60k-row table plus a 31.6 MB save_json into the OneDrive-synced src/data/ - i.e. 277 of the 322 seconds. stop_guard bypassed that rebuild for its own log_event in 2026-09-23 but every mt.cmd_flatten/cmd_cancel/place call it makes went through moomoo_trade and paid it in full. THE CHANGE: (a) trade_log.MIRROR_REFRESH_ENABLED is disabled process-wide by the daemon, so no protection path rewrites that file synchronously; (b) preCheck runs under a reserved EOD_PRECHECK_BUDGET_SECONDS = 60s (15s RPC startup + 15s RPC + 30s flatten, derived not literal) and opens with a checkpoint, so an overrun reports phase preCheck where it happens; (c) both R24 flatten call sites run through run_bounded_flatten(), which waits at most EOD_FLATTEN_ATTEMPT_BUDGET_SECONDS = 30s - measured from 104 real flatten bursts since 2026-08-01, worst single <=10-order batch 24.0s, p90 12.0s, median 7.0s - and at the bound abandons the WAIT, never the flatten: it runs to completion on its own thread and no second flatten is launched while one is in flight; (d) a finally hands stop evaluation CHECK_CYCLE_RESERVED_SECONDS = 90s whatever preCheck cost. 60 + 90 = 150: the wall is NOT raised, and in a normal cycle the original deadline is still the binding one. UNCHANGED: the 14:41 flatten, its retry-on-uncovered behaviour, CRITICAL-eod-flatten-incomplete with verification unavailable, and the rule that 'cannot prove flat' means 'attempt the flatten' (the checkpoint immediately before the flatten is deliberately the non-raising kind). A preCheck overrun does NOT arm PROTECTION_CYCLE_SURRENDER_AFTER - that counter means exit-for-a-fresh-process, and a degraded broker is not a wedged daemon. TELEMETRY: protection-cycle-deadline and guard-cycle-stalled now carry stopsPolled, stopsExamined, stopsExaminedCodes, positionsInSnapshot and lastCheckpoint, and a cycle that never read the book says STOPS-NOT-POLLED in its heartbeat summary, so 'stops were evaluated and the cycle then overran' is distinguishable from 'stops were never looked at'. Safety invariant, not an expectancy claim; R18's 15-episode bar does not apply. Regression: tools/tests/test_eod_precheck_protection_budget.py. HALF SIX (ENFORCED 2026-10-05) - THE SURRENDER MAY NOT HAND PROTECTION TO A RECOVERY PATH THAT IS NOT RUNNING. Half two's PROTECTION_CYCLE_SURRENDER_AFTER=2 exit is justified solely by R49's one-minute watchdog restarting the daemon promptly; this rule's own CRITICAL-guard-daemon-exit note calls that watchdog 'the authoritative recovery path'. Twice in two trading days the surrender fired while the watchdog was itself suppressed, and the cure cost more unprotected time than the disease. 2026-10-02 (clean, self-consistent chain): guard-cycle-deadline-repeated consecutiveDeadlines=2 at 14:30:06Z, after cycles 108 and 109 ran checkCycle 184.2s and 157.9s with stopsPolled=true and all 12 positions examined in both; guard-shutdown 14:30:15Z with 12 open positions; watchdog-fires-missed at 14:34:29Z reporting gapSeconds=273.0 and suppressedFires=3, i.e. the last COMPLETED watchdog run ended about 19 seconds BEFORE the surrender and not one completed during the gap; guard-protection-gap 276.3s, startupKind=cold-start. A cycle that was slow but still evaluating every stop about every 160s was therefore traded for 276.3s of ZERO stop evaluation on 12 positions. 2026-10-01: the same shape with 10 positions inside a 566.6s zero-polling gap (that day also carried a watchdog-restart-error WinError 5 whose 54-second share the 2026-10-04 R49/R54 process-state correction has since removed; what is left of that gap is this). THE GATE: before raising ProtectionCycleSurrender the daemon must read .tmp/guard_watchdog.json's lastCompletedRun.completedAt - a local JSON read, no broker call, no new dependency, the same stamp guard_watchdog.missed_fire_seconds() already uses - and may surrender only if a watchdog run completed within WATCHDOG_MISSED_FIRE_INTERVALS x WATCHDOG_TRIGGER_INTERVAL_SECONDS (3 x 60 = 180s, the identical window the watchdog itself already calls an outage). Missing, unparseable, or older than that window: the surrender is WITHHELD. The daemon stays up, half one keeps publishing status degraded / entriesEnabled false so NO new exposure is added, stop evaluation continues, and one routine guard-surrender-withheld event records the watchdog gap and the consecutive withhold count. That event is deliberately NOT error-shaped: the concurrent watchdog-fires-missed already pages the critical lane, and a second error-shaped event per cycle would only duplicate it. The gate is re-evaluated every cycle, so the surrender fires the moment recovery is demonstrably available - which on 2026-10-02 was 14:34:29Z, about four minutes before the cold-start restart actually landed. THREE BOUNDS KEEP THIS FROM PRESERVING A WEDGED DAEMON: (a) the withhold requires the daemon to still be protecting - a deadline cycle that recorded stopsPolled=false surrenders regardless, because staying up then protects nothing and a cold start is strictly better; (b) consecutive withholds are capped by PROTECTION_CYCLE_SURRENDER_WITHHOLD_MAX = 5 (about 790s at the observed 157s cycle - chosen to exceed the worst observed watchdog outage of 566.6s and to stay far short of a session), after which the daemon surrenders anyway and escalates CRITICAL; and (c) R49's independent stale-liveness replacement still applies - a withheld daemon that genuinely wedges stops stamping livenessAt and is replaced on the unchanged 180s predicate as soon as the watchdog resumes. ENFORCEMENT: shared watchdog constants, the pure predicate, routine-withhold path and CRITICAL cap are pinned by tools/tests/test_r54_surrender_gate.py::test_watchdog_freshness_window_is_one_shared_derived_definition, ::test_withheld_daemon_cycle_keeps_lock_and_deadline_evidence_and_logs_routine, and ::test_withhold_cap_surrenders_and_escalates_critical. NOTHING ELSE CHANGES: the 150s wall, PROTECTION_CYCLE_SURRENDER_AFTER=2, R49's 180s liveness predicate, the 120s recovery budget, the 3/day recovery cap, the 300s cooldown, the no-two-daemons invariant, and every stop, target, trail and sizing path are untouched. DOWNSIDE ANALYSIS (required of a safety-core change): this gate can only ADD daemon uptime, and the uptime it adds is uptime in which stops are polled and entries are blocked. It creates no unbounded loss, no unjournaled exposure, no direction ambiguity, and no position without enforceable invalidation; its single new risk, a wedged daemon living longer, is bounded three ways above. Safety invariant, not an expectancy claim: R18's 15-episode bar does not apply and no stat block is offered because no edge is claimed. THIS IS NOT THE CAUSE FIX: why checkCycle needs 105.2-184.2s with 12 positions remains open (backlog task-226 and task-205), and the gate is deliberately independent of that answer. One thing this window does settle - task-226's leading hypothesis is REFUTED: the surrender is not downstream of the 2026-10-01 broker-RPC storm, because 2026-10-02 carried only 2 broker-rpc-timeout events, both at 13:45:11Z and 42 minutes before the first of that day's six checkCycle stalls, and the daemon surrendered anyway. HALF FOUR'S PER-ORDER-ATTEMPT HALF ENFORCED 2026-10-06 -- it had never been built. Half four's text promised 'a checkpoint per qualified symbol AND per order attempt', but entry-cycle:candidate sits ABOVE the guarded_entry() call site and guarded_entry()/guarded_short_entry() together contained ZERO protection_cycle_checkpoint()/protection_cycle_timeout() calls, so a candidate admitted while the budget still had room then ran qualification, R47 classification, broker RPCs and order placement with no further raising checkpoint. 2026-10-06 cycle 66: entry work began 14:09:53Z (checkCycle only 44.9s), the 150s wall expired ~14:11:38Z, the last event for 446 seconds was paradigm-shift-inverted on US.XP at 14:10:04Z, and the cycle OPENED NEW EXPOSURE -- order-placed US.XP SHORT 38 @ 29.46 at 14:17:30Z, ~352s past its wall -- with protection-cycle-deadline phase=entryCycle raised by the NEXT candidate's checkpoint 9 seconds later; that 510.5s overrun was deadline #1 of the pair that surrendered the daemon at 14:20:50Z into a 202.5s gap on 7 positions. This was NOT an order-time-gate failure: the half-three freshness ceiling inside guard_order() is a point-in-time precondition and it ran while the completed-cycle stamp was still inside 150s -- the stall came after it, in the unbounded broker path, and no order-time check can bound what happens after it returns. ENFORCED: a raising checkpoint entry-cycle:<sym>:pre-order immediately before mt.place() in both entry paths -- the last point at which refusing is free, because nothing is at the broker yet -- and deliberately nowhere after it, since mt.place() writes the journal row and the stop together and a raise between submission and that record would leave exposure unprotected. Because that checkpoint necessarily sits INSIDE the per-symbol (Exception, SystemExit) handler, entry_cycle() now re-raises ProtectionCycleDeadlineExceeded explicitly before that handler, or the deadline would be absorbed as <sym>:error exactly as half four warned. auto-entry events now also carry cycleRemainingBeforeOrderSeconds and placementSeconds, the two numbers whose absence made the 446-second silence undecomposable. Risk-reducing sells, covers, cancels and flattens are not bounded by any of this, and no threshold moved. Regression: test_an_exhausted_budget_aborts_BEFORE_the_order_is_placed, test_a_deadline_from_an_order_attempt_is_not_filed_as_a_symbol_error, test_an_order_already_sent_is_recorded_before_the_abort_propagates, test_the_pre_order_checkpoint_sits_above_mt_place_in_both_entry_paths. Half seven (ENFORCED 2026-10-09) - inside check_cycle(), the budget must cut OPTIONAL work first and PROTECTIVE work last. Part one: half five stopped preCheck from spending check_cycle's 65s, but nothing stopped the four phases INSIDE check_cycle from starving the one mandatory pass. On 2026-10-08T15:23:33Z guard-cycle-stalled reported cycle work 191.7s with phaseSeconds preCheck 0.0 / checkCycle 191.7 / entryCycle 0.0 / unattributed 2.8, and both it and the paired protection-cycle-deadline (lastCheckpoint check-cycle:stale-protective-exits) carried positionsInSnapshot 9, stopsPolled true, stopsExamined 0 - the broker snapshot was in hand and not one of nine stops was compared to a price, because account_state(), reconcile_journal_wal(), cancel_resting_entry_orders() and cancel_stale_protective_exits() had spent the whole wall. The mandatory fixed-stop pre-pass now holds a reserved FIXED_STOP_PASS_RESERVED_SECONDS = 20s floor, granted from now by rearmed_fixed_stop_deadline() whenever the pre-stop phases leave it less - the same max()-shaped helper and reasoning as half five's rearmed_cycle_deadline(), one level down - and a deadline raised in those phases is caught and reported as prestop-phase-budget-expired instead of propagating past the pass. account_state() is deliberately outside that guarded block, because without its snapshot there is nothing to compare a stop against. The reserve is handed back once the pass completes, so it cannot become extra wall for the optional chandelier overlay, bar fetch or rich-exit pass; those are what an overrun gives up. This raises nothing: 150s, preCheck 85s and the 65s stop reserve are unchanged, 85 + 65 = 150 still holds, and the 20s is carved out of the 65s, sized against measured placement cost (n=154 placementSeconds: median 3.35s, p90 7.30s, p99 19.20s) rather than a guess. Widening the wall was rejected: a cycle reached check-cycle:complete with 13 positions at 151.5s of real work on 2026-10-09T14:52:02Z, so the wall is already binding at current book size and a raise would mask a capacity question (open task-230) instead of fixing an ordering defect. Part two applies the same principle to the exit-side R37 probe: kronos_exit_tightening() runs BEFORE the branches that decide and place an exit, and on budget exhaustion it re-raised ProtectionCycleDeadlineExceeded, so an optional soft signal aborted the mandatory trail/target/stop comparison it exists only to tighten - three times in three consecutive sessions (US.UUUU 2026-10-07T15:21:56Z, US.PCRX 2026-10-08T15:08:54Z, US.CLSK 2026-10-08T15:15:36Z, each an exit-error during bounded-wait followed about a second later by protection-cycle-deadline at check-cycle:rich-exit-pass). It now takes its own documented inert path with eff_stop unchanged and logs kronos-exit-probe-abandoned, matching R37's existing treatment of a forecast timeout as abandoned so stop protection can continue; every other failure mode is untouched. Verified before relying on it: nothing in guarded_sell/guarded_cover or moomoo_trade.py consults the cycle budget (R54 half three's order-time freshness ceiling is a point-in-time precondition, not a bounded wait), and a direct run placed an exit with the aggregate budget at -41.7s. KRONOS_ADVISORY_ONLY is True, so the probe could not have tightened a stop even on success - a purely observational call was preempting protective work. Fixed stops were not blind in those cycles, since the broker-price pre-pass had already checked them, so the cost was lost trail/target enforcement, not an unprotected position; the log cannot say whether an exit would have fired, only that no comparison happened. No threshold, stop, target, trail or sizing path changes. Regression: tools/tests/test_fixed_stop_pass_protection_budget.py (17 tests, including the invariant that a non-empty snapshot reporting stopsPolled true may not report stopsExamined 0) and tools/tests/test_kronos_exit_probe_budget.py (12). HALF EIGHT - PENDING ENFORCEMENT, authored by critical note-review 2026-10-26: the surrender must prove the restart is PERMITTED, not merely that the watchdog is ALIVE. Half six gated the surrender on the watchdog running; nothing asked whether it would agree to restart. guard_watchdog.restart_brake_reason() refuses outright once restarts_today >= MAX_RESTARTS_PER_DAY (3). On 2026-10-26 .tmp/guard_watchdog.json held restarts: 3 from 09:24:17 CT; cycles #49 and #50 (10:25:15, 10:30:37) hit cycle-deadline(stops-polled) at check-cycle:rich-exit-pass; the surrender raised at 10:30:37 reporting 'watchdog last completed 3.5s ago, within the 180s freshness window' and exited 1; then about 31 consecutive 60s watchdog fires logged 'no action: already restarted 3x today' while Kitty Stop Guard sat State=Ready, LastTaskResult=1, NextRunTime=10/27 08:31. That cost 32m23s of zero stop evaluation on 5 positions, ended only by a manual scripts/reload_stop_guard_supervised.ps1 at 11:03:00; unattended it ran to the close (~4h29m) and would have taken the R24 14:41 EOD flatten with it, since that flatten executes in the daemon's own preCheck block. The gate is inverted rather than incomplete: a braked watchdog does no work, so it restamps lastCompletedRun every 60s in 1-2s, making a braked watchdog the freshest possible watchdog; the 3.5s-of-180s margin was near the best score obtainable, earned because the watchdog had stopped doing the thing the gate infers from it. The deciding datum was already in hand - surrender_is_safe() receives the parsed watchdog_state dict and 'restarts': 3 sits in it unread, beside the lastCompletedRun it does read. GATE: consult that dict's restarts/lastRestart against the watchdog's own brake policy, as one shared definition rather than a second copy of the integers, and withhold when the brake would refuse. A per-day-cap withhold is deliberately exempt from PROTECTION_CYCLE_SURRENDER_WITHHOLD_MAX, because that cap's justification - that surrendering anyway is better since a cold start is strictly better - is false when no cold start will occur; brake exhaustion resets only on date change, so capping the withhold merely re-stages the same void about 5 cycles later. A cooldown-only refusal (RESTART_COOLDOWN_SECONDS = 300) stays a normal capped withhold because waiting genuinely clears it. The state is reported on the CRITICAL lane as guard-surrender-withheld-brake-exhausted, rate-limited to once per episode, because half six's routine-shaped withheld event relied on a concurrent watchdog-fires-missed to page and a braked watchdog fires fine and raises nothing. BOUNDS: half six's bound (a) is unchanged and unconditional - a cycle recording stopsPolled false surrenders regardless, brake or no brake - and both 2026-10-26 cycles were stopsPolled true, slow but still examining every stop. Missing or unparseable restarts, or a count stamped for a prior date, is not a brake. No threshold, budget, cap, cooldown, stop, target, trail or sizing path changes. Safety invariant, not an expectancy claim: R18 does not apply. DOES NOT CLOSE note-350 D2: MAX_RESTARTS_PER_DAY remains a terminal 'needs a human' gate on live protection and brakes the wedged replace path too, so an exhausted brake still leaves a genuinely wedged daemon without autonomous recovery. Half eight enforcement LANDED 2026-10-26 (tools/stop_guard_supervision.py shared restart_brake/watchdog_restart_bookkeeping with the daily-cap branch ordered before the withhold cap, tools/tests/test_r54_surrender_gate.py, CRITICAL event at tools/stop_guard.py:831, docs in commit 9805ebb); queue task a6811a4ef97f495b was cancelled as superseded rather than re-run against landed protection code. Evidence and full timeline in backlog note-350. HALF NINE - ENFORCED 2026-10-26: the per-day restart cap may be terminal on a WEDGED daemon, never on a DEAD one. Half eight stops a living daemon surrendering into a braked watchdog. The same ownerless cap refusal caused two gaps on 2026-10-26: 1,926.5s (15:30:39Z to 16:03:00Z) and 5,551.0s (17:02:51Z to 18:35:22Z), both with 6 positions, totalling 7,477.5s / 2h04m38s with zero watchdog recoveries. In the second gap, the daemon was killed instead and the same exhausted brake refused to replace a corpse. OSError Errno 28 No space left on device from 16:51:40Z, raised out of a logging flush; guard-shutdown 17:02:51Z shutdownKind uncaught-exception, lastLivenessPhase kronos-exit:CRWD:after, openPositionsAtExit 6 (TTMI, ORKA, ASST, DK, WGS, CRWD); CRITICAL-guard-daemon-exit again named the watchdog the authoritative recovery path. The watchdog spent restart #3 at 17:17:05Z on an instance that published status starting / cycle 0 at 17:17:15Z and died inside startup, so the final restart budget bought zero protected seconds; from then to 18:32:05Z about 70 consecutive 60s fires logged 'no action: already restarted 3x today'. watchdog-fires-missed reported gapSeconds 782.0 / suppressedFires 12 and later 340.2 / 4; the scheduled task read lastResult 1. MEASURED GAP 17:02:51Z to 18:35:22Z = 5551s = 92m31s of zero stop evaluation on 6 open positions, ended only because the review ran scripts/reload_stop_guard_supervised.ps1 by hand; unattended it reached the 15:00 CT close and would again have forfeited the R24 14:41 EOD flatten, which executes in the daemon's own preCheck block (half five). With half eight's incident that is 124m54s of protection lost in one session and BOTH recoveries were manual - a dependency the system is not permitted to have. WHY THE CAP'S JUSTIFICATION DOES NOT REACH THIS BRANCH: recovery_action() consults the identical brake twice. At tools/guard_watchdog.py:460-462 it gates the replace path, where a daemon is alive and relaunching risks two daemons - there 'a crash loop needs a human, not another relaunch' is sound and STAYS. At 468-470 it gates the ownerless path, whose own reason string is 'daemon gone (no live lock owner)'. There is no daemon there, so no second-daemon risk; the only real hazard is a rapid relaunch loop, which RESTART_COOLDOWN_SECONDS = 300 already bounds AND which clears by waiting. The per-day cap contributes nothing to that bound except permanence. The branch cannot be entered on a bad read: lock_alive comes from pid_is_alive, which returns False only on an OS-proved exit or an absent/unparseable lock, while access-denied and every ambiguous query fail closed as ALIVE. THE GATE: on the ownerless branch, inside the session window, the per-day cap is not terminal - once restarts_today >= MAX_RESTARTS_PER_DAY the watchdog keeps relaunching on a widened EXHAUSTED_BRAKE_RELAUNCH_COOLDOWN_SECONDS (900s) and raises a CRITICAL-lane event on each such relaunch carrying restartsToday, lastRestart, liveness age and open-position count. The replace branch keeps the cap exactly as it is. BOUNDS: in_session() unchanged, so a dead daemon after hours still gets nothing; relaunch rate capped at 4/hour, about 26 across a 6.5h session, against today's 0; a relaunched daemon publishes entry:disabled-first-protection-cycle (observed live in cycle #1 at 18:35:22Z) so a relaunch loop cannot add exposure, only restore stop evaluation; MAX_RESTARTS_PER_DAY, RESTART_COOLDOWN_SECONDS, R49's 180s liveness predicate, HEARTBEAT_STALE_SECONDS, the 2026-10-06 ownerless-lock latency correction, the no-two-daemons invariant and every stop, target, trail, flatten and sizing path are untouched. DOWNSIDE: worst case is a daemon that crashes on every startup being relaunched every 15 minutes for the rest of a session - about 20s of CPU and one CRITICAL row per attempt, each attempt entry-disabled - against 92m31s of unevaluated stops on 6 positions plus a forfeited R24 flatten. No unbounded loss, no unjournaled exposure, no position without enforceable invalidation. Safety invariant, not an expectancy claim: R18's 15-episode bar does not apply; the defect is proved by reading both mechanisms (CRITICAL-guard-daemon-exit names the watchdog as the authoritative recovery path while the watchdog's own code refuses to be one) and the two measured gaps establish cost and frequency, not existence. COMPANION ENFORCEMENT: D3, the fatal exception came out of a logging flush (logging/__init__.py:1164 emit -> flush -> OSError, via guard_broker_rpc.worker_main -> moomoo_lib.open_trade_context -> moomoo/common/ft_logger.py, with kitty's own handler failing the same way) ; both the daemon and separately spawned broker worker now set logging.raiseExceptions false before SDK logging, and guard-owned file handlers absorb OSError; genuine non-logging failures keep the existing loop-crash/shutdown/CRITICAL path. D4 remains inconclusive: Errno 28 fired with 87.51 GB free on C: and with fresh 50 MB writes to both the repo .tmp and %TEMP% succeeding at over 1.4 GB/s; fsutil quota query C: returns Error 5 Access is denied so an NTFS per-user disk quota is not excluded and is the leading hypothesis for ERROR_DISK_FULL alongside free space, and the repo also sits under OneDrive; no matching disk/NTFS/OneDrive event or accessible OneDrive diagnostic proved the cause. cleanup_tmp now enforces a narrow 30-day SDK daily-log retention predicate; this pass did not delete the 2026-10-26 evidence, and logs/archive remains under its bounded GFS policy. Regression: test_ownerless_at_cap_relaunches_when_widened_cooldown_clears, test_ownerless_at_cap_names_remaining_widened_wait, test_ownerless_at_cap_relaunches_after_hours_of_refusals, test_replace_branch_keeps_daily_cap_terminal_at_any_age, test_the_fast_path_is_still_inside_the_session_window_only, test_ownerless_below_cap_still_obeys_the_ordinary_cooldown, test_watchdog_restart_brake_is_one_shared_definition, test_fresh_watchdog_with_exhausted_restart_brake_withholds_uncapped, test_guard_owned_file_handler_absorbs_flush_oserror_even_in_debug_mode, test_raiseexceptions_false_covers_standard_sdk_emit_flush_path, and test_broker_worker_reapplies_logging_configuration_in_spawned_child. Evidence and full timeline in backlog note-351 and note-352.
rule authored by critical note-review 2026-09-22; enforcement auto-applied by recurring Codex action-log repair session 2026-09-23; evidence: .tmp/action_review_window.json (guard-cycle-stalled cycles 47/49/50/14/24, watchdog-restart-error x2, guard-protection-gap 943.3s), broker verified flat 2026-09-23. Regression: tools/tests/test_guard_liveness.py, tools/tests/test_guard_watchdog.py, tools/tests/test_stop_guard_broker_rpc_budget.py. Mirror row added by critical note-review 2026-09-23 after the rules.json/trading_lessons.md parity test went red; write-up in internal notes Half three added by batch note-review 2026-09-24 and enforced the same day by the recurring Codex action-log repair session; evidence: 70 age-based order-refused events in src/data/trade_log.db/json across 2026-09-16..2026-09-23 (re-derived independently), tools/stop_guard_supervision.py:13-37,194-250, tools/stop_guard.py:83-88,126,4672. Half six authored by critical note-review 2026-10-05 and enforcement auto-applied by FIX session 2026-10-05 with PROTECTION_CYCLE_SURRENDER_WITHHOLD_MAX = 5; Regression: tools/tests/test_r54_surrender_gate.py::test_watchdog_freshness_window_is_one_shared_derived_definition, ::test_withheld_daemon_cycle_keeps_lock_and_deadline_evidence_and_logs_routine, and ::test_withhold_cap_surrenders_and_escalates_critical; evidence re-derived from .tmp/note_review_window_critical.json: guard-cycle-stalled cycles 108/109/253/256/432/433 (checkCycle 184.2/157.9/108.8/137.0/105.2/157.3s, all 12 positions, stopsPolled=true), guard-cycle-deadline-repeated 2026-10-02T14:30:06Z, guard-shutdown and CRITICAL-guard-daemon-exit 14:30:15/14:30:43Z, watchdog-fires-missed 14:34:29Z (gapSeconds 273.0, suppressedFires 3), guard-protection-gap 14:35:57Z (gapSeconds 276.3, startupKind cold-start), and the 2026-10-01 counterpart at 14:20:12-14:36:22Z with 10 positions and gapSeconds 566.6 Half nine authored by critical note-review 2026-10-26 and enforced by interactive FIX session 2026-10-27; corrected evidence: two ownerless daily-cap gaps totalling 7,477.5s / 2h04m38s on 6 positions, backlog note-351 and note-352; regression: test_ownerless_at_cap_relaunches_when_widened_cooldown_clears, test_ownerless_at_cap_names_remaining_widened_wait, test_ownerless_at_cap_relaunches_after_hours_of_refusals, test_replace_branch_keeps_daily_cap_terminal_at_any_age, test_watchdog_restart_brake_is_one_shared_definition, and test_fresh_watchdog_with_exhausted_restart_brake_withholds_uncapped.
ENFORCED 2026-09-26. A BUY whose --strategy resolves to a watchlist row carrying a positive numeric entryTrigger is REFUSED (below-entry-trigger) unless its LIMIT PRICE is at or above that trigger, and a trigger-conditioned win rate may never be quoted for an entry that never triggered. EVIDENCE: on 2026-09-24 13:51 CT a session placed US.SMCI BUY 100 @ limit 42.20 (filled 42.107) under next-day, whose row declared entryTrigger 42.74 -- a fill 1.48% below trigger -- and the journaled thesis (trade_journal id 58e00607) both admitted it was 'entering preemptively' and cited '81.82% win rate on this setup'. next_day_watchlist.json's own method field defines that statistic as counting a historical trade only when the next session TRIGGERED the entry, so the number is conditioned on the trigger firing. THE GAP WAS STRUCTURAL: stop_guard.next_day_trigger_quality() already failed closed on last < trigger (below-next-day-trigger), but moomoo_trade.py's BUY branch checked direction, paper-security support, the ripcord halt, the 14:30 CT cutoff and the watchlist universe and had NO trigger check -- the same order-time/entry-time disagreement as R43's direction-mismatch. The order-time gate is now synchronized. THE COMPARAND IS THE LIMIT PRICE, NOT A FRESH QUOTE: the guard prices its own entries at last x (1 + ENTRY_LIMIT_PREMIUM) only after clearing last >= trigger, so guard entries pass by construction and no re-quote can manufacture a spurious refusal. The rule is written against the FIELD, not the tag, so any future trigger-conditioned list inherits it. Exact trigger and above pass; missing/null/zero/non-numeric entryTrigger is unaffected; SELL, SHORT, BUY-to-cover and the R24 flatten are untouched -- this gates NEW EXPOSURE only. NOT AN EDGE CLAIM: the SMCI trade's -$30.70 is not the justification and no stat block applies; this is enforcement consistency (a documented entry condition enforced on one of two paths), judged under Track B, not an R18 expectancy verdict. Frequency is honestly unknown rather than low -- only 2 of the tag's 113 journaled BUY rows quote a numeric trigger at all. Not a safety-core rule: the safety core is execution/risk plumbing, not entry filters. SCOPE LIMIT RECORDED 2026-10-10 (filed, not enforced): the 'written against the FIELD, not the tag' inheritance above does NOT reach lists that name their break-above price `trigger` instead of `entryTrigger`. The live case is lux-smc (R43 non-inverted, lifecycle active): stop_guard's kind == 'lux-smc' entry gate refuses last < row.trigger as below-bos-trigger with a trigger + 2 x atr14d no-chase cap, while moomoo_trade.numeric_entry_trigger() reads entryTrigger only, so the order-time half is missing for that tag. Filed rather than enforced on measurement: of 119 lux-smc BUY journal rows, 115 are ENTRY-GUARD auto-entries gated at entry time and all 4 hand-placed ones were at or above their cited trigger (SMA 34.86/35.10 vs 33.85 2026-07-16; ITUB 10.02 vs 8.62 2026-10-05; HUM 437.24 vs 415.77 2026-10-09, cancelled by R12), so zero violations exist. A blanket second field name would be WRONG: `trigger` means different things per list (vixfix's is a prior WVF reading, dip/fade lists buy BELOW a level, and R43-inverted lists execute SHORT and are outside a BUY gate), so enforcement requires a declared tag->field map mirroring stop_guard's per-kind predicate (one member today: lux-smc -> trigger). See backlog task-274. The enforced entryTrigger half is unchanged.
rule auto-applied by batch note-review 2026-09-25; enforcement auto-applied by Codex FIX_MODEL session 2026-09-26; mirror row added by overnight review 2026-09-26 after the rules.json/trading_lessons.md parity test (tools/tests/test_generate_strategy_diagrams.py) went red. Evidence: src/data/trade_journal.json id 58e00607 (2026-09-24T18:51:59Z), src/data/next_day_watchlist.json method, tools/stop_guard.py:2816-2831, tools/moomoo_trade.py. Regression: tools/tests/test_r55_entry_trigger.py. Write-ups in internal notes and [internal note]
ENFORCED 2026-09-30. A NEW ut-bot entry is refused while minutes since the 09:30 ET open < 60, in both layers via one pure function (tools/entry_time_floor.py STRATEGY_MIN_SINCE_OPEN): stop_guard.guarded_entry skips with r56-min-since-open, and moomoo_trade.py's new-exposure BUY branch refuses with before-strategy-open-floor. A missing clock reading is inert; SELL, SHORT, cover and the R24 flatten are untouched. EVIDENCE: all 96 closed ut-bot episodes in trading.json closedEpisodes, bucketed by first-lot fill time: first 60 minutes -$898.10 over 50 (28.0% WR), 60+ minutes +$1,027.03 over 46 (39.1% WR); first hour lost in July (-$708.36, n=26) and September (-$263.52, n=14), slightly positive only in small-n August (+$73.78, n=10). Week 2026-09-23..30: first hour 0W/8L -$429.90, after 4W/5L +$307.02, every winner at 71m or later. NOT A PROVEN EDGE: permutation p=0.15 vs the 60m+ baseline, per-episode mean CI [-$52.03, +$17.31] includes zero, and the 60m cut was chosen on the same data; the September holdout mean is -$18.82. It removes a bucket whose measured expectancy is at best zero. Deliberately NOT R31: R31 permits 15-60m (ut-bot's worst window) and vetoes 60-120m where MXL/CDNA won; a test asserts the floor table never overlaps R31's roster. RETIRE/RELAX if, after >=15 further post-10:30 ut-bot episodes, that bucket's mean turns <= 0, or if a forward shadow count of r56-min-since-open skips shows the first hour would have been net positive. AMENDED 2026-10-31 (overnight review, Track A): the extension criterion is now DIRECTION-SCOPED and mechanically checkable. A new STRATEGY_MIN_SINCE_OPEN entry requires entry_time_floor.bucket_split_verdict(episodes, floor, strategy=tag, r31_strategies=stop_guard.R31_STRATEGIES) to return 'supported'. That pure function scopes the split to operative_direction() (the direction of the tag's most recent episode) and counts the rest as excludedOtherDirection instead of pooling them; returns 'insufficient-sample' unless BOTH cohorts clear R18's 15; returns 'not-supported' when the blocked bucket is not measurably worse than what survives (such a floor starves the strategy); and returns 'forbidden-r31-overlap' for any tag in R31_STRATEGIES, making the pre-existing non-overlap invariant a positive refusal. The R31 roster is passed in, not imported, because stop_guard imports this module. WHY: a lifetime split on an R43-inverted tag pools two directions and the time bucket absorbs the regime change. Worked near-miss: vixfix-trend minutes<30 read -0.4385 R (n=26) vs -0.0448 R (n=305) lifetime, p=0.0824 with a mean CI excluding zero, but +0.0469 R (n=13) vs +0.0423 R (n=219), p=0.9883 scoped to SHORT, the direction R43 left it trading; the whole effect sat in the retired pre-inversion LONG era (n=14, -0.9238 R). NO LIVE ROUTING CHANGE: the table is still exactly {ut-bot: 60}. Re-validated: ut-bot @60 is 'supported' direction-scoped (LONG-only, blocked -0.2541 R n=41 vs kept +0.2565 R n=65), so R56 clears the stricter bar in R units its USD-only evidence never showed; max-loser-breakdown @30 is 'not-supported' (early bucket is the better one) and gap-go @30 is 'forbidden-r31-overlap' (and p=0.3090 besides). Regression: tools/tests/test_r56_min_since_open.py.
auto-applied by interactive judgment session (Opus) at the operator's direction, 2026-09-30, from the weekly trade-coach review (journal lesson L20260930a). Evidence: src/data/trading.json closedEpisodes joined to src/data/trade_journal.json entry timestamps. Regression: tools/tests/test_r56_min_since_open.py. Write-up in internal notes
ENFORCED 2026-10-01. merge_closed_trades() in tools/moomoo_sync.py may DELETE a persisted round-trip when both its deal ids appear in authoritative_deal_ids and the pair is absent from the fresh FIFO output (the 'stale reconstruction' path). SIMULATE rejects deal_list_query and history_deal_list_query, so fills_from_orders() synthesizes that id set from the ENTIRE retained journal -- it therefore spans all history even when the broker answered nothing, licensing a deletion performed by a re-pairing built from strictly less evidence than the rows it removed. closed_history_is_authoritative(hist_deals, timing) now gates the license: real broker deal rows authorize it outright, otherwise syncTiming.historyOrdersOk must be true (the row count cannot distinguish a rate-limited failure from a genuinely empty history). When withheld, main() passes authoritative_deal_ids=None: persisted rows are retained, fresh id-bearing pairs are admitted only when neither leg is already consumed by a persisted row, legacy no-id rows keep field-tuple handling, and a closed-history-recompute-withheld sync event is logged. The live account snapshot (funds, positions, open orders) publishes identically either way, so a degraded sync still serves the stop guard. EVIDENCE: the 2026-09-30 15:30 CT sync recorded historyOrdersOk false / historyOrderRows 0 / historyOrdersSeconds 12.64 and rewrote settled history -- 13 episodes from 2026-07-20..2026-09-28 vanished (+$265.32 between them) and 5 had P&L rewritten (20260720-EDZ-01 -176.40 -> +699.63, 20260720-CLSK-05 +19.69 -> -359.35; net delta +$386.00). All are low-frequency symbols, the signature of one shifted lot boundary cascading through per-symbol FIFO numbering. The distortion exceeded the book's entire lifetime net (+$173.01 over 3,568 episodes): the 09-29 scoreboard cutoff moved from 3,558 episodes/+$217.50 to 3,547/+$494.41. R18 batches, R34 lock/release and R44 sizing all read this file. PREVENTION, NOT REPAIR: the 2026-09-30 damage is persisted and neither pairing is broker-confirmed; both sides are preserved in state/episode_history_forensics/2026-09-30_closed_history_rewrite.json and (because state/* is gitignored) in wrgvault/Knowledge/Kitty/Self-Improvement/data/, and reconciliation needs one successful history_order_list_query (backlog task-211). Treat episode history older than the most recent successful history pull as provisional to roughly that magnitude. Not a safety-core rule. Regression: tools/tests/test_moomoo_sync.py::TestClosedHistoryDeletionLicense (6 cases incl. a granted-license control). HALF TWO (ENFORCED 2026-10-05 BY FIX SESSION; AUTHORED BY CRITICAL NOTE-REVIEW) -- ADDITIVE IS NOT SAFE: A WITHHELD SYNC MAY NOT ADD A ROUND-TRIP WHOSE FILLS ARE ALREADY CONSUMED. Half one's remedy rests on an assumption its own text states as fact -- that the additive-only merge "may still ADD newly reported round-trips but cannot rewrite settled ones." That assumption is false and is now measured. The journal-only FIFO produced by a failed history query does not merely omit pairs, it RE-PAIRS the same opening fills against different closes; because the merge is keyed on (openDealId, closeDealId), each re-pairing is a key the persisted set does not contain, so it is admitted as a new row while the original row it contradicts is retained. The settled ledger is then rewritten by DUPLICATION rather than by deletion, through the identical lot-boundary divergence half one blocked in the other direction. MEASURED 2026-10-05 (re-derived offline against the live files, no writes): fills_from_orders([], journal.trades) -> fifo_closed_trades() -> merge_closed_trades(prev, fresh, None) against src/data/trading.json at closedTrades=4129 yields 4143 rows -- 14 added, +$387.63 between them -- and moves the summed ledger from $70.80 to $458.43 -- about 6.5x -- while the headline stats.realizedPnl published at the deflated snapshot read $68.80 (the $2.00 gap between the stats block and the sum of the rows in the same file is itself consistent with the concurrent-writer hypothesis in backlog task-231, and is not load-bearing here). CONFIRMED ON DISK, NOT ONLY IN SIMULATION: the 2026-10-05T18:20:06Z sync recorded historyOrdersOk=false / historyOrderRows=0 / closedTradeDeletionLicense=false and published exactly the predicted inflated state -- src/data/trading.json now carries closedTrades=4143, stats.realizedPnl=$458.43, and all three CLSK rows simultaneously (the persisted 358sh +19.69 alongside the duplicate 339sh +172.21 and 19sh -42.37). The offline derivation predicted $458.43 before that write landed. ALL 14 added rows re-use BOTH an openDealId and a closeDealId already consumed by a persisted row, i.e. not one of them is a genuinely new round-trip: order-719842 (EDZ) appears as the open leg of two added pairs closing 2026-07-27 and 2026-07-29, and order-718584 (CLSK) opens both an added 339sh and an added 19sh pair. THE CLEAREST SINGLE CASE: order-718584 (CLSK, a 358-share opening fill) is persisted exactly once as US.CLSK LONG qty 358 order-718584 -> order-718971 pnl +19.69 closed 2026-07-20, and the journal-only FIFO re-splits that same fill into an added 339sh pair (-> order-719360, +172.21) AND an added 19sh pair (-> order-742160, -42.37). 339 + 19 = 358: the identical shares are booked twice and that one opening fill contributes +$149.53 instead of +$19.69. 20260720-CLSK-05 +19.69 is the very row half one's evidence block names as rewritten in the DELETION direction on 2026-09-30 -- same lot, same divergence, opposite sign. Direction consistency is not preserved either: order-719842 (EDZ) is persisted as the CLOSE leg of 8 SHORT round-trips of 147sh each and re-paired as the OPEN leg of two added LONG round-trips (128sh +355.20, 140sh +217.70), so one order is simultaneously a short cover and a long open. The +$387.63 magnitude is within $1.63 of half one's own +$386.00 2026-09-30 deletion delta on the same low-frequency symbols (EDZ, CLSK, KEEL, NVDA, BEKE), which is the consistency check that these are one divergence seen from two sides. OCCURRENCE RATE: 8 closed-history-recompute-withheld events between 15:54 and 18:00Z on 2026-10-05 alone, and prevClosedTrades in those events oscillates 4125, 4125, 4140, 4140, 4127, 4141, 4127, 4142 -- the ledger flaps by ~14 rows and ~$390 depending on which sync wrote last, which is exactly the condition half one was written to end ("a losing batch can appear or disappear on nothing but a rate-limited broker call"). R18 batch boundaries, R34 lock/release, R44 sizing and the strategy scoreboard all read this file, so those gates are currently evaluated on non-deterministic input. THE OPERATIVE CHANGE: when the deletion license is withheld, a freshly computed pair may be ADDED only if NEITHER of its legs (openDealId, closeDealId) already appears as a leg of any persisted row. A pair whose legs are already consumed is DISCARDED, not added and not used to replace the persisted row -- a degraded sync has strictly less evidence than the row it would contradict, so it gets no vote either way. Genuinely new round-trips, whose two legs are both unseen, are still admitted, so today's closes continue to publish on a degraded sync and nothing is delayed to the next authoritative pull. Legacy rows without deal ids keep half one's field-tuple path unchanged. The live account snapshot (funds, positions, open orders) is unaffected, so the stop guard is untouched. DOWNSIDE ANALYSIS: the only thing this can lose is a true round-trip that legitimately shares a deal id with a persisted row, which cannot exist -- one fill belongs to exactly one lot -- and in the pathological case where a persisted row is itself the wrong pairing, the correction simply waits for the next sync whose history query succeeds and which therefore holds the deletion license. That is the same prefer-the-better-evidence ordering half one established. Safety/integrity invariant, not an expectancy claim: R18's 15-episode bar does not apply and no stat block is offered because no edge is claimed. NOT a safety-core rule (it touches no stop, size or order path), so workflows/trading_safety_core.md is unchanged. ENFORCED 2026-10-05: merge_closed_trades() carries the leg-collision guard and tools/tests/test_moomoo_sync.py pins the 14-row/+$387.63 case as discarded plus a granted-license control that still deletes; enforced by FIX session 2026-10-05. Regression: tools/tests/test_moomoo_sync.py::TestClosedHistoryDeletionLicense::test_withheld_license_discards_the_pinned_clsk_duplication, ::test_withheld_license_still_admits_a_genuinely_new_round_trip, ::test_withheld_license_discards_a_one_leg_collision, ::test_granted_license_still_supersedes_a_stale_pairing, and ::test_legacy_row_without_deal_ids_keeps_field_tuple_upgrade_path.
auto-applied by scheduled overnight review (Opus), 2026-10-01, reviewing the 2026-09-30 trading day; evidence: syncTiming in src/data/trading.json, the 2fc30959->e19dec0b episode diff in state/episode_history_forensics/, and sync-slow/sync-error action-log events. See vault report [internal note] Half two authored by critical note-review 2026-10-05 from .tmp/note_review_window_critical.json (8 closed-history-recompute-withheld events 15:54-18:00Z with oscillating prevClosedTrades, 5 sync-slow events all dominantPhase=processingSeconds / historyOrderRows=0); the 14-row/+$387.63 delta was re-derived offline against src/data/trading.json and src/data/trade_journal.json via tools/moomoo_sync.py's own fills_from_orders/fifo_closed_trades/merge_closed_trades, read-only. Write-up in internal notes; backlog note-229, task-230, task-231; fix brief 8267738ce0df4833 enforced by FIX session 2026-10-05.
ENFORCED 2026-10-01. An ARMED trailing stop must sit in profit at the moment it arms. In stop_guard.effective_stop() a SHORT's trail stop is trough*(1+w) and the arm fires at trough <= entry*(1-a), so at the instant of arming the live stop is entry*(1-a)*(1+w); it is protective only when (1-a)(1+w) < 1, i.e. w < a/(1-a). The LONG mirror anchors above entry, giving the tighter bound w < a/(1+a). short-gap-open-close ran w=3% against a=2%: the trail armed at entry*1.0094 -- 0.94% ABOVE entry -- and could not exit in profit until MFE passed 2.91%, which only 26% of its 39 MFE-measured episodes (2026-07-23..2026-09-30) ever reached against a 2.20% median MFE, so the MEDIAN episode's trail could only close red. 2026-09-30 confirmed both cases live: 20260930-TXG-06 peaked at 2.998% MFE (~w, the predicted breakeven case) and its trail fired at 93.72 against a 93.555 entry for +$0.49/+0.00R, while 20260930-CAPR-12 peaked at 3.972%, cleared the bound, and fired at 9.8799 against 9.945 for +$6.99/+0.05R -- 3.8% of the $184.07 excursion it generated. Lifetime 50 attempts, 25W/25L, +$24.84: a 50% hit rate going nowhere, the signature of a capture defect rather than a broken signal. SAME DEFECT the 2026-09-29 review struck from max-loser-failed-vwap (2%/1%); that sweep emptied SHORT_TRAIL_OVERRIDES but could not reach a width living in module constants. Auditing the dispatch site found two more live configs with the identical geometry, because trail and trail_arm are selected there by INDEPENDENT conditionals so a kind can inherit one and default the other: dilution-open-trail2 at 2%/1% (arming 0.98% below breakeven) and dilution-open-trail1-arm05 at 1%/0.5% (0.495% below; journal row 7c80d263's own lesson records that the first uptick 99s after entry covered it, CBRS -$16.00/-0.14R). OPERATIVE: SHORT_GAP_OPEN_CLOSE_TRAIL_PCT is 0.02 (was 0.03) -- the widest width satisfying the invariant at a=0.02, so the tag keeps the deliberate 'widest trail in the system, not a scalp' character of its 2026-07-31 human directive (still 2x the 1% SHORT default). protective_trail_pct() enforces the invariant at the SHORT dispatch site, clamping a violating width to the bound (floored to basis points) and logging a routine trail-geometry-clamped event. SCOPE: armed trails only -- an unarmed trail (LONG tj-pmh 5%, etf-scalp 2%, R25's TIGHT_TRAIL_PCT) trails from entry by design and is out of scope, not in violation. A 5bp TRAIL_GEOMETRY_TOLERANCE_PCT keeps the standard 1%/1% pair untouched (clears outright SHORT, misses by 1bp LONG). HONEST LIMITS: the outcome sample does NOT establish an edge (mean +0.1393 R, 95% CI [-0.2057,+0.5300] includes zero, sign-flip p=0.4800) and NO P&L improvement is claimed -- the change rests on the arithmetic plus the MFE distribution. The width is derived, not fitted; one candidate was evaluated, so no multiple-comparison problem, and the split is within a single tag so there is no composition confound. MFE cannot simulate a path: a narrower trail gives a runner 1% less consolidation room and this tag's p90 MFE is 8.16% (max 13.14%), so the cost of cutting runners short is NOT quantified. The tag is mid-R18 RETESTING cycle 2 at 1/15, so fixEffectiveAt is restamped 2026-10-01 02:00:00 and the next 15 episodes are the declared forward test. Clamping dilution-open-trail2 collapses it onto dilution-open-trail1, erasing that A/B rung -- tolerable only because the family emits nothing (task-60) and trail2 has 0 lifetime attempts; re-specifying the ladder is filed, not done. Per-episode P&L inherits R57's provisional-history caveat; the MFE figures do not, being journal- and bar-derived. Not a safety-core rule. Regression: tools/tests/test_trail_geometry.py (20 cases, 3 driving the real effective_stop() mechanic).
auto-applied by scheduled overnight review (Opus), 2026-10-01, reviewing the 2026-09-30 trading day; evidence: bar-derived MFE telemetry on 39 short-gap-open-close episodes in src/data/trade_journal.json, episodes 20260930-TXG-06 / 20260930-CAPR-12, and short-trail-stop-cover events in the action log. See vault report [internal note]
ADDED 2026-10-06. Short an intraday fade through -5% in a name whose offering was announced today, or on the previous trading day at/after the 16:00 ET close; hold to the 14:41 CT flatten behind a 3% hard buy-to-cover stop with NO trailing cover. Closes the dilution family's structural blind spot: dilution_short_report.py screens at 08:31 CT and keeps only names already negative at the open, so it sees gap-downs only. Measured on 2026-10-06 against that day's 41 qualifying losers, for the 25 largest: 2 (8%) were already <=-5% at the 08:31 instant, 23 (92%) were not, and ZERO had gapped down >=5%; SYRE opened -0.69% and crossed -5% at 09:40 ET. Entry requires the confirmed fade (below the regular-session open AND below VWAP, 30-minute trend non-positive), >=5% intraday loss, >=1x volume pace, broker short inventory, and an entry window closing 13:00 ET / 12:00 CT so the trade has room before the flatten. Candidates come from tools/dilution_fade_scan.py; a scan not dated today emits nothing (dark, never stale). NO TRAILING STOP is the one point where this contradicts the proposal that prompted it (a 2% trail): over the 10 captured fade episodes hold made +4.27% mean / 90% WR / +3.45% ex-best against every one of the nine trail widths the sweep actually ran, all at its single 1% arm (dilution_fade_report.json entryRule.trailArmPct = 0.01): 5% +3.15%, 4% +2.63%, 3% +2.28%, 2.5% +2.11%, 2% +1.53%, 1.5% +0.67%, 1% +1.03%, 0.75% +1.16%, 0.5% +0.65%. A 2% trail took TWST at +1.95% where holding made +11.65%. CORRECTED 2026-10-07 by Codex action-log review: the original text claimed arm/trail PAIRS and 'tighter monotonically worse'; the sweep is one-dimensional so no pair can be read off it, and tightening is not monotonic below 2% (1.5% at +0.67% is worse than both 1% at +1.03% and 0.75% at +1.16%). Only the stated evidence changed -- 'every width was worse than holding' re-derives exactly, and the no-trail decision stands. The announcement window is a TIME rule, not a day-lag rule, and is journaled per fill (announcedAt / announcementWindow / minutesSinceAnnouncement): a filing accepted at 10:00 ET yesterday had a full session to be priced and does NOT qualify, while one accepted at 17:24 ET yesterday does -- both are 'lag 1'. Measured 2026-10-06 that separates IMNM (8-K at 12:30 ET prior session, excluded) from PCVX (424B5 at 21:24 ET prior session, included). Read from EDGAR acceptanceDateTime, never filingDate, which rolls to the next business day after ~17:30 ET. The existing 180-day lookback made 7 of 10 'verified' episodes 32-169 days stale, and on that sample fresh and stale were indistinguishable (+4.25% vs +4.28%, n=3 vs 7). HONEST LIMITS: all 10 episodes are from ONE session (2026-10-06, a broad growth/biotech down day), so they are correlated rather than independent; there is no holdout; p=0.0461 is marginal, is permuted against the 2% trail rather than against no-edge, and is the best of nine swept widths with no multiple-comparison correction. Starts derived TESTING, judged by R18 at 15 episodes. If the split stays flat across the R18 sample, the announcement gate should be struck and this is max-loser-breakdown with a better exit.
human directive 2026-10-06; evidence: [internal note], state/dilution_fade_episodes.json, src/data/dilution_fade_report.json
ADDED 2026-10-30. stop_guard.short_gap_signal() gated this tag on a clock check alone (9:30-9:45 ET) while every other news-gap short kind built on the SAME shortBucket() candidate universe already required the fade to be confirmed first: short-gap-5m-low (vs < 0 and trend <= 0), short-gap-failed-10m (last < regularOpen and trend <= 0), short-gap-vwap-15m (last < regularOpen and vs < 0 and trend <= 0), plus short-dilution-fade (R59) and wsb-popularity-open-green. R60 adds the missing leg: vsVwapPct must be < 0 at entry. The primary justification is internal consistency, not the statistics -- stop_guard already COVERS an open short of this kind the moment it breaks back above VWAP (the 2026-10-29 US.CMG exit is journaled 'COVER SHORT: CMG broke above VWAP (+0.95% vsVwap) ... Invalid SHORT setup'), so the system was exiting at a condition it was willing to enter at. Measured over the tag's complete 68-episode history, split on vsVwapPct parsed from each entry's own journaled thesis: above VWAP 27 episodes, 8W/19L, -$923.88, mean -0.2194 R; below VWAP 41 episodes, 26W/15L, +$483.75, mean +0.2531 R. Both cohorts clear R18's 15. The split variable is produced by the market, not set by the strategy, and the comparison is within one tag, so the 2026-10-01 composition confound does not apply. Every above-VWAP entry also had a positive 30-minute trend, so the two are inseparable in this sample; the gate uses vs alone because the five below-VWAP/positive-30m episodes were +$49.00 net. Exactly two variants were examined; no threshold sweep. HONEST LIMITS: label-shuffle p=0.1096 does NOT clear 0.05 and the mean-R CI [-0.0588, +0.6034] includes zero -- the statistics are confirmatory only. The gate does not strictly delete the 27 above-VWAP episodes, because the 15-minute window leaves room for some of those names to be shorted later at a lower price once they cross below VWAP, so the magnitude is an upper bound. Episodes overlap in time and cluster by day. Forward slice since the 2026-10-01 R58 trail change: 1W/4L / -$606.26 above VWAP vs 8W/5L / +$141.29 below. The same sign split appears in org-gap-fade-short (193 above, -$908.76; 35 below, +$495.44) and wsb-popularity-green-open (13 above, -$395.53; 43 below, +$108.40); those are deliberately NOT changed here and are left to the Saturday rollup's structural pass. SCOPE: strictly short-gap-open-close. The dilution-open-trail* kinds that shared the old code branch are explicitly excluded and keep the window-only rule, because dilution-open-trail1 runs 12W/3L for +$648.98 with price ABOVE VWAP -- the opposite sign. Not a safety-core rule: no new mechanism, no loosened stop, no widened cap, and it can only reduce entries.
auto-applied by overnight review, 2026-10-30 (target date 2026-10-29); evidence: 68-episode closedEpisodes split re-derived from journaled theses, order-path log for US.CMG/US.FTAI/US.PRCH/US.HAYW; regression tools/tests/test_short_gap_open_close_vwap_gate.py; writeup [internal note]
LIVE 2026-11-02, human directive (the operator). Every closed episode carries an execution-integrity verdict - clean or contaminated - decided by tools/execution_integrity.py from LOG EVIDENCE ALONE and written to the sidecar src/data/execution_integrity.json, keyed by episodeId. Contaminated episodes are excluded from strategy judgment at strategy_lifecycle._tagged_episodes(), the single pooling point for cycle verdicts, lifetime P&L, R34 batches and normalized episode P&L. Raw all-trades totals are ALWAYS retained and reported beside the judged ones, so the published number still reconciles with the broker. WHY: when a power cut, a reboot, a dead stop-guard, a broker outage or a failed EOD flatten stops the close-out from running, the resulting P&L measures the outage, not the edge - the 2026-08-04 14:51 CT power loss and the 2026-10-28 missed flatten are the reference cases. THE RAILS, which are the rule and not decoration: (1) classification never sees P&L - classify_day()/classify_episode() take no pnl or outcome argument and the tests assert the signatures, because an outcome-dependent integrity flag is a laundry; (2) MISSING EVIDENCE IS NEVER AN EXCUSE - exclusion requires POSITIVE evidence of an execution-affecting fault, and a day whose logs are gone still COUNTS, stamped evidenceCoverage: partial; (3) every reason carries a re-checkable evidence pointer (source, timestamp, locator) or it is discarded; (4) absence is only evidence when corroborated - 'no flatten ran' needs an independent carry signal, since most days need no flatten; (5) only faults that could have changed an entry or exit exclude anything, and only for episodes OPEN while the fault was in force - routine degradation (a failed EOD review, a retried broker call, a brief cycle stall) is recorded as `degraded` and excluded from nothing; a rule-following failure on the strategy path (no journaled stop) is attributed to the STRATEGY and still counts. A STOP THAT FIRED CORRECTLY AND LOST MONEY IS CLEAN. SANITY CHECK, standing: at adoption the contaminated set was net +$1,629.18 across 439 episodes, so the flag moved judged P&L DOWN (+$1,340.20 all-trades -> -$288.98 judged). A future detector change that IMPROVES the headline P&L is suspect by that fact alone and must be justified against evidence before it is accepted. The detector runs deterministically in run_trade_session.cmd's eod-closeout step before eod_review.py, so an outage is flagged the same day, and again on overnight-review with --refresh-host-events. It fails OPEN: a missing sidecar means no exclusion rather than a silently shrunken sample.
human directive (the operator), 2026-11-02; full backfill and decision audit in [internal note]; regression tools/tests/test_execution_integrity.py
REMOVED 2026-07-23 by human directive. The former percentage-of-equity dollar-risk formula no longer sizes positions. R32's later flat-$100 cap was also removed 2026-08-07, so stop distance no longer changes quantity. R21/R28 notional limits, R38 heat, R40 daily deployment, and structural stops remain live. Kept listed so neither retired formula is reintroduced blind.
framework design 2026-07-07; raised 2%→3% 2026-07-13; removed 2026-07-23; R32 distinction synchronized 2026-07-29
REMOVED 2026-07-23 by human directive. There is no system-wide opening delay; each strategy's own trigger/confirmation requirements still apply. R31 (added 2026-07-24) separately gates the first 15 minutes for its own strategy roster (score90, score80, gap-go, rumor-momo, inst-continuation). Kept listed per R18 so a blanket opening-delay is never re-implemented blind.
prior 2026-07-07; removed 2026-07-23
STRUCK ENTIRELY 2026-07-13 (human directive: "allow stocks to be bought and sold many times throughout the day, do not restrict the number of times you can buy and sell a stock"). Originally: one re-entry per name per day; amended 2026-07-09 to two stop-outs per name per day. No per-symbol daily limit on entries or stop-outs exists anymore. The remaining re-entry gates are: the signal must re-qualify fresh, a pending-order skip in the entry-guard, and R2/R21 sizing. Kept listed per R18 so it is never re-implemented blind.
prior 2026-07-07; struck 2026-07-13
STRUCK 2026-07-13 (human directive: "no restrictions on number of trades per strategy a day"). Originally allowed up to 5 trades per strategy per day. No daily trade cap exists anymore, per strategy or otherwise, and there is no concurrent-position cap. Hard limits that still bind: R21/R28 per-SYMBOL combined notional (10% of equity; 7.5% for an initial entry), R38 aggregate dollar-risk heat (2.5% of equity, measured as the sum of qty x distance-to-its-own-stop and NOT as notional), R40 daily deployment (toggleable, currently OFF), and flat by the close. R32's flat-$100 max-loss cap was REMOVED 2026-08-07 and no longer binds. The surviving ACTIVITY EXPECTATION is operative: a strategy is expected to trade most days, and a session whose list is populated but takes nothing must name WHICH criterion blocked each top candidate. Stand-down substantiation (added 2026-10-30): a stand-down citing exposure or a risk cap must name the rule, state its basis (per-symbol notional vs aggregate dollar risk vs daily deployment), and print the computed value against the ceiling -- a bare percentage with no rule attached does not satisfy that requirement. Kept listed per R18 with a revert path if unlimited volume produces churn losses.
human directive 2026-07-10; struck 2026-07-13; stale R32 reference removed and R38/R40 + stand-down substantiation added by critical note-review 2026-10-30
REMOVED 2026-07-30 by human directive. The target-cap/no-entry-into-wall/pin-caution/wall-break-trail gates that once ran in stop_guard.py's guarded_entry() (r23_entry_plan()) are gone; entries no longer adjust for options walls at all. The underlying wall/pin summary (scripts/options_walls.mjs, stamped onto watchlist rows by build_watchlist.mjs) is kept and still displayed on the dashboard for a session's own judgment -- only the automated enforcement was removed. R31's time/PM gate reused this rule's strategy roster (score90, score80, gap-go, rumor-momo, inst-continuation) for an unrelated reason; that roster now lives independently as stop_guard.R31_STRATEGIES. Kept listed per R18 so a target-capping rule is never re-implemented blind without re-establishing the evidence this one had.
human-approved 2026-07-11; TESTING, first scored 2026-07-17 rollup; removed 2026-07-30
A capable-model session wrote this independently the same day as R24, before knowing the R24 fix already existed — same VG 2026-07-13 incident, same root cause, same prescribed fix. No longer deferred: R24 documents the applied, tested, live fix. See R24 for the current, authoritative rule and fix details. Kept listed per R18 as a genuine independent discovery, not a mistaken rule.
same incident as R24, 2026-07-13
RETIRED by human directive ('allow the app to do whatever sizing it thinks makes sense'). R32 was the binding cap on the large majority of setups, so leaving it in would have made the new 10% per-name ceiling unreachable and the directive a no-op. WHAT THIS GIVES UP: a single trade's loss is no longer bounded by a flat dollar figure, and position size no longer varies with stop distance AT ALL -- R32 was the only cap that read the stop, so a 3%-wide stop now produces the same quantity as a 1%-wide one and therefore three times the loss. What still bounds a position: R21 (10% per name) and, across positions, R38's aggregate heat cap -- which is exactly why R38 was KEPT when R32 was dropped. sizing.risk_capped_qty() survives only so blocked_trade_analysis.py can recompute historical refusals from the permanent action log. Removal decision, evidence and downside analysis: [internal note]
human directive, 2026-07-28 ("add a rule that the max loss of $100 allowed"); opening-bell propagation auto-applied 2026-07-30. RETIRED 2026-08-07 by human directive; removal write-up cited in the rule text above.
RETIRED by human directive ('remove R36 and all of kelly'). Replaced by R44. The failure was structural, not mathematical: a fraction derived from a strategy's own history is a confirmed ZERO for exactly the strategies with a losing record, which sized them to zero shares, which made their R18 retest budget unspendable, which required close_deadlocked_retests() to exist purely to break the resulting deadlock, which in turn produced permanent retirements on budgets that could never be spent (kronos-momentum, rsi-dip, 2026-08-06/07). Every Kelly function, the HALF_KELLY_* gate set, HALF_KELLY_ENABLED, and close_deadlocked_retests() were deleted together. Kept as a tombstone per R18: never re-implement blind. Removal write-up: [internal note]; same-day sizing-directive context in [internal note]
human directive, 2026-08-01; rollout documentation completed by recurring Codex action-log repair session, 2026-08-01; original Kelly criterion: https://doi.org/10.1002/j.1538-7305.1956.tb03809.x; fractional/drawdown context: https://stanford.edu/~boyd/papers/kelly.html. RETIRED 2026-08-07 by human directive; removal write-up cited in the rule text above.
The roster moved to a page of its own on 2026-11-04 — it outgrew a heading here. See all 58 strategies › — status, lifecycle, trades, win rate and paper P&L, each row opening that strategy's own page.
Trade with Moomoo (referral link)