Changelog

Change log for public semantics, observability contract, and engine history.

Website canon-line updates

  • Route/decision-centric dashboard semantics as primary model.
  • SPEC page added as canonical technical specification.
  • FAQ rewritten into economic/legal/technical knowledge format.
  • Knowledge pages + 'what is kapitaalbot' aligned to shared terminology.
  • Public safety boundary made explicit: functionally complete, non-reproducible.

Engine commit history

Source history from the engine repository. This list is informational and points to commit intent, not source code explanation on this page.

Generated: Wednesday, 6 May 2026 at 18:34:30
1379 commits
/srv/krakenbot

  1. 11c4352
    feat(observability): public_status_snapshot.risk_block_active (contract 1.4)
    Show commit body
    Adds a runtime-truth boolean so the website "Stop / no new risk" tile
    can fire only on an actually active block, not on a 24h funnel residue.
    
    - snapshots.rs: bump CONTRACT_VERSION to "1.4"; new fields
      risk_block_active, risk_block_active_window_secs,
      risk_block_active_reasons[]; new DTO RiskBlockActiveReason. Doc on
      PublicStatusSnapshot warns that entry_blocked_recent (24h, historical)
      must not drive a "blocked now" UI tile.
    - observability_queries.rs: active_global_entry_block_reasons(pool,
      window_secs) aggregates trading_funnel_events on the decision pool over
      GLOBAL_BLOCKING_ENTRY_EVENT_NAMES = {GLOBAL_EXPOSURE, DEPLOYMENT_CAP,
      ELIGIBILITY_STATE_STALE} within the window. SPOT_LONG_ONLY is excluded
      (structural policy filter, not a block-state).
    - export.rs: env RISK_BLOCK_ACTIVE_WINDOW_SECS (default 120s) drives the
      query; new fields populated on the public_status_snapshot.
    
    Trading impact: none (export-only). Ingest impact: none. DB migrations:
    none. Website (Davelaar/KapitaalBot-Website) must remap the Stop tile to
    risk_block_active (boolean) instead of entry_blocked_recent.length > 0.
  2. 571d81c
    feat(position_monitor): broaden Fix 1 MFE-lock to static stop-loss protection
    Show commit body
    The first live MFE case (BILL/USD at 14:48) revealed that Fix 1 v1 missed
    profit-lock opportunities purely because the protection order was a
    `stop-loss` (static) rather than `trailing-stop`. The +100 bps MFE shadow
    fired but no amend ran. Post-deploy mix is ~60 % trailing-stop / 40 %
    static stop-loss, so ~40 % of qualifying positions were silently outside
    Fix 1's scope.
    
    Broadens the MFE-driven breakeven-lock to act on **both** protection
    order types using the same trigger criterion (`pullback` cohort, MFE >=
    TRIGGER_BPS in side direction):
    
    - `trailing-stop` (unchanged): tighten distance via
      `amend_trailing_stop_trigger_pct(new_trail_bps = pnl - offset)` only
      when `new_trail < active_trail` (capital protection invariant).
    - `stop-loss` (new):  move the static trigger to
      `entry × (1 ± offset_bps/10000)` via `amend_stop_loss_trigger`, only
      when this moves the trigger in the profit-protective direction
      (long: above current SL; short: below current SL with > 0 guard).
    
    Single shared funnel event `MFE_BREAKEVEN_LOCK_FIRED` with a
    `protection_type` field in detail JSON to distinguish the two paths.
    
    Capital invariant preserved: amends never widen, only tighten or shift
    into the profit-protective direction.
    
    Cohort scope is unchanged (pullback only); liquidity tier still v2.
  3. 37ff414
    fix(pipeline): persist EXPANSION_GATE_LIVE_* via insert_event (not per-iteration buffer)
    Show commit body
    The expansion-gate funnel events were pushed to `funnel_pending`, a
    per-iteration `Vec` that is dropped at the end of each loop body without
    flushing — only `SIGNAL_ADMISSION_DECISION` (line 3943) and the
    no-price-snapshot continue (line 4094) trigger flushes, both of which
    fire *before* the live-gate block runs. The result was zero
    `EXPANSION_GATE_LIVE_SKIPPED` rows in the funnel table even though
    `V2_SHADOW_BLOCKED ... blocker=expansion_gate_skip` log lines proved
    the skip code path was being hit.
    
    Switch the gate's emission to `insert_event` (single-row write per fire,
    same path used for `MFE_BREAKEVEN_LOCK_FIRED`). Rate is low (a few per
    minute), so per-row insert is cheap and avoids the buffer-loss bug.
  4. ab247e3
    feat(position_monitor): MFE-driven profit-lock + breakeven-shift v1 live
    Show commit body
    Smallest live action against the giveback leak surfaced by the 4d
    shadow + live profit analysis. When a trailing-stop position with
    `thesis_family='pullback'` reaches MFE_BREAKEVEN_LOCK_TRIGGER_BPS
    (default 50) of unrealised side-direction gain, tighten the trailing
    distance so the resulting trigger sits at entry + MFE_BREAKEVEN_LOCK_OFFSET_BPS
    (default +5 bps), locking a small profit while leaving headroom for
    further upside.
    
    Mechanism:
    - new trail_bps = (pnl_bps - offset_bps).max(MFE_LOCK_MIN_TRAIL_BPS=10)
    - amend only when new_trail < pos.active_trail_bps (never widen)
    - one-shot per position via `mfe_breakeven_lock_applied` flag
    - piggy-backs on existing `position_monitor` 5s loop and
      `amend_trailing_stop_trigger_pct` WS path (no new infra)
    
    Cohort scope (v1):
    - thesis_family='pullback' only (gated)
    - liquidity_exec_tier deferred to v2 (concession; admission already
      blocks Ineligible, LowLiquidity pullback fills are <5% of pullbacks
      in 4d data, and locking entry+5 on any pullback is unambiguously
      better than passive 75 bps median giveback)
    
    New env-flags (defaults shipped):
    - MFE_BREAKEVEN_LOCK_ENABLE=true
    - MFE_BREAKEVEN_LOCK_TRIGGER_BPS=50
    - MFE_BREAKEVEN_LOCK_OFFSET_BPS=5
    
    Telemetry: emits `MFE_BREAKEVEN_LOCK_FIRED` funnel event with full
    detail JSON (trigger, offset, pnl_at_lock, old/new trail, amend_ok,
    thesis_family, hold_secs).
  5. cfb3872
    feat(pipeline): live expansion gate for pullback non-expansion + 5% probe
    Show commit body
    Promote the previously shadow-only `is_expansion` gate to a real admission
    hard-skip. When `route_family=pullback`, `is_expansion=false`, and
    `expected_net_edge_bps < EXPANSION_GATE_LIVE_MIN_EDGE_BPS` (default 60),
    the pipeline now sets `ExecutionDecision::Skip("expansion_gate_skip")`
    and emits `EXPANSION_GATE_LIVE_SKIPPED`, except for ~5% probe traffic
    (1-in-20 counter, env `EXPANSION_GATE_PROBE_ONE_IN_N`) which passes
    through and emits `EXPANSION_GATE_LIVE_PROBE_PASS` so we keep collecting
    realised outcomes on the would-skipped cohort.
    
    Default behaviour: enabled. To revert to shadow-only:
      EXPANSION_GATE_LIVE_ENABLE=false
    
    Empirical justification (4d):
    - shadow `is_expansion=true` → 72.8% TP-rate (n=42K)
    - shadow `is_expansion=false` → 48.7% TP-rate (n=1.39M)
    - live small-edge pullback (<200 expected_move_bps) → -38.8 bps avg net
      on 57 trades, ~$3.91 of $-8.4 total 4d realized PnL.
    
    When the live gate fires, the earlier `EXPANSION_GATE_WOULD_SKIP` shadow
    event is suppressed for the same trace to avoid double-counting; the
    LIVE_SKIPPED / LIVE_PROBE_PASS events carry the same detail JSON.
  6. fe6660d
    feat(observability): EXPANSION_GATE_WOULD_SKIP shadow on pullback non-expansion
    Show commit body
    Emit `EXPANSION_GATE_WOULD_SKIP` funnel events when a route would have been
    admitted under the current pipeline but `is_expansion=false`,
    `route_family=pullback`, and `expected_net_edge_bps<60`. Pure observability:
    admission is not changed.
    
    Goal: size the would-skipped pullback non-expansion / weak-edge cohort
    before any live admission cut. Shadow data shows expansion=true has
    +24-pp TP-rate gap (72.8% vs 48.7%) and live small-edge pullback bleeds
    ~$3.91 over 57 trades / 4 days.
    
    Detail JSON captures expansion_score, expected_net_edge_bps,
    expected_move_bps, horizon and route_type for post-hoc cohort analysis
    against realized PnL.
  7. 64cac3b
    feat(execution): broaden maker→taker fallback eligibility (shadow-gated)
    Show commit body
    Extend `PostExpiryPolicy::ImmediateTakerReeval` from `ignition && breakout`
    to all maker entries on `pullback`/`breakout` routes in MID/HIGH liquidity
    above $5 dust floor. The actual taker submit is gated by a new env-flag
    `TAKER_REEVAL_SHADOW` (default `true`): when shadow=true, gates are
    evaluated and a `TAKER_REEVAL_SHADOW_WOULD_FIRE` funnel event is emitted
    with full decay/spread/net-edge snapshot, but no taker order is sent.
    
    New env knobs:
    - `TAKER_REEVAL_SHADOW` (default true)
    - `TAKER_REEVAL_MIN_NET_EDGE_BPS` (default 5)
    
    Goal: measure viable_pct against the unfilled-maker cohort (~500/7d, 71%
    miss rate, p90 mid-drift +197 bps during 75s) before any live taker
    activations. Existing `attempt_taker_reeval` remains intact.
  8. 20d1c51
    feat(observability): MFE-lock shadow events on post-fill drift
    Show commit body
    Emit `MFE_LOCK_WOULD_FIRE` funnel events from the post-fill drift sampler
    when realised drift in the side-direction crosses 50/100 bps thresholds
    on a fresh price snapshot. Pure observability: no protection-flow effect.
    
    Goal: shadow-measure how often a profit-lock / breakeven-shift / partial
    market exit at MFE thresholds would have fired on filled trades, before
    wiring the same signals into protection_flow live.
    
    Threshold ladder is monotonic per fill (only one event per crossed
    threshold across the 15/30/60s horizons) and writes detail JSON with
    horizon, observed mfe_bps, threshold, suggested_action.
  9. 5541f5f
    Add minimal execution_precheck lineage rows for active no-CDV paths.
    Show commit body
    Persist observability-only CDV lineage on emergency_stop and external_manual_backfill traces so active NULL/no-CDV blind spots no longer grow from these execution paths.
  10. b308c0b
    fix(db): append cdv_lineage_stage at end of execution_round_trips view (PG OR REPLACE)
  11. c2f661e
    fix(db): execution_round_trips CDV join prefers admission, falls back to execution_precheck
  12. 3106576
    docs(scripts): run heavy DECISION retrospective on server filesystem, not over SSH stdin
  13. 7493820
    Add DECISION trade retrospective SQL + runner (execution_round_trips enrichments)
  14. cf3410d
    Promote NoTrade to Selected when executable BigMove store matches candidate
    Show commit body
    - Add RouteStateStore::executable_atr_breakout_matches_route_candidate (shared with v2_resolved_route_family_label).
    - After adaptive route eval, optionally promote symbols with NoValidRoute if a valid candidate matches an executable AtrBreakout row; env BIGMOVE_EXECUTABLE_STORE_ROUTE_PROMOTION_ENABLE (default on).
    - Wire optional route_state_store into run_adaptive_route_analysis; live + exec-only runners pass the store; CLI callers pass None.
    - Extract spawn_route_state_store_maybe for live bootstrap and exec-only loop; fill route monitor slot in exec-only via Arc clone; pass store into run_strategy_pipeline_v2 for exec-only.
    - Unit test for store key/candidate matching.
  15. e27d40e
    fix(route_state): BigMove AtrBreakout Executable via bigmove_score floor
    Show commit body
    Bridge marked all BM rows Blocked when expected_net_edge_bps<=0 while
    RESEARCH publishes high bigmove_score; v2_resolved_route_family_label
    skips Blocked entirely.
    
    - EdgeboardSignal: optional bigmove_score from RESEARCH fetch
    - BIGMOVE_EXECUTABLE_SCORE_FLOOR (default 80) gates Executable only for
      AtrBreakout; other families unchanged (net-edge rule)
    - dominant_reason labels: edgeboard_bigmove_score_floor /
      edgeboard_bigmove_score_below_floor
    - Tests for floor pass/fail; pipeline label tests unchanged
  16. 39042f1
    fix(route_state): BigMove bridge stores maker RouteStateKey alongside taker
    Show commit body
    RCA: v2_resolved_route_family_label matches AtrBreakout store rows on
    route_type, horizon, and entry_mode; live routes were Maker while the
    edgeboard handoff only keyed Taker rows.
    
    - Add signal_to_route_state_with_entry_mode; default Taker unchanged
    - ingest_edgeboard_batch: duplicate upsert with EntryMode::Maker when
      BIGMOVE_EXECUTION_ENABLE and big_move_capture only
    - Tests: H5m RouteCandidate matches 300s horizon; maker label + key test
      + edgeboard duplicate-key unit test
  17. 4f708f4
    fix(edgeboard_bridge): align BigMove horizon_sec to RouteHorizon enums
    Show commit body
    RouteState keys used a two-bucket horizon map (≤300→H3m else H10m), so
    RESEARCH rows with horizon_sec 60/300/900 did not match V2 breakout horizons
    (H1m/H5m/H15m) and pipeline could never attach route_family=big_move_capture.
    
    Nearest max_hold_secs mapping fixes store↔candidate identity for Bridge→pipeline.
    
    Tests cover 900→H15m, 300→H5m, 60→H1m.
  18. b775a99
    feat(v2 pipeline): emit big_move_capture route_family from RouteStateStore
    Show commit body
    - Resolve funnel/CDV/route_family labels to big_move_capture when an executable
      AtrBreakout bridge row matches the selected route (type/horizon/entry_mode).
    - Thread optional RouteStateStore into run_strategy_pipeline_v2*; live runner passes
      the store; run-execution-only, recycle flow poller and run-execution-once use None.
    - Fix Outcomes that previously ignored the resolved funnel route_family string.
    - Add DECISION-SQL counterfactual script for high-vol path_tape cohort vs +200 bps exit
      proxy (post-fill drift limitations documented in-file).
    - Tests: executable AtrBreakout store row yields big_move_capture label.
  19. aedcaff
    Route-state: ingest BigMove rows from visible RESEARCH snapshot
    Show commit body
    - Map big_move_capture to StrategyFamily::AtrBreakout so rows do not collide
      with blended RAM-cache momentum placeholders.
    - Add fetch_big_move_capture_signals_for_visible_snapshot: bounded JOIN on
      MAX(snapshot_ts) only (no CDV scans).
    - live_runner periodic sync merges synthetic momentum + fetched BigMove;
      BIGMOVE_EXECUTION_ENABLE still gates store upserts.
    - Docs: telemetry on fetch failure momentum-only fallback.
  20. 83e4363
    Add reproducible cohort PnL drilldown SQL for decision pool
  21. 3cf4bbf
    fix(order_reconcile): decode nullable exchange_order_id in cancel_orphaned_exit_orders
    Show commit body
    The query in cancel_orphaned_exit_orders selects child.exchange_order_id from
    execution_orders without a NOT NULL filter. Rows that never received an
    exchange ack (status reaches reconcile_required without an exchange ID)
    have NULL there, which crashed sqlx decoding the tuple's column 2 as `String`:
    
      "error occurred while decoding column 2: unexpected null;
       try decoding as an `Option`"
    
    This caused the recurring `startup_orphaned_exit_order_cancel_failed` and
    `epoch_cycle_orphaned_exit_order_cancel_failed` warnings: the entire orphan
    sweep aborted on the first NULL, so no parent-terminal orphaned exits were
    cleaned up at all. The function already had a code path for "no
    exchange_order_id" (mark canceled in DB, no WS cancel) but never reached it.
    
    Decode as `Option<String>` and treat None identically to an empty string —
    the existing `is_empty()` branch handles it. No behaviour change for rows
    with non-null exchange_order_id.
    
    Tests: cargo check clean, 79 execution::* tests pass.
  22. 492e5fa
    fix(position_monitor): orphan recovery trail is fee-aware (v1 doctrine)
    Show commit body
    Replace hardcoded ORPHAN_RECOVERY_TRAIL_BPS=40 with v1 fee-floor-aware trail
    computed via estimate_trail_bps_v1, mirroring protection_flow::protect_exposure
    exactly: trail = clamp(max(alpha*ATR15m, 2*taker+10, M_MIN=20), M_MIN, M_MAX=180),
    with ORPHAN_RECOVERY_MIN_TRAIL_BPS=40 fed as the explicit strategy floor so the
    historical "at least 40 bps" intent is preserved.
    
    Behaviour change (deliberate, per investigation A): orphan-recovery TSL no
    longer trails inside fee-floor noise. With live taker=35 the new minimum is
    80 bps (2*35+10), up from the prior fixed 40 bps. Wider trails are *less*
    aggressive (later trigger, more drawdown tolerance) — never tighter — so this
    cannot increase position risk; it eliminates micro-loss exits where the trail
    trigger sat below break-even after fees.
    
    Logging now exposes trail_bps, taker_fee_bps, fee_floor_bps, min_trail_bps,
    atr_from_db so the operator sees the full v1 derivation in
    ORPHAN_RECOVERY_TSL_SENT.
    
    Threading: attempt_orphan_recovery + refresh_monitored_positions now accept
    &LiveFeeProvider (already available via run_monitor_loop's fee_provider param
    since the previous commit).
    
    Tests: cargo check clean, 79 execution::* tests pass.
  23. c5a0090
    fix(execution): thread fee_provider into position_monitor breakeven calc
    Show commit body
    Move LiveFeeProvider construction in run_execution_live up into
    prepare_live_startup_context (right after api_key/api_secret are loaded) so it
    can be passed to spawn_position_monitor. The same instance is then surfaced
    through LiveStartupContext into run_execution_live where it feeds:
    - run_live_startup_protection_and_cleanup (protect_exposure fee_floor)
    - bootstrap_live_preloop_runtime (runtime fee-refresh loop)
    
    Same change in run_execution_only_loop: fee_provider is now constructed before
    spawn_position_monitor (and before the startup-protection block), removing the
    remaining FEE_MODEL_FALLBACK_USED reason=no_provider warning emitted at
    position_monitor loop init.
    
    position_monitor::spawn_position_monitor + run_monitor_loop now accept the
    provider; tier_from_provider_or_bootstrap(Some(&fee_provider)) replaces the
    prior bootstrap call. Roundtrip-fee breakeven now uses live taker (35bps -> 70bps
    roundtrip) instead of bootstrap (40bps -> 80bps roundtrip).
    
    Plumbing only: no formula change, no order-side effect change. Resolves
    investigation B call-site B6.
    
    Tests: cargo check clean, 79 execution::* tests pass.
  24. f4bdb9a
    fix(execution): thread fee_provider through startup-protection + emergency-retry-loop
    Show commit body
    Move LiveFeeProvider construction in run_execution_live (and the analogous
    block in run_execution_only_loop) to BEFORE the startup-protection block, so
    protect_exposure() at startup uses the live fee tier (taker=35bps -> floor=80bps)
    instead of bootstrap fallback (taker=40bps -> floor=90bps).
    
    Same provider instance is now passed to:
    - run_live_startup_protection_and_cleanup (new fee_provider param)
    - spawn_emergency_protection_retry_loop (new fee_provider param, all 4 spawn
      call-sites updated: live startup, live runtime mid-loop, exec-only startup,
      exec-only runtime mid-loop)
    - bootstrap_live_preloop_runtime (now accepts fee_provider as param instead
      of constructing its own; same provider feeds the periodic refresh loop)
    
    Plumbing only: no formula change, no order-side effect change. Eliminates the
    spurious FEE_MODEL_FALLBACK_USED reason=no_provider warnings observed at every
    service start (per investigation B call-sites B1, B2, B3).
    
    Tests: cargo check clean, 79 execution::* tests pass.
  25. 8569df9
    fix(execution): thread fee_provider into run-execution-once protection paths
    Show commit body
    Pass `Option<&LiveFeeProvider>` to `emergency_protect_with_retry` and
    `ensure_protection_after_exit_fill` so the v1 fee_floor in `protect_exposure`
    is computed against the live tier (taker=35bps -> floor=80bps) instead of the
    bootstrap fallback (taker=40bps -> floor=90bps).
    
    Plumbing only: no formula change, no order-side effect change. Eliminates
    spurious `FEE_MODEL_FALLBACK_USED reason=no_provider` events from the
    run-execution-once CLI / validation paths where a live provider is already
    constructed at the top of the function (runner.rs:124).
    
    Per investigation B (call-sites B4 & B5): consistency-defect fix without
    safety impact; trail distance becomes 10 bps tighter to live floor.
    
    Tests: cargo check clean, 79 execution::* tests pass.
  26. 1e5d51c
    log(protection_flow): TSL-semantic EXPOSURE_PROTECTION_STARTED; drop misleading static stop_price
    Show commit body
    For trailing-stop orders, Kraken receives a positive distance via
    `triggers.price` with `price_type=pct` (e.g. `0.80` = 0.80% reversion
    from peak); there is no static trigger price on the wire. Yet the
    pre-call log fed `EXPOSURE_PROTECTION_STARTED` with `stop_price`
    computed by `compute_stop_price` (using `EMERGENCY_STOP_PCT=2%`).
    That value was never sent to Kraken and conflicted with the actual
    `trail_bps` (80 bps), making operator dashboards/diagnostics show a
    fictitious 2% static stop while Kraken honored a 0.80% dynamic trail.
    
    Log-only / observability fix:
      • `EXPOSURE_PROTECTION_STARTED` now emits `order_type=trailing-stop`,
        `trail_bps`, `current_price`, `initial_trigger_price` (= placement
        reference × (1 ± trail), the *starting* trigger only) plus
        `trigger_semantics=dynamic_trailing_peak_based` and an explicit
        `note` warning that the trigger follows the peak.
      • `compute_stop_price` is retained for `side` / `current_price`
        extraction (its static-stop output is bound to `_` and ignored).
      • `normalize_stop_price` was reachable only from the dropped log
        path; removed as dead code.
      • `prefer_trailing` / `trail_bps` computation is hoisted above the
        started-log so the log shows the real wire parameters. Pure
        compute (DB read + cached fee tier); no wire side-effects.
    
    No order, execution, risk, fee, OTO, or guard behavior changed:
    the WS JSON payload (`{"order_type":"trailing-stop","triggers":{
    "reference":"last","price":<pct>,"price_type":"pct"}}`) is bit-for-bit
    identical — same `trail_bps` value, same `submit_trailing_stop_and_wait_ack`
    call site.
    
    Tested: full `cargo test` 479/479 pass; cargo check clean.
  27. 2336f1c
    fix(exposure): always attempt protection on real exchange holdings; residual band → observability
    Show commit body
    Real exchange balances above qty_min — even at $1–$20 notional — are genuine
    spot positions whose downside risk belongs to the operator, not to the
    "residual small exposure" policy band that previously suppressed protection
    placement entirely. Twelve real holdings (~$36 cumulative on production:
    L3, XPL, GHST, IP, WAXL, CLANKER, LOBO, PEAQ, MIRA, SCOR, IAG, GLMR) sat
    unprotected on Kraken because all three protection-scheduling sites
    (`reconcile_exposure_at_startup`, `run_periodic_reconcile`,
    `run_periodic_position_monitor`) hard-skipped sub-band positions.
    
    Convert the residual policy band into observability-only by default:
      • New env-var `RESIDUAL_SMALL_EXPOSURE_SKIP_PROTECTION` (default `false`)
        preserves operator opt-in for the historical noise-suppression
        behavior. Default `false` = real holdings get a protection attempt.
      • `kraken_adapter::guard_prewire_notional` inside `attempt_orphan_recovery`
        and `protection_flow` continues to gate Kraken cost_min/qty_min, so
        physically-unplaceable orders still never hit the wire.
      • Existing 3-attempt cooldown / EXHAUSTED gate caps retry cost.
    
    No safety guards weakened: dust (sub-qty_min exchange truth) skip is
    unchanged; `no_reference_price`/`no_instrument_data` defer is unchanged;
    prewire notional guard is unchanged; max-attempt cooldown is unchanged.
    
    Tested: 4 new unit tests for the env-var parser; full `cargo test` suite
    passes 479/479. Pure helper `parse_residual_small_exposure_skip_protection_env`
    extracted to make OnceLock-backed config testable.
  28. 3f41692
    position_truth: force-flat ghost long + spot-impossible short
    Show commit body
    The reconcile_open_positions_truth_on_load path early-returned in two
    cases that produced false-open positions blocking exposure accounting
    and trapping stale orders:
    
    1. db_qty <= 0 -> return Ok(()).  A negative base_position on a spot
       account is a fills-ledger drift (more sells aggregated than we ever
       owned).  It can never represent a real spot-short position, but the
       row stays open and feeds downstream logic as if there were exposure.
    
    2. resolved_balance_for_base_asset returns None -> return Ok(()).
       Kraken's WS balances channel omits assets that are at zero, so for
       assets we sold to flat the cache reports None even on a fresh
       snapshot.  The DB long row never gets reconciled to zero, surfaces
       as an "open" ghost, and prevents try_reconcile_orphaned_open_orders
       from clearing the symbol's stale reconcile/suspect phantom orders.
    
    Observed in production (commit 32ddc8c): DB had ENJ/PLAY/PNUT (long
    ghost, exchange balance absent) and ZBT (negative -51.27 ledger drift,
    exchange balance zero) staying "open" indefinitely while the actual
    spot wallet shows only L3 + USD.
    
    Fix introduces a pure decision helper (decide_balance_sync_action) that
    encodes two invariants:
    - Spot-impossibility short -> ForceFlatShortSpot regardless of balance
      resolution; respect has_recent_fills cooldown to avoid racing a
      freshly-landed sell.
    - Ghost long (target == 0, either resolved-zero or absent on a fresh
      snapshot) -> ForceFlatGhostLong via the existing
      set_long_position_qty_from_exchange_truth(0, ...) path which already
      zeroes status='closed', preserving truth/lifecycle column semantics.
    
    Distinct event names (POSITION_TRUTH_SHORT_SPOT_DESYNC_FORCE_FLAT,
    POSITION_TRUTH_GHOST_LONG_FORCE_FLAT) make the cleanup auditable.
    
    Existing guards remain: has_recent_fills, has_inflight_entry_covering_balance,
    balance_fresh, BALANCE_FRESH_MAX_SECS, RESIDUAL_SMALL_EXPOSURE policy
    band — none weakened.  Real positions (L3/USD: balance present,
    matches DB) are unaffected.
    
    Tests: 9 new unit tests cover the pure decision helper.  All execution
    module tests (79) continue to pass.
  29. 32ddc8c
    exposure_reconcile: force-flat dust positions instead of re-opening
    Show commit body
    Closed positions with a residual sub-qty_min/cost_min base (rounding
    crumb from a previous fill or a balance-sync downgrade) were being
    re-opened on every reconciler pass via reopen_if_nonzero. Re-opened
    dust then pinned the symbol into universe-truth, producing
    pinned_invalid_l3 -> exit_only on every execution attempt and silently
    blocking legitimate trades (observed: ORCA/USD with base_position=
    0.00000001 reappearing within ~2 min after manual cleanup).
    
    Now load_open_positions runs assess_position_actionability up-front:
    sub-qty_min / sub-cost_min rows (open OR closed) are force-flat via
    close_dust_position and skipped from ExposureRecord. Exchange minimums
    are exchange truth -- sub-min qty is unreachable, never tradable, and
    must not consume an exposure slot.
    
    Real open positions blocked by L3 quality (e.g. ENJ/L3/PLAY) remain
    unaffected: they exceed qty_min/cost_min and stay in the exposure set.
  30. d54dbbe
    fix: anchor expected_move_bps on realized horizon moves
    Show commit body
    The route engine's expected_path model was disconnected from realized
    market moves: for APE/USD with 280 bps actual move in the last 5 min,
    the model predicted only 6.78 bps based on micro-level trend_strength
    (0.04). This pushed expected_net_edge_bps to -27 and produced
    tradable_count=0 across the universe.
    
    Add a `realized_move_anchor_bps` helper that consults
    `horizon_movers` (which the V2 selector already uses to rank candidates)
    and applies a conservative fraction of recent realized travel as a
    floor on `expected_move_bps`:
    
    - Pullback / Breakout continuation: 25% of realized_range_bps
      (symmetric travel; we expect some continuation of an already-moving
      market)
    - Pump fade / Dump reversal: 30% of |realized_move_bps|
      (the bigger the impulse, the more snapback potential)
    
    Strict gates protect against stale or thin data:
    - realized window < 30s old
    - >= 5 samples
    - direction must be set (not Neutral)
    
    Acts purely as a floor — when the model's prediction already exceeds
    the anchor, nothing changes. Confidence/probability gates remain
    intact, so weak-conviction trades still get filtered.
  31. c0a9acb
    fix: derive TE strategy from V2 route type when activation is NoOpportunity
    Show commit body
    When the V1 activation returns NoOpportunity (e.g. CHAOS regime for
    volatile altcoins), the TE resolution was using the no_opportunity
    floor (9.9M bps) — a hard kill-switch. This blocked all V2 route-engine
    candidates even when they had strong positive economics (e.g. APE/USD
    with 243 bps move and 15.9 bps spread).
    
    Now falls back to the V2 route type for the TE floor lookup:
    - BreakoutContinuation → breakout_immediate (10 bps floor)
    - PullbackContinuation → momentum_ride (10 bps floor)
    - PumpFade/DumpReversal → mean_reversion_snapback (6 bps floor)
    - PassiveSpreadCapture → maker_passive_queue (3 bps floor)
  32. fb45c06
    fix: normalize microprice_deviation_bps in strategy pipeline regime detection
    Show commit body
    The strategy_pipeline activation path passed raw microprice_deviation_bps
    to detect_regime(), but the function expects normalized input (micro/40.0,
    capped at 2.0). This caused nearly every symbol to be classified as CHAOS
    (any micro_bps > 1.0 triggered it), resulting in NoOpportunity activation,
    a 9.9M bps floor, and complete execution blockage.
    
    All other call sites already normalize correctly. This aligns the single
    broken call site with the canonical formula: (micro.abs() / 40.0).min(2.0).
  33. 7eafd2a
    BigMoveCapture validation: add doctrine health script
    Show commit body
    scripts/bigmove_doctrine_health.sh queries research DB for:
    - Cohort row counts, TP/TSL arm split, p100/score distributions
    - Legacy route expectancy (baseline comparison)
    - Top 10 BigMove candidates from latest snapshot
    
    Use after shadow deployment (commit 2) to validate gates before live enable.
  34. 7addc2c
    BigMoveCapture doctrine commit 3: execution doctrine
    Show commit body
    Wire BigMoveCapture to PathTapeClass::BigMoveHighVol in classify_path_tape
    (exact-match on route_family="big_move_capture", before continuation/pullback
    substring checks).
    
    BigMoveHighVol exit arm in exit_config_for_path_tape:
    - Noise-aware static SL (class_k=1.20).
    - If bigmove_tp_bps from edgeboard is set: static TP at that level.
    - Otherwise: delayed trailing activation (TSL-arm, same amend pattern as
      AcceleratingContinuation).
    
    Extend delayed_trailing_enabled gate in exit_lifecycle to include
    big_move_high_vol alongside accelerating_continuation.
    
    Add bigmove_tp_bps field to Outcome (pipeline → execution passthrough).
    Pass bigmove_tp_bps_in to exit_config_for_path_tape from flow_execution.
    
    Live only after shadow gates: BIGMOVE_EXECUTION_ENABLE=false blocks BigMove
    signals at edgeboard_bridge (commit 2). Setting it to true activates the
    full execution doctrine.
  35. fc3d8e9
    BigMoveCapture doctrine commit 2: cohort ranking + scoring (shadow)
    Show commit body
    Score BigMoveCapture rows with directional event metrics (bigmove_p100,
    bigmove_cond_mean, bigmove_tp_bps, bigmove_score). Separate cohort ranking
    by bigmove_score DESC with cap N=3 and per-symbol cooldown 90s. Merge
    BigMoveCapture rows additively into the board alongside generic ranking.
    
    Kill-switch in edgeboard_bridge: BIGMOVE_EXECUTION_ENABLE=false (default)
    blocks BigMoveCapture signals from entering the route-state store. Research
    DB retains all shadow data for observability.
    
    MODEL_VERSION bumped to v9_bigmove_doctrine. New env vars:
    BIGMOVE_MAX_PER_SNAPSHOT, BIGMOVE_SYMBOL_COOLDOWN_SECS,
    BIGMOVE_TP_FLOOR_BPS, BIGMOVE_TP_CEILING_BPS, BIGMOVE_TP_PROB_TARGET,
    BIGMOVE_EXECUTION_ENABLE.
    
    INSERT SQL for edgeboard_candidates/snapshots now includes bigmove columns.
  36. 9f99f13
    BigMoveCapture doctrine commit 1: schema + types (no runtime behavior)
    Show commit body
    Add RouteName::BigMoveCapture as fourth route with vol + aligned-pressure
    predicaat. Add PathTapeClass::BigMoveHighVol variant with class_k=1.20 for
    noise-aware SL. Add ExitConfig.bigmove_tp_bps field (None default).
    Add event_capture_directional_metrics and pick_empirical_tp_bps helpers.
    Add research migration for nullable bigmove columns on edgeboard tables.
    Unit tests for route predicaat, directional metrics, and TP picker.
    
    No runtime behavior change: snapshot_run does not yet score BigMoveCapture
    rows, classify_path_tape does not yet route to BigMoveHighVol.
  37. 2562260
    feat(edgeboard): v8 MMS gapfill for per-symbol snapshot continuity
    Show commit body
    Primary MMS scan window is configurable via EDGEBOARD_MMS_LOOKBACK_HOURS
    (default 6). Symbols present in the previous edgeboard snapshot but missing
    from that window get gap-filled rows from research MMS up to
    EDGEBOARD_GAPFILL_LOOKBACK_DAYS (default 7), then any scope_mode, then ingest
    L2 — so ranking/event-capture still runs each refresh without dropping symbols.
    
    Also: shared pooled_adjust_positive for ranking + explainability_json;
    route_hist_route_aligned_n tie-break after pooled flag; MODEL_VERSION v8.
  38. e429c07
    edgeboard: add event-capture target as primary ranking key (v7)
    Show commit body
    Additive model output: every candidate now carries
    event_capture_probability_100bps, event_capture_cond_mean_abs_100bps and
    event_capture_expectancy_bps alongside the existing mean-return target.
    The new ranking primary key is event_capture_expectancy_bps (pooled-evidence
    penalized), with the legacy expected_net_edge_bps demoted to a diagnostic
    secondary tie-breaker.
    
    Why: SSOT analysis on movement_opportunity_events showed that high-vol
    buckets with >=70% probability of >=100 bps moves were ranked near zero
    because mean-return collapses by symmetry; the bot misses the executable
    big-move events it was built to catch. Confidence formula and pooled-row
    visibility are unchanged; route-aligned evidence is explicitly preferred
    via the existing pooled-bucket tie-breaker.
    
    - match_tiers.rs: event_capture_metrics helper + 4 unit tests
    - snapshot_run.rs: Candidate fields, ranking_event_capture_expectancy_bps,
      rank_candidates primary key swap, full explainability_json (p100/200/500,
      cond_mean_abs, n_hits, gross, expectancy, ranking variant)
    - constants.rs: MODEL_VERSION bump to ..._v7_event_capture_target
    - migration 20260427190000: nullable columns + indexes on
      edgeboard_candidates and edgeboard_snapshots
  39. d5cbc5a
    fix(sql): use CTE cutoff for since_ts in SSOT summary script
  40. 5b5b66f
    fix(research): collapse SSOT view COMMENT to single string literal
  41. 71dc1f1
    feat(research): movement opportunity events SSOT view + summary script
    Show commit body
    Canonical truth-layer in research DB linking realized big-move events
    (>=100 bps, horizons 1/3/5/10/15m) to the best ex-ante edgeboard row
    visible in the 15m window before event_start_ts. Used for measuring
    ex-ante detectability and ranking against actual executable moves.
    
    - migrations/research/...movement_opportunity_events.sql:
      CREATE VIEW krakenbot.movement_opportunity_events joining
      directional_forward_observations + market_microstructure_snapshots
      + edgeboard_snapshots (lateral) with executability flags and pooled
      / route-aligned provenance.
    - scripts/sql/movement_opportunity_events_summary.sql: 5-block summary
      (frequency, ex-ante breakdown, missed pre-move features, filtered
      drilldown, top 20 missed events).
    
    Horizons covered: 1m/3m/5m/10m/15m. 60m horizon not in forward
    observations and intentionally out of SSOT v1; documented.
  42. 15928e1
    fix(edgeboard): demote pooled ranking evidence
    Show commit body
    Keep pooled bucket estimates visible while ranking them by a provenance-adjusted edge when route-aligned evidence is sparse.
  43. 19ec051
    fix(edgeboard): bound latest microstructure reads
    Show commit body
    Limit latest-MMS snapshot input to recent rows and add the composite research index needed for per-symbol edgeboard refresh queries.
  44. d07dfeb
    fix(edgeboard): publish snapshots before shadow eval
    Show commit body
    Keep research edgeboard freshness independent from shadow-trade backlog by writing the snapshot before optional training and shadow evaluation work.
  45. 73f9b52
    fix(observability): restore multi-symbol dashboard board
    Show commit body
    Rank decision fallback rows per symbol before the dashboard limit and stop shadow-eval no-progress loops from blocking research edgeboard snapshots.
  46. a62aefe
    fix(execution): delay accelerating continuation trailing
    Show commit body
    Replace native OTO trailing for accelerating continuation with immediate static venue protection and bot-managed tighten-only stop-loss amends after favorable activation.
  47. b5a514d
    fix(execution): restore instrument limit price precision
    Show commit body
    Remove the global USD price precision cap and require instrument constraints for limit-price wire normalization, while keeping trailing pct triggers on Kraken's separate 4dp pct format.
  48. 208a59e
    fix(execution): cap trailing pct trigger precision
    Show commit body
    Kraken validates conditional trailing-stop percent triggers with the same 4dp price precision cap, so quantize pct trigger_price before submit to avoid entry OTO rejects.
  49. 5961ad3
    chore(observability): add pullback capture monitor
    Show commit body
    Track pullback and continuation capture after the path-tape fix from committed SQL instead of server-local drift.
  50. 104b66f
    chore(observability): add candidate trace helper
    Show commit body
    Keep the candidate disappearance trace reproducible from committed tooling instead of leaving the helper as local drift.
  51. 7ced431
    fix(observability): keep export timer independent of oneshot state
  52. 0bcc613
    fix(execution): cap USD limit price wire precision
    Show commit body
    Ensure Kraken USD limit orders use a conservative 4dp wire cap without rounding maker buys upward, and skip prices that cannot be represented instead of producing venue rejects.
  53. 55ef00d
    fix(execution): persist prepared wire limit price for venue precision audits
    Show commit body
    Execution prepared/quantized the order price before submit, but persistence and
    tracker state still stored the pre-normalized intent price. That made rejected
    order forensics look like the exact wire value and obscured whether Kraken saw a
    venue-valid price.
    
    Persist and track `prep.limit_price` from `prepare_order_for_submit` so future
    precision rejects are audited against the actual Kraken-bound price.
  54. cff9fde
    fix(safety): surface residual-small exposure separately from unprotected breaches
    Show commit body
    Keep the residual-small exposure split visible in validation and audit tooling.
    The hard invariant audit now reads the new residual/dust log families and skips
    symbols that only have residual-small or exchange-dust evidence, so small
    administrative balances do not reappear as hard unprotected breaches.
    
    Server validation now reports residual-small and dust counts alongside true
    EXPOSURE_UNPROTECTED counts, preserving visibility without weakening the causal
    protection checks for real exposure.
  55. 93b1a90
    fix(exposure): pure exchange dust (qty/cost) vs residual_small policy band
    Show commit body
    Remove `PROTECTION_DUST_NOTIONAL_USD` from any dust / tradability path — it
    mixed venue minima with an arbitrary USD policy.
    
    Dust / non-dust is now only:
    - below `qty_min`, or
    - at/above `qty_min` but notional < `cost_min` (needs `price_cache` last price),
      matching `InstrumentConstraints` + `assess_position`.
    
    Sub-`cost_min` dust does not zero the DB row (only strict sub-`qty_min` does).
    
    New **incident-only** layer: `RESIDUAL_SMALL_EXPOSURE_USD` (default 20, 0
    disables). `notional_in_residual_small_exposure_policy_band` + logs
    `EXPOSURE_RESIDUAL_SMALL_EXPOSURE` / `POSITION_MONITOR_RESIDUAL_SMALL_EXPOSURE`
    suppress `EXPOSURE_UNPROTECTED` and orphan recovery for venue-tradable small
    restants without labeling them dust.
    
    Startup/periodic reconcile: run delta-dust and residual policy before emitting
    `needs_protection` MSP for real incidents only.
  56. 0e8e462
    fix(dust-classification): add protection-worthy notional floor so economic residuals do not trigger orphan-recovery or unprotected incidents
    Show commit body
    Dust classification was bound exclusively to Kraken exchange minimums
    (qty_min, cost_min). A position of 12-14 USD notional (e.g. ENJ/PLAY/PNUT)
    passes both minimums, so exposure_reconcile and position_monitor treated
    it as fully protection-worthy: EXPOSURE_UNPROTECTED fires, orphan-recovery
    places TSL/emergency orders, symbol_exit_lease gets re-leased. That burns
    protection cycles and log noise on exposure whose economic value is
    smaller than its protection cost.
    
    Introduce a single protection-worthy notional floor, wired into both
    classifiers:
    
    * `instruments::protection_dust_notional_usd_floor()` reads env
      `PROTECTION_DUST_NOTIONAL_USD` (default 20.0, 0/negative/invalid
      disables). Cached once via OnceLock. `assess_position` is left alone so
      exchange-minimum semantics stay pure.
    * `exposure_reconcile::DustSkipReason` gains `BelowProtectionNotionalFloor`
      and `close_dust_in_db()` gating. `classify_qty_as_dust` checks the floor
      only when a last_price is cached, after the qty_min check. All four
      `close_dust_position_and_sync_msp` call sites now only run for the
      exchange-untradable kind so economic residuals keep their DB row and
      exchange <-> DB stay in sync.
    * `position_monitor::refresh_monitored_positions` computes an effective
      dust flag that folds the notional floor into the existing
      POSITION_MONITOR_DUST_ORPHAN branch; orphan-recovery attempts (TSL
      re-arm, emergency stop) are skipped for residuals.
    
    Net effect:
    * ENJ/PLAY/PNUT no longer log EXPOSURE_UNPROTECTED and no longer trigger
      POSITION_MONITOR_ORPHAN_DETECTED / ORPHAN_RECOVERY_TSL_SENT.
    * DB positions row stays open/non-zero (administratively visible).
    * Genuine positions (>= floor) continue to protect normally.
    * Entry/exit decisions are untouched; the floor only affects the
      protection/orphan lifecycle of already-open residuals.
  57. db74b34
    fix(protection): split coverage semantics and clear exit-lease on phantom reconcile
    Show commit body
    Two-part fix for "unprotected position" incident (SPACE/ENJ/PLAY/PNUT):
    
    A. Split status semantics in exit_coverage_semantics.rs
       - Rename EXIT_COVERAGE_STATUSES_SQL -> ENTRY_ADMISSION_BLOCK_STATUSES_SQL
         (broad set, unchanged semantics for entry-admission gating and stale-row
         cleanup). All callers of the broad set are updated.
       - Add PROTECTION_COVERAGE_STATUSES_SQL = pending_submit, pending_new,
         acked_open, partially_filled. Only genuinely live / in-flight exit rows
         count as actual protection coverage.
       - Switch exposure_reconcile::load_open_exit_qty_per_symbol to the narrow
         PROTECTION_COVERAGE_STATUSES_SQL so that phantom rows (reconcile /
         reconcile_required / suspect / cancel_requested) no longer mask an
         unprotected position from protect_exposure / orphan recovery.
    
    B. Lease cleanup in order_reconcile::reconcile_phantom_open_orders_inner
       - After marking stale rows to status='reconcile', collect the distinct
         affected symbols.
       - For each such symbol, if there is no remaining live exit order
         (PROTECTION_COVERAGE_STATUSES_SQL), call symbol_exit_lease::clear(symbol)
         and log PHANTOM_LEASE_CLEARED.
       - Without this, the CSS lease kept pointing at a dead exchange_order_id and
         position_monitor skipped the symbol (POSITION_MONITOR_DIAG_SKIP css_leased),
         so protection was never re-armed on a still-open position.
    
    No scope-creep: no status-machine changes, no new status values, no changes
    to detect_partial_fill_protection triage, no flatten logic.
  58. 56c036a
    fix(db): single-flight + PG statement_timeout + partition prune to break refresh query storm
    Show commit body
    Root cause of `eligibility_state_stale = 100%` observed 2026-04-23 was a
    three-stage cascade in the ingest DB:
    
    1. `l2_snap_latest_age_ms_for_symbol` did `SELECT ts_local ... ORDER BY
       ts_local DESC LIMIT 1` without a `ts_local >=` predicate. On range-
       partitioned `l2_snap_metrics` this opens every daily partition; one call
       was holding a 1h+ `DataFileRead` wait in prod.
    
    2. That zombie SELECT blocked the nightly retention `DROP TABLE
       l2_snap_metrics_p_ts_<date>` on an `AccessExclusiveLock`, which in turn
       queued every subsequent ALTER/DROP/INSERT behind it.
    
    3. `refresh_run_symbol_state` is called by live_runner, ingest_runner and
       flow_execution concurrently. The Rust-side `timeout(60s)` cancels the
       future but not the PG backend, so each timed-out refresh kept executing
       in PG while the next cycle fired another one — observed 8 stacked refresh
       statements, oldest 10+ minutes old. `MAX(updated_at) FROM run_symbol_state`
       consequently stayed 3-4 minutes old, which is the sole hard freshness
       authority in `apply_route_freshness_filter` and thus blocked every
       tradable candidate with `eligibility_state_stale`.
    
    Fix slice (smallest surgical change that breaks all three):
    
    - `src/db/run_symbol_state.rs`
      - Global `tokio::sync::Mutex` single-flight guard in
        `refresh_run_symbol_state` so at most one refresh is in flight per
        process; log waits >=1s.
      - `SET LOCAL statement_timeout = 55000` prepended inside both
        `do_full_refresh` and `do_incremental_refresh` transactions so PG cancels
        the backend strictly before the 60s Rust timeout — refreshes can no
        longer stack in PG.
    
    - `src/db/read/stats_queries.rs` / `src/pipeline/strategy_pipeline.rs`
      - Add `AND ts_local >= NOW() - INTERVAL '15 minutes'` to the per-symbol
        newest-L2 probes so the planner prunes to the latest 1-2 partitions
        instead of scanning every historical partition. 15 min is an order of
        magnitude above `LIVE_DATA_MAX_AGE_SECS` and cannot produce false
        "no L2" answers for live symbols.
  59. 6c6a605
    fix(path_tape): route-family override prevents continuation routes from leaking into FadingMomentum 45s time-stop
    Show commit body
    Problem: classify_path_tape classified on ignition_state before checking
    route_family, so pullback/continuation routes (multi-minute edge horizon
    selected by route engine) got demoted to FadingMomentum when the ignition
    snapshot was Quiet/Compression/Exhaustion. FadingMomentum + TimeDecay then
    applied the 45s scalp time-stop, killing trades like LCX/USD and PRIME/USD
    at exactly 45s despite being H1m+ continuation theses (realized -56 to
    -114 bps on routes with +12 to +21 bps expected edge).
    
    Fix: in classify_path_tape, when route_family contains "pullback" or
    "continuation", return AcceleratingContinuation (tight spread) or
    LinearDrift (wider spread) before the ignition_state match. Route thesis
    overrides ignition snapshot for these cohorts. Thin/low-liquidity gates
    above still apply. Genuine fade/mean routes unaffected.
  60. 322ae8c
    fix(kraken-ws): buffer add_order deadline max from 60s to 55s to prevent clock-skew rejects
    Show commit body
    Kraken validates deadline as 0.5–60s offset from *their* receive clock.
    When host clock runs ahead of exchange clock, local Utc::now()+60s
    exceeds Kraken's 60s limit, causing EGeneral:Invalid arguments:deadline
    rejects (observed on 8/25 orders = 32% reject rate).
    
    Reduce KRAKEN_ADD_ORDER_DEADLINE_MAX_MS from 60_000 to 55_000 (5s buffer).
  61. 7378f2a
    fix(pipeline): demote selector_v2 from hard veto to annotation for route-selected symbols
    Show commit body
    The v2_selector_symbols gate hard-excluded all symbols not in its
    independent top-N ranking. Since the selector uses realized_move ×
    tradability_score (a spread proxy) while the route engine evaluates
    net edge after spread/fees/slippage, the two systems selected fully
    disjoint sets — 0 overlap between 13 route-engine TRADABLE symbols
    and the selector's 10 picks. Result: pipeline_outcomes=0 despite
    proven positive edge.
    
    Fix: RouteSelection::Selected symbols (route-engine economic authority)
    always enter the pipeline loop. The selector result is preserved as a
    SELECTOR_V2_BYPASS log annotation for observability. The selector
    continues to operate for non-Selected symbols and as a prioritization
    signal, but can no longer veto economically tradable candidates.
    
    Evidence: run_id=4682 eval=2 — 13 tradable symbols with 1.6-79.4 bps
    edge, all skipped by selector gate, producing 0 pipeline outcomes.
  62. 7cb649c
    fix(epoch): remove ticker/trade from epoch degraded status determination
    Show commit body
    criteria_ticker_ok and criteria_trade_ok required 90% of execution
    symbols to have >=10 ticker/trade updates per 15-min window. With 583
    symbols (post universe gate fix), most microcap pairs have <10 trades
    while L2 books stream every second, causing every epoch to be marked
    "degraded" and triggering false pushover alerts.
    
    Epoch status now depends only on L2 availability. Ticker/trade criteria
    are still computed and logged as diagnostic fields but no longer affect
    the valid/degraded determination.
  63. fefe972
    chore: remove dead StaleKind::Message variant and kind field
    Show commit body
    StaleKind::Message is never constructed after b03e992 demoted
    message_age to a soft signal. Remove the variant, the kind field
    from EligibilityStaleBlock, and the match arms in live_runner.rs.
  64. b03e992
    fix(eligibility): demote message_age from hard block to soft signal when state is fresh
    Show commit body
    The route freshness filter hard-blocked 92-98% of tradable candidates via
    `eligibility_message_stale` because their last ticker/trade WS message
    exceeded the 45s limit. However, state_age_secs=0-1 proved the ingest
    pipeline was alive with L2 books streaming every second.
    
    The message_age measures trade activity (liquidity proxy), not data quality.
    Most Kraken microcap pairs have <10 trades per 15-min window, making
    message_age perpetually stale despite healthy L2 data.
    
    Fix: state_age is now the sole hard-block authority. When state is fresh,
    stale message_age is logged as ROUTE_FRESHNESS_OK_MESSAGE_SOFT (soft
    warning, not blocked). State-stale remains a hard block unchanged.
    
    Evidence: run_id=4680 — USDUC/USD had 158-267 bps stable edge across 5
    evals, blocked every time by message_age=207-709s while state_age=0-1s.
  65. a79d32b
    fix(universe): remove ticker/trade gate from execution layer — score all 582 symbols
    Show commit body
    The execution universe gate required (ticker >= 10 OR trade >= 10) AND
    l2 >= 50 within a 15-minute rolling window. While all 582 symbols had
    sufficient L2 data (>15K samples/15min), 478 had <10 ticker AND <10
    trade samples — Kraken spot microcap pairs update infrequently. This
    excluded 82% of the pool before any scoring/ranking occurred, violating
    the "score all, rank all, annotate all, execute few" principle.
    
    Fix: retain only the L2 activity gate (l2 >= 50), which already ensures
    orderbook data quality for route evaluation. Ticker/trade sparsity does
    not impair L2-based execution decision-making.
    
    Evidence: run_id=4676 eval=5 — pool=582, exec_universe=106, route_eval=101.
    After fix: exec_universe should equal ~582 (all with L2 >= 50).
  66. 71b181f
    feat(observability): trace drain task lifecycle to isolate hang point
    Show commit body
    Adds FLOW_DRAIN_TASK_SPAWNED, FLOW_DRAIN_ENTERED,
    FLOW_DRAIN_GEN_SNAPSHOT_OK, FLOW_DRAIN_EXEC_ENTER,
    FLOW_DRAIN_EXEC_RETURNED, FLOW_DRAIN_TASK_COMPLETED to pinpoint
    exactly where the parallel drain task hangs despite dedicated pool.
  67. ee023fe
    fix(execution): dedicated flow_drain pool isolates heap drain from eval DB contention
    Show commit body
    Root cause: the flow drain task shares the decision pool (5 conn max)
    with the eval loop, which runs queries taking 225–318s. Pool starvation
    blocks the drain task on its first DB await, holding the in_flight_symbols
    guard indefinitely, preventing pop_batch from returning any candidate.
    
    Fix: create a dedicated flow_drain pool (2 conn, same decision DB) used
    exclusively by drain_single_heap_entry and flow_execute_single_candidate.
    This isolates the heap execution handoff from eval loop contention.
    
    No trading-behaviour, heap-semantics, or execution-logic changes.
  68. e58b063
    feat(observability): INFO-level drain-path logging in flow_poller
    Show commit body
    Expose the silent gate/drain-miss paths in spawn_flow_poller that
    previously had no INFO-level output, causing candidates to expire
    unprocessed without any logged reason.
    
    Adds FLOW_POLLER_GATE_BLOCKED (allow_new_entries, bid, rid gates)
    and FLOW_POLLER_DRAIN_MISS (heap_active > 0 but pop_batch empty)
    with run/correlation ids, heap_active, exclude_count, suppressed_slots,
    and oldest candidate symbol/score/ttl.
    
    No trading-behaviour, heap-semantics, or execution-logic changes.
  69. cc56eb8
    chore(ssh): canonical production host root@snapdiscounts.nl
    Show commit body
    Document default SSH and DEPLOY_HOST in remote-execution rule and deploy.sh.
  70. 3c9ad42
    feat(execution): route_eval_universe vs submit_exec_allowed for V2
    Show commit body
    Clone symbols after hard-blocks and before fanout as route_eval_universe;
    pass it to run_adaptive_route_analysis / run_v2_route_analysis_with_ingest.
    Pipeline unchanged: still Some(post-fanout/post-freshness allowlist).
    
    Log ROUTE_SUBMIT_SCOPE with route_eval_universe_count, submit_exec_allowed_count,
    tradable_in_report, tradable_in_submit_scope on main loop, execution-only loop,
    and run-execution-once.
  71. b86913f
    fix(pipeline): rank all V2 Selected; gate submit on execution allowlist
    Show commit body
    Freshness can shrink exec_allowed after v2_report is built; filtering the
    ranked set by allowlist duplicated that cut and emptied the board while
    tradable_in_report stayed high. Risk gate now blocks with
    execution_allowlist outside scope; V2_PIPELINE_SCOPE logs ranked vs
    submit-scope counts.
  72. aa31c09
    fix(observability): correct entry_blocked scope for message vs state stale
    Show commit body
    MESSAGE_STALE is per-symbol in exec_allowed; STATE_STALE shares global
    state_age and blocks all selected routes in that evaluation. Dashboard
    scope strings now match runtime semantics.
  73. 1bfbb49
    fix(trading): TE contract spread uses route signed spread_impact
    Show commit body
    Route expectancy subtracts spread_impact_bps from capturable move; for maker
    entry cost_model makes that impact negative (half-spread income). The trade
    expectancy admission contract instead added a positive bid/ask width as
    te_spread_bps, so te_net_bps collapsed vs expected_net_edge_bps and admission
    choked on trade_expectancy_below_floor despite healthy route scores.
    
    Plumb route.spread_impact_bps into LiveRouteInputs and prefer it when building
    the sealed contract; keep L1/ingest spread fallback only when unset.
  74. 4fda0ee
    fix(observability): rank decision edgeboard fallback inside ingest universe
    Show commit body
    The decision fallback query selected the top CDV rows globally by edge, then
    filtered in Rust to run_symbol_state symbols. LIMIT applied before the filter,
    so high-edge rows outside the active ingest run consumed the cap and the UI
    could show a single symbol. Load ingest symbols once, query CDV with
    symbol = ANY(universe), and reuse the cached set for the research filter.
  75. fd72d56
    observability: log exec_allowed after route freshness and gen gate
    Show commit body
    Add ROUTE_FRESHNESS_EXEC_SCOPE_FINAL (singleton + csv/preview) so operators
    can see which symbols still enter the pipeline when tradable_in_scope is 0.
  76. 1fb56f6
    fix(path_tape): spread-aware static SL floor (Fix-γ)
    Show commit body
    Forensic γ-2 (last 7d): 41/71 sub-30s exits were exit_sl_static placed
    22-32 bps below entry, i.e. on/inside the live 20-50 bps bid-ask. Root
    cause: measured_static_stop_magnitude_bps used `clamp(22.0, ..)` and
    `liquidity_floor = sp*2 + 8`, ignoring spread for the lower clamp.
    
    Changes (single function in src/execution/path_tape.rs):
    - liquidity_floor: sp*2 + 8 → sp*2.5 + 12
    - final clamp lower bound: fixed 22 → max(22, sp*1.5 + 8) [spread-aware]
    
    Effects (modeled):
    - sp=10 bps (majors): floor ≈ 23 bps — unchanged
    - sp=30 bps: floor = 53 bps (was 22) — SL outside spread band
    - sp=80 bps (illiquid): floor = 128 bps — wider, no instant-trigger
    - Upper clamp 250 bps preserved.
    
    All 10 path_tape unit tests pass. Single-file change. No TE/admission/
    panic/TP/trailing/cost-model touched.
  77. 7f2a6b0
    chore(sql): tmp gamma-2 exit trigger audit
  78. 79d286d
    audit: ephemeral exit-hold SQL (to be deleted post-iteration)
  79. 6b6027a
    activation: gate MakerPassiveQueue on drift>=fee-breakeven floor
    Show commit body
    Add MIN_ABS_DRIFT_1M_FOR_MAKER_PASSIVE_BREAKEVEN=11 bps/min so MakerPassiveQueue
    rejects when drift cannot cover Kraken round-trip fees (~55 bps maker+taker)
    within a typical hold. Combined with the existing MAX_ABS_DRIFT_1M=2.40 upper
    bound, MakerPassiveQueue is now structurally rejected on the current spot fee
    tier with explicit reason (drift_too_weak_for_round_trip_fee_breakeven), which
    removes ~10.5k redundant SurplusBelowFloor/SpreadTooWide CDV rows per cycle.
    
    Cohort-bounded: only MakerPassiveQueue. mean_reversion_* and other strategies
    unchanged. Also drops ephemeral admission-audit SQL.
  80. 89e6981
    EXIT_HOLD_ALIGNMENT: floor static time-stop to route horizon third
    Show commit body
    Forensic (7d live LONG cohort, 2026-04-18): 78% of SL+TS exits fired in
    30-90s while price continued +59 to +234 bps at t+300s. Admission
    contracts on route.max_hold_secs (e.g. 900s for H15m), but the per-class
    static defaults in path_tape (60/45/120s) truncate that horizon and close
    trades before the edge materializes.
    
    This slice aligns the static-time-stop path to the admitted route horizon:
    - route_aligned_time_stop(existing, route_max_hold_secs) clamps to
      [route_max_hold_secs / 3, route_max_hold_secs] when a horizon is known.
    - Applied to SpikyChaotic, ThinSlow, FadingMomentum, MeanRevertHarvest,
      IlliquidJumpy (the cap_trailing=false branches that drive live
      exit_time_stop_static / exit_sl_static exits).
    - Trailing classes (AcceleratingContinuation, LinearDrift, Default with
      TSL) and the stop-loss / panic / TP magnitudes are untouched.
    
    For H15m routes this lifts the static time-stop floor from 45-120s to
    300s. When route horizon is unknown (None), behavior is preserved.
    
    Scope-bounded per the slice brief: no TE tuning, no admission-floor
    change, no slippage model change, no route-horizon change, no broad
    refactor. TE-vs-realized recalibration is deferred until post-deploy
    data confirms the hold-collapse is resolved.
    
    Existing test mean_revert_harvest_static_doctrine updated from pinning
    pre-alignment behavior (<=120s) to enforcing the alignment invariant
    (>=300s for H15m). Two regression tests added: static classes floor to
    route_max/3 for H15m; behavior preserved when route horizon is None.
  81. fe9349b
    audit: ephemeral SQL for post-deploy admission audit (to be deleted)
  82. a3270a2
    feature_ready_gate: count distinct symbols, not arbitrary rows
    Show commit body
    `l2_raw_feature_ready()` used `LIMIT 8000` on raw rows then `COUNT(DISTINCT)`.
    On a fresh execution-service restart bound to a long-running ingest run_id,
    the first 8000 rows of the `(run_id, spread_bps)` B-tree scan clustered on
    only 2–3 symbols, so the gate reported `symbols_with_spread=2` even though
    the underlying table had 581 symbols × 581 with spread. Result: pipeline
    never became ready → 0 CDV rows for 23+ min after restart.
    
    Fix: `SELECT DISTINCT symbol ... LIMIT 100` caps on distinct symbols instead
    of raw rows. Same semantics, correct result, bounded work.
    
    No schema, no threshold, no admission change.
  83. 603d1ea
    te_spread_bps: prefer live L1 spread from price_cache over run-lifetime mean
    Show commit body
    Edge discovery evidence (24h, INGEST vs DECISION) showed that the ingest-side
    `run_symbol_state.avg_spread_bps` — a run-lifetime arithmetic mean of
    per-snapshot L2 spreads — is structurally 3–6x above the true current spread on
    fat-tail microstructure (RAVE 251 vs 43 median, API3 204 vs 54, DENT 229 vs 81,
    EIGEN 41 vs 11). Root cause is window staleness combined with mean-vs-median
    bias from occasional wide-spread spikes. This inflated `te_spread_bps` into
    `te_total_cost_bps` and pushed otherwise-tradable candidates below the
    trade_expectancy floor.
    
    Smallest safe correction: prefer the in-memory ticker-fed `price_cache` L1
    spread (sub-second freshness) with guards `ask > bid > 0` and `age <= 5s`.
    Falls back to the existing `state_row.avg_spread_bps` when no fresh snapshot is
    available — no regression when price_cache is cold.
    
    Cohort-self-scoped: majors (BTC/ETH/SOL with ratio ~1.0) get ~no correction;
    the fat-tail cohort gets the full correction. No migrations, no floor/tier/
    universe/slippage changes, no admission-gate relax.
  84. b80fc09
    config: default edgeboard RESEARCH snapshot interval to 60s when RESEARCH_DATABASE_URL is set
    Show commit body
    Previously required FORWARD_RETURNS_OBS_ENABLE; without it the interval stayed 0 and research edgeboard went stale on the dashboard.
  85. 8f347fd
    feat(observability): post-fill drift samples + round-trip view (observability-only)
    Show commit body
    Adds per-fill post-fill drift sampling at t+15/30/60s and a round-trip view that joins
    admission CDV (te_* model inputs) to realized_pnl and real post-fill mid drift. Fills the
    measurement lacuna where krakenbot.fills.post_fill_mid was hardcoded to orderbook_mid at
    fill-time (fills_ledger.rs:62), making actual post-fill directional drift unobservable.
    
    No gate, sizing, or exit-policy change. Spawn-and-forget tokio task; insert-only writes;
    UNIQUE(fill_id, horizon_secs). Enables evidence-based decisions on LOW_LIQUIDITY symbols
    (PRIME/USD, ALICE/USD) after ~7 days of data.
    
    - migrations/decision/20260419100000_execution_post_fill_drift_samples.sql
      new table krakenbot.execution_post_fill_drift_samples + view krakenbot.execution_round_trips
    - src/db/execution_post_fill_drift_samples.rs
      insert_drift_sample helper (PriceSource::{Fresh,Stale,Missing})
    - src/execution/post_fill_drift_sampler.rs
      spawn_post_fill_drift_sampler: tokio task that sleeps to each horizon, reads
      price_cache snapshot, persists drift row. All errors logged, never bubble up.
    - src/execution/ws_handler.rs
      hook after successful fills_ledger::process_fill (non-duplicate, fill_id present)
    - scripts/sql/realized_edge_vs_te_net_report.sql
      canned 7-day analysis: per-tier gap, per-symbol losers, drift coverage, low-liq focus
  86. e45a834
    fix(observability): persist 15m market forecasts under data_run_id for Tier-2 export
    Show commit body
    tier2_data_bundle keys market_forecast_15m by ingest status_run_id (same as
    data_run_id when ingest feeds execution). Snapshots were stored under
    execution run_id, so the Strategy/Regime matrix stayed empty on the site.
    
    Also align snapshots module doc with CONTRACT_VERSION.
  87. 2ca9066
    fix(eligibility): split state vs message staleness; use global ingest freshness
    Show commit body
    ENTRY_BLOCKED_ELIGIBILITY_MESSAGE_STALE root-cause was a two-fold bug:
    
    1. `state_updated_at(pool, run_id)` filtered `ticker_samples`/`trade_samples`
       by the *execution* run_id, but ingest writes under a different (ingest)
       run_id. The execution-owned run_id never matches ingest samples, so the
       query returned NULL every cycle and the filter unconditionally flagged
       every symbol as stale.
    
    2. `apply_route_freshness_filter` received `state_age_secs` passed in twice
       (once as state, once as message), so the message-age limit was actually a
       state-age check with a different threshold. All stale blocks were then
       emitted as MESSAGE_STALE regardless of root cause.
    
    Slice A — global ingest freshness
    
    - Add `run_symbol_state::state_updated_at_global(pool)`: `MAX(updated_at)`
      on the `run_symbol_state` table (no run_id filter). This reflects ingest
      writer aliveness independently of the execution run_id. Cheap: ~500 rows.
    - Remove the per-run `state_updated_at` function (unused after the switch;
      removing prevents future miswiring).
    - `run_readiness_analysis_for_run_from_state` now uses the global variant
      for `state_updated_at` / eligibility age.
    
    Slice B — real per-symbol message age + split funnel events
    
    - `apply_route_freshness_filter` now takes a `message_age_lookup` closure
      and returns a `Vec<EligibilityStaleBlock>` with a `StaleKind::{State,Message}`
      tag. Classification: state-stall wins when both fail; message reported
      only when state is fresh but the specific symbol's WS feed has gapped.
    - Unknown message age (symbol not in price cache) is conservative: does not
      by itself block; state-age alone still proves ingest aliveness.
    - `live_runner` call sites supply
      `|sym| price_cache::last_trade_age_ms(sym).map(|ms| (ms/1000) as i64)` as
      the message-age source (public WS trade recency per symbol).
    - Funnel events now split correctly:
        * `ENTRY_BLOCKED_ELIGIBILITY_STATE_STALE`   (reason=eligibility_state_stale)
        * `ENTRY_BLOCKED_ELIGIBILITY_MESSAGE_STALE` (reason=eligibility_message_stale)
      Event detail JSON adds `stale_kind` and `max_age_secs` (renamed from
      `max_message_age_secs` to reflect the now-kind-specific threshold).
    
    Scope-bound: no threshold changes, no entry-policy changes, no semantic
    changes beyond correctly sourcing and labeling freshness.
  88. 20d2ca1
    fix(website-pm2): load tier secrets from /etc/trading/kapitaalbot-website.env
    Show commit body
    PM2 previously only passed NODE_ENV, PORT, and OBSERVABILITY_EXPORT_DIR, so
    TIER2_SECRET, TIER_COOKIE_SECRET, and REDIS_URL from the server envfile were
    never applied. Merge optional KEY=value file into ecosystem env with explicit
    overrides for PORT and export dir.
  89. 99a1d12
    fix(cdv/schema): allow stage='readiness' in candidate_decision_vectors
    Show commit body
    The base migration (20260331100000) only whitelisted
    {route_eval,path_validation,conflict_lane,admission,capital,execution_precheck}.
    Commit 84baa9c started writing readiness-annotator rows with stage='readiness'
    so they no longer masquerade as admission rows, which hit the legacy CHECK
    constraint and caused every readiness persist to fail
    (READINESS_VECTOR_PERSIST_FAILED / CDV_WRITE_FAILURE on the hot path).
    
    Expand the allowlist to include 'readiness'. No data rewritten.
  90. fb6c95d
    fix(l2 cutover): exclude pg_backend_pid() from active-INSERT self-match check
  91. b3e29d5
    ingest(l2): add time-partitioned l2_snap_metrics target + cutover tooling
    Show commit body
    Root cause of recurring disk-space incident: krakenbot.l2_snap_metrics is
    RANGE-partitioned on run_id with all ~1.8B rows in a single DEFAULT partition
    (~725 GB: 326 GB heap + 399 GB indexes over 7 indexes). DELETE-based retention
    leaves heap + index bloat and cannot reclaim disk without VACUUM FULL/REINDEX,
    which is infeasible on the live volume.
    
    Fix:
    - migrations/ingest/20260418120000_l2_snap_metrics_time_partition_target.sql
      new parent krakenbot.l2_snap_metrics_ts_target RANGE(ts_local) with daily UTC
      partitions, minimal index set (PK(ts_local,id), (run_id,id), (run_id,symbol),
      BRIN(ts_local)), ensure_l2_daily_partitions() and today-2..today+14 seed.
    - scripts/maintenance/l2_time_partition_cutover.sh
      rename swap (old -> _runid_legacy, ts_target -> l2_snap_metrics) with
      sequence realignment and L2 watermark reset. Requires service stopped.
    - scripts/maintenance/l2_ts_partition_{ensure,retention_drop}.sh
      daily UTC partition ensure + KEEP_DAYS=4 retention drop with per-partition
      watermark preflight.
    - src/db/retention.rs: gate per-run DELETE on l2_snap_metrics behind
      L2_RETENTION_MODE (default time_partition = skip). FK-violation on
      observation_runs is now tolerated in time_partition mode (next cycle after
      partition drop clears it).
    - scripts/l2_retention_cycle.sh: no-op when L2_RETENTION_MODE=time_partition.
    
    Why partition drop beats mass DELETE: DROP TABLE <partition> is metadata-only;
    disk is returned to the filesystem immediately without VACUUM. Retention then
    becomes a bounded daily operation on ~80 GB/day of L2 heap.
  92. 84baa9c
    fix(cdv/observability): label readiness CDV rows with stage='readiness'
    Show commit body
    spawn_readiness_vector_persistence previously wrote readiness-annotator
    rows into candidate_decision_vectors with stage='admission'. This
    conflated two distinct semantic layers:
      - readiness: per-pair tradable/blocker annotation (no contract authority)
      - admission: trade-expectancy-contract decision (te_resolution, te_net_bps)
    
    With stage='admission' applied to both, 'SELECT ... WHERE stage=admission'
    returned a mix of readiness blockers and V2-pipeline contract rows, making
    te_* coverage, admission_ok counts, and economics_unresolved_* rates
    unreliable for any analytics.
    
    Label readiness rows as stage='readiness'. The admission stage now
    strictly contains contract-authority output from run_strategy_pipeline_v2
    (or its recycle-lane equivalent). route_family='readiness' continues to
    mark readiness rows inside their own stage.
    
    No runtime dependency required stage='admission' on readiness rows (both
    strategy_pipeline.rs stage checks target the V2 path; flow_execution.rs
    is an unrelated funnel event). Downstream analytical SQL under
    sql/analysis/** may need follow-up to adopt the cleaner partition.
  93. 3299e1e
    fix(cdv): enable recycle-lane pipeline CDV persistence
    Show commit body
    Flip RECYCLE_SYMBOL_PIPELINE_CDV_PERSIST from false to true so Trade
    Expectancy Contract resolutions (te_resolution, te_net_bps, admission_ok,
    trade_expectancy_below_floor, economics_unresolved_*) emitted by the
    per-symbol recycle lane become observable in candidate_decision_vectors.
    
    The original disable reason (avoid NULL evaluation_index fan-out) no
    longer holds: evaluation_index is nullable in the schema
    (migrations/decision/20260331100000_candidate_decision_vectors.sql) and
    persist_pipeline_candidate_vector enforces the per-(symbol, route_family)
    cdv_row_budget cap (64), bounding recycle-frequency traffic per
    invocation.
    
    Without this flip 99.97% of V2 pipeline invocations (recycle lane) ran
    build_resolution correctly but dropped the resulting CDV rows, leaving
    te_* NULL across the DECISION pool.
    
    Update cdv_contract_tests::recycle_heap_refresh_* to assert the new
    invariant.
  94. 718c7e8
    fix(db/cdv): add missing $111 placeholder to candidate_decision_vectors INSERT
    Show commit body
    Commit b66071f added 14 te_* target columns (te_expected_hold_minutes ..
    te_provenance_json) to `candidate_decision_vectors.insert_vector`, but the
    VALUES clause only gained 13 new placeholders: 111 target columns vs 110
    placeholders. Postgres rejected every CDV insert with
    "INSERT has more target columns than expressions", so no CDV row has been
    written since the deploy at 2026-04-17 14:00:09+02. Both readiness vector
    persistence and pipeline admission/capital vectors failed; te_* persistence
    was therefore untestable.
    
    Fix: extend the final VALUES line to $97..$111 (15 placeholders covering
    edge_source + 14 te_* columns) so target columns, placeholders and the
    .bind(...) chain are all 111.
    
    Extract the SQL into `cdv_insert_sql()` and add a regression test
    `cdv_contract_tests::insert_sql_column_placeholder_parity` that re-parses
    the template and asserts
      column_count == placeholder_count == highest $N == CDV_INSERT_BIND_COUNT.
    This prevents the same off-by-one if te_* or other columns are added later.
    
    Scope: strictly src/db/candidate_decision_vectors.rs. No pipeline, tuning,
    observability or te_* semantic changes.
  95. b66071f
    db(cdv): persist trade expectancy contract + canonical admission reason
    Show commit body
    - Add te_* and provenance columns to INSERT for candidate_decision_vectors
      (migration 20260415160000 already defines columns).
    - apply_trade_expectancy_resolution_to_cdv maps Resolved / Unresolved /
      SafetyViolation to typed columns and optional provenance JSON.
    - Build TradeExpectancyResolution before admission CDV write; persist
      contract on admission and capital-stage rows; funnel SIGNAL_ADMISSION_DECISION
      uses the same decide_admission verdict as flow_execution (no execution
      behavior change).
    - Admission-stage reason_code and admitted now match AdmissionReasonCode
      from the contract choke (SSOT with flow_execution).
    
    Add scripts/sql/t2_t22_eval_querypack.sql for T2.2 lifecycle and exit-mix
    analysis on DECISION.
  96. b507702
    execution(exit): T2 partial_take_profit_then_trail on static-SL path
    Show commit body
    Venue-conform single-leg partial TP for Kraken spot:
    - At first internal-TP crossing (tp_hit in static-SL monitor), cancel the
      active SL, market-sell PARTIAL_TP_FRACTION (0.50) of live balance, then
      re-arm a fresh SL on the remainder at the original sl_trigger. Exactly
      one partial per trade (partial_tp_done guard). Breakeven amend re-arms
      after partial so remainder can tighten further.
    - Single-leg only: no concurrent reduce-side orders, no re-introduction
      of TP_MAKER_REJECTED_STATIC. Trailing-primary path (native trailing)
      unchanged this slice — static-SL is the measured capture lek.
    - Persistence reuses execution_order_events (no migration):
      exit_policy_selected on plan creation, partial_tp_executed on success,
      partial_tp_aborted on fallback to the pre-T2 full-exit path.
    - Rollback hendel: env EXIT_PARTIAL_TP_ENABLE (default true). Setting to
      false and restarting reverts behavior without git revert.
    - Any error inside the helper (cancel/market/rearm) falls through to the
      existing full-exit path; failure surface is identical to pre-T2.
  97. 899e3b0
    fix(db/execution_orders): inherit decision_ts on exit orders (observability)
    Show commit body
    `insert_exit_order` previously left `decision_ts` NULL on all exit rows
    (SL, TP, trailing, emergency, etc). Analysis queries that window on
    `decision_ts > now() - X` therefore silently dropped the entire exit side
    of the bot lifecycle, which misled the recent tuning round into concluding
    "0 bot-driven sells" (actual: ~64 filled/48h).
    
    Inherits `decision_ts` from the parent entry row via the same subquery
    pattern already used for `decision_trace_id`, `path_tape_class`,
    `position_id`, and `correlation_id`. Pure observability — no behavior or
    lifecycle change; exit placement, submission, and monitoring are unchanged.
  98. 27e5d51
    fix(exit_lifecycle): disable concurrent maker-TP on Kraken spot (venue-conform)
    Show commit body
    Kraken spot reserves the full base-qty at the first reduce-side leg (trailing-stop
    or stop-loss). The concurrent maker-TP limit attempt on the same qty is therefore
    rejected with `EOrder:Insufficient funds`. Production logs show this 100% of the
    time on static-SL exits. The in-process TP branch (`tp_limit_order_id.is_none()`
    in both monitor loops) already handles TP crossings via cancel-protection +
    market exit; `exit_tp_internal_static` / `exit_tp` fill regularly and are the
    proven path.
    
    Introduces module const `MAKER_TP_CONCURRENT_SPOT = false` and gates both maker-TP
    placement sites (native_trailing_primary + static_sl_primary) on it. A single
    `TP_MAKER_SKIPPED_VENUE_CONFORM` warn is emitted per skip for log-based
    verification. No flag, no policy change, no entry/admission change — just the
    venue invariant that Kraken spot does not support concurrent reduce-legs on
    the same qty.
  99. 674f0a4
    refactor(pipeline): demote conflict_lane hard-block + legacy writes to annotation
    Show commit body
    Three legacy conflict_lane write-paths in run_strategy_pipeline_v2_inner
    are removed as authoritative admission signals:
    
      1. FORCE_TRADE_BREAKEVEN_OVERRIDE block — flipped
         conflict_lane_passed back to true if force_trade_on_breakeven
         and emitted its own funnel event with admitted=true.
      2. conflict_lane_rejected block — emitted a CDV row
         (stage=conflict_lane, admitted=false, reason=<final_reason>),
         two funnel events (conflict_lane_rejected_other_reason +
         SIGNAL_ADMISSION_DECISION), a shadow trade, a skip Outcome,
         and `continue`ed.
      3. conflict_lane diagnostic allowed write — emitted a CDV row
         (stage=conflict_lane, admitted=true, reason=conflict_lane_*_allowed)
         and a duplicate funnel event for the same decision.
    
    All three were authoritative legacy paths that set reason_code and
    admitted-state independently of TradeExpectancyResolution. They are
    removed wholesale.
    
    What is preserved:
    - conflict_lane_eval itself still runs (it is the source of
      `size_multiplier_effective` and `adjusted_soft_gate_score` still
      applied downstream for sizing/scoring).
    - CONFLICT_LANE_EVAL + CONFLICT_LANE_TAKER_SOFT_PENALTY info logs
      stay for operator visibility.
    - The full conflict_lane outcome (enabled, passed, reason, soft
      penalty, size multipliers, required_net_edge_bps_final, soft
      gate scores) is now embedded as `legacy_conflict_lane_*` and
      `legacy_{base,adjusted}_soft_gate_score` annotations in the
      admission CDV row's detail_json.
    
    Net effect on authority:
    - strategy_pipeline V2 no longer short-circuits candidates on
      edge_floor or conflict_lane. Every candidate reaching the V2
      admission phase is handed to flow_execution::decide_admission,
      which is the sole admission authority and the sole reason_code
      setter (AdmissionReasonCode).
    
    cargo check clean; pipeline+trading tests 75/75 and 20/20 green.
  100. 819a438
    refactor(pipeline): demote edge_floor_block from blocker to annotation
    Show commit body
    edge_floor_block was a legacy hard-gate in run_strategy_pipeline_v2_inner
    that short-circuited candidates whose edge_for_ev_floor was below a
    route-specific floor, emitting its own CDV row (stage=admission,
    admitted=false, reason=edge_floor_block), funnel event, shadow trade,
    skip Outcome, and `continue`.
    
    Authority over admission is now solely flow_execution::decide_admission,
    driven by TradeExpectancyResolution. The edge-floor check must not
    set reason_code or admitted-state on its own.
    
    Changes:
    - Remove the entire edge_floor_block branch (persist/funnel/shadow/skip/continue).
    - Keep the `edge_floor` and `hc_taker_edge_bypass` locals; they are still
      referenced by the admission-OK funnel event + detail_json for
      continuity.
    - Capture `edge_floor_breach` as a boolean annotation, passed into the
      admission CDV row's detail_json as `legacy_edge_floor_breach` plus
      `legacy_edge_for_ev_floor`, so observability keeps the signal without
      projecting a reason_code.
    - `skipped_negative_ev` counter on the V2 path is now never incremented;
      left at 0 for telemetry parity with V1 until the reporting migration.
    
    No ingest, no schema, no exit-policy changes. `cargo check` clean;
    pipeline unit tests 75/75 green.
  101. df4db75
    feat(market-state): plumb L2 top-of-book notional (quote) into CurrentRunMarketRow
    Show commit body
    Unblocks live economics: the slippage specialist requires
    bid_notional_quote_top / ask_notional_quote_top in quote units to
    return Resolved. Before this commit the pipeline adapter passed
    None for both fields, so TradeExpectancy systematically fell
    closed on EconomicsUnresolvedSlippage for every live candidate.
    
    Data path:
      l2_snap_metrics (best_bid, bid_depth_top5, ...)  [INGEST pool]
        -> stats_queries::l2_avg_top_notional_quote_by_run
           (AVG(best_bid * bid_depth_top5) / AVG(best_ask * ask_depth_top5)
            over the rolling L2 spread window)
        -> CurrentRunMarketRow { bid_notional_quote_top, ask_notional_quote_top }
        -> trade_expectancy_pipeline_adapter::LiveMicroInputs
        -> SlippageInputs -> estimate_slippage_resolution -> Resolved
    
    Wiring:
    - new SQL helper l2_avg_top_notional_quote_by_run in stats_queries;
      parses numeric(text) -> f64, rejects non-finite / non-positive
      values so absence == Unresolved (no silent default).
    - analyze_run_from_state: join the new query into the existing
      tokio::join! block alongside spread/L2-count readers.
    - analyze_run (legacy 24h path): single await, same treatment.
    - Three CurrentRunMarketRow constructors populate the new fields;
      tests with struct literals (conflict_lane, market_features) set
      both to None.
    - strategy_pipeline adapter now reads the real values.
    
    Window semantics: reuses L2_SPREAD_ROLLING_WINDOW_SECS so the
    notional average is consistent with the spread baseline already
    used for contract cost accounting.
    
    DB-target verification (local .env is a placeholder, no live URL):
    schema confirmed from migrations/ingest/20240308100000_krakenbot_schema.sql
    (best_bid NUMERIC, best_ask NUMERIC, bid_depth_top5 NUMERIC,
    ask_depth_top5 NUMERIC). Server deploy will run the full precheck
    via validate_live_engine_server.sh before live execution reads the
    new column.
    
    Tests: cargo check clean; 20/20 trading tests pass.
  102. fe54bc4
    feat(pipeline): build + attach TradeExpectancyResolution at admit site
    Show commit body
    strategy_pipeline now builds a sealed trade-expectancy resolution
    for every admitted candidate via the pipeline_adapter and attaches
    it to the Outcome via with_trade_expectancy. This closes the
    authority path end to end:
    
      pipeline (build) -> Outcome.trade_expectancy -> flow_execution
      (decide_admission) -> AdmissionReasonCode on CDV/funnel.
    
    Live L2 top-of-book notional (bid/ask_notional_quote_top) is not
    currently plumbed through CurrentRunMarketRow. Intentionally left
    as None so the slippage specialist returns Unresolved and the
    contract fails closed with EconomicsUnresolvedSlippage. Plumbing
    the top notionals into the pipeline state row is a follow-up
    slice; the contract design treats missing live depth as
    unresolved, not as a silent default.
    
    With this commit, every flow_execute_single_candidate invocation
    reads the contract as the sole economics authority. Legacy hard
    blockers (readiness_gate, edge_floor, conflict_lane, execution_
    mandate) still run for observability/pre-filtering but no longer
    set the final reason_code on the admission path.
  103. 2b30d04
    feat(execution): flow_execution decide_admission as sole authority
    Show commit body
    Adds decide_admission() to flow_execution and wires it as the
    canonical economics gate on the flow path. The function reads the
    TradeExpectancyResolution attached to the outcome and returns an
    AdmissionReasonCode:
    
    - None / SafetyViolation / Unresolved / BelowFloor => skip with
      the precise enum reason; pipeline-side blocker strings are no
      longer the authority on the flow path.
    - Resolved with net >= floor => AdmissionOk; proceed to realism +
      submit path.
    
    Wired early in flow_execute_single_candidate (after MSP + safety
    state, before expensive tape/L2 queries). Emits a
    FLOW_ADMISSION_BLOCKED funnel event carrying reason_code, detail,
    net_bps, floor_bps for the decision_trace_id.
    
    NOTE: strategy_pipeline does not yet attach the contract to
    outcomes, so in steady state every flow candidate currently fails
    with EconomicsUnresolvedFloor/no_contract_attached. The next
    commit wires the pipeline-side build + attach so resolutions
    flow end to end.
  104. c0b4a3d
    feat(economics): trade expectancy pipeline adapter + outcome field
    Show commit body
    Adds an adapter that translates primitive live pipeline inputs
    (fees, spread, L2/L3 micro, route horizon + expected move, strategy
    name) into TradeExpectancy BuildInputs and returns a sealed
    TradeExpectancyResolution.
    
    - New module trading::trade_expectancy_pipeline_adapter with
      PipelineContractInputs that only carries primitives, so callers
      in strategy_pipeline.rs can adapt from existing state without
      pulling pipeline types into the trading crate.
    - Outcome gains trade_expectancy: Option<TradeExpectancyResolution>
      and with_trade_expectancy builder. This is the channel through
      which pipeline hands the resolution to flow_execution.
    - Slippage + fill probability use specialist Resolution APIs added
      in the previous foundation commit; any Unresolved from those
      collapses the contract to Unresolved with the precise field.
    - trade_floor_bps is resolved from the DB table only; a missing row
      stays Unresolved — still no hardcoded default.
  105. 9342760
    feat(economics): specialist resolution APIs (slippage, fill_probability, route_cost)
    Show commit body
    Adds admission-layer Resolution-returning entry points alongside existing
    functions:
    
    - slippage_estimator::estimate_slippage_resolution — Resolved only with
      L2 depth (and L3 queue signals for maker). No fallback promotion.
    - fill_probability::fill_probability_resolution — Resolved only for the
      L3Based path; Hybrid/Fallback return Unresolved so the builder fails
      closed with EconomicsUnresolvedFillProbability.
    - cost_model::compute_route_cost_resolution — Resolved only when fees
      are live (RouteCostProvenance.fees_are_live) AND spread is live and
      positive; otherwise Unresolved{Fees|Spread}.
    
    All additions are currently unused in the codebase (dead_code allow) and
    will be consumed by the trade expectancy builder in the next commit. No
    existing callers were modified; behaviour of legacy paths is unchanged.
  106. c8e52aa
    feat(economics): pure trade expectancy contract, builder, floor resolver, reason codes
    Show commit body
    Adds the four new pure modules that define the canonical admission contract:
    
    - trading::trade_expectancy — sealed contract types, provenance, unresolved
      field taxonomy, safety kinds. ResolutionSource is intentionally two-valued
      (Explicit | Annotated); Fallback does not exist by design.
    - trading::trade_expectancy_builder — sole constructor. Any Annotated decision
      input collapses to Unresolved(<field>); SafetyKind short-circuits to
      SafetyViolation; Resolved contracts are sealed (all decision inputs
      Explicit). No fallback defaults.
    - trading::trade_floor — DB-backed (trade_expectancy_floors) resolver,
      Option<f64>, no code default. Missing row => UnresolvedField::Floor at the
      builder layer.
    - trading::admission_reason_code — canonical AdmissionReasonCode enum.
      Setter-side mappers from SafetyKind / UnresolvedField enforce exactly one
      reason per admission.
    
    All modules are additive; no existing code consumes them yet. Unit tests
    cover contract invariants (resolved-is-explicit, annotated collapses,
    safety-short-circuits, reason-code mappings).
  107. 77a907c
    feat(economics): add trade expectancy contract migration + floors seed
    Show commit body
    Introduces the sealed admission contract's persistence layer:
    - CDV columns (te_*) for contract fields, resolution status, provenance.
    - trade_expectancy_floors table: required SSOT for trade_floor_bps with no
      hardcoded code default. Seed covers every ActivatedStrategy enum variant
      plus legacy SelectedStrategy umbrellas so live traffic cannot produce an
      unnecessary EconomicsUnresolvedFloor at startup.
    
    Pure migration; no code consumes these yet (follow-up commits).
  108. bcb501b
    fix(website): use PM2-recognized ecosystem.config.cjs
    Show commit body
    PM2 only loads ecosystem.config.{js,cjs,mjs,...}; pm2.ecosystem.cjs was
    started as a plain script (wrong process name, no listener on 3001).
    Remove legacy name, delete stray pm2.ecosystem process in deploy script.
  109. 301e3a5
    fix(runtime): align strategy truth in fallback and gate admission/exit relaxations
    Show commit body
    Make decision fallback and Tier2 payload strategy-first so board labels match runtime admission truth, and gate high-conviction admission/realism/exit adjustments behind explicit feature flags to keep guardrails auditable during controlled rollout.
  110. 1de9b98
    chore(website): add deploy script, PM2 ecosystem, and SSOT docs
    Show commit body
    Stage server deploy path (clone → runtime), PM2 env defaults, and
    update AGENTS/.gitignore/Cursor rule so the monorepo mirror is not
    referenced as a code location.
  111. 30ddb90
    chore(website): remove monorepo KapitaalBot-Website mirror
    Show commit body
    Remove the embedded Next.js tree to avoid deploy drift; SSOT remains
    Davelaar/KapitaalBot-Website with server deploy via scripts. Update
    AGENTS.md, .gitignore, and Cursor rule globs accordingly.
  112. 002ce2b
    docs(website): OBSERVABILITY_EXPORT_DIR required for standalone snapshot APIs
  113. 3e1efb6
    fix(website): add snapshot_age_ms to Tier2Bundle edgeboard type (build)
  114. c60b28f
    fix(observability): make decision fallback rank by actionable positive edge
    Show commit body
    Rank decision fallback rows by net edge and confidence (not recency-first) and exclude non-positive net-edge rows so the live route board no longer surfaces fresh-but-non-actionable symbols as top candidates.
  115. 15bf98b
    fix(observability): stamp tier2 export at completion and harden stale snapshot detection
    Show commit body
    Set tier2 bundle exported_at at the end of bundle assembly and treat research snapshots as stale on both delay_until and snapshot age so the live board no longer appears fresh while serving old research snapshots.
  116. 47b0291
    fix(observability): fail closed on stale edgeboard and stamp tier2 export at write time
    Show commit body
    Stop serving stale research edgeboard rows when decision fallback is unavailable, and set tier2_data_bundle exported_at at bundle build time so dashboard recency reflects the actual file write rather than export start.
  117. 9816cfb
    fix(observability): correct edgeboard confidence units and live freshness age
    Show commit body
    Use volatility-scale variance penalties when computing edgeboard confidence and reason codes, and export freshness as current age from snapshot time so stale snapshots no longer look artificially fresh on the dashboard.
  118. a8c458c
    fix(execution): pause quiet/msp symbols to prevent heap starvation
    Show commit body
    Treat quiet-mode and MSP-ineligible symbols as temporary per-symbol halts so a single blocked market cannot monopolize flow cycles and suppress other entry opportunities.
  119. 753e641
    fix(observability): keep live route board fallback fresh
    Show commit body
    Limit decision-fallback edgeboard rows to recent admissions and rank by recency first, then expose fallback-derived snapshot age so stale state is visible in Tier2 UI.
  120. f89d4ac
    fix(execution): stop tagging live flow as validation
    Show commit body
    Use `pipeline_allow` for live flow execution intents so funnel and decision logs no longer mark executable orders as `live_validation`.
  121. 8640144
    fix(website): dashboard page dynamic import without ssr:false (matches /status)
  122. 87ffaa1
    fix(website): rename next/dynamic import to avoid clash with route segment config
  123. 7bb2196
    fix(website): /dashboard uses live HomeDashboard, tighten health API cache
    Show commit body
    - Add /[lang]/dashboard page (force-dynamic) so route beats CMS slug markdown.
    - Block CMS slug collision: add dashboard to KNOWN_ROUTES.
    - Redirect /dashboard -> /nl/dashboard.
    - health/snapshot + health/chart: Cache-Control private no-store.
    - HomeDashboard: fetch with cache no-store for snapshot, chart, audit, ready.
  124. 6171099
    fix(observability): export uses run_symbol_state sums for raw counts
    Show commit body
    run_raw_counts COUNT(*) on l2_snap_metrics can exceed the observability
    role 300s statement_timeout on large runs. Use run_raw_counts_from_state
    (SUM from run_symbol_state) for export and tier2_data_bundle, aligned
    with ingest hot path after refresh.
  125. 26c8901
    feat(website): poll Tier2 bundle and status every minute
    Show commit body
    - Tier2OperationalBoard: refetch tier2-bundle every 60s with cache no-store
    - Status Tier2 page: refetch /api/status/tier2 every 60s for generated_at_ts
    - tier2-bundle API: Cache-Control private no-store for fresh exports
  126. a48f2d7
    fix: enforce 60s entry lifetime and unblock flow starvation
    Show commit body
    Apply hard 60s entry TTL constraints, cancel timed-out open entries, and prevent symbol-lock recycle starvation so one blocked symbol cannot suppress other candidates. Add a continuation structural move floor to reduce pathological move_below_fees filtering on high-conviction directional setups.
  127. 804435b
    fix: query observation_runs on ingest pool (not decision)
    Show commit body
    observation_runs does not exist on the decision database (physical
    separation). The shadow_trades/event buffer queries use the execution
    run_id, which is also stored on the ingest pool's observation_runs.
    This was causing the export to crash with "relation does not exist".
  128. ba85b2d
    perf: use run_symbol_state.updated_at for feed freshness
    Show commit body
    Replace expensive MAX(ts_local) scans on raw sample tables (ticker,
    trade, l2_snap_metrics, l3_queue_metrics — millions of rows each)
    with a lightweight MAX(updated_at) from run_symbol_state which is
    already maintained as an aggregate. The l2_snap_metrics MAX alone
    was blocking the export for >5 minutes.
  129. 4341f65
    perf: use flat-set freshness queries instead of cross-join pattern
    Show commit body
    The first version used JOIN ... ON run_id IN (dr.exec_id, dr.data_id)
    which degenerates into a cross-join against large sample tables.
    Rewrite to: collect all needed run_ids into a flat set (all_ids),
    scan each sample table once with WHERE run_id IN (SELECT rid FROM
    all_ids), then LEFT JOIN the small result back to data_run.
  130. 817fa09
    fix: health timeline shows L2/L3 data from linked ingest run
    Show commit body
    execution_live runs use book-only L2 (no DB writes); all L2/L3 sample
    data lives under the ingest run_id. The health timeline query now
    resolves the linked ingest run via ingest_epochs.execution_run_id and
    includes that run's run_symbol_state + sample freshness, so the
    dashboard correctly shows non-zero L2/L3 counts and feed freshness.
  131. ca0c622
    feat: integrate drift (bps/min) into all move thesis families
    Show commit body
    Previously only momentum.rs and breakout.rs used drift_metrics from
    MarketFeatures. All other families computed expected_move blind to
    directional price momentum, systematically undervaluing high-drift
    opportunities and overvaluing counter-trend trades during trends.
    
    Per-family drift integration (economically correct direction):
    - pullback: aligned drift boosts continuation thesis (up to +20%)
    - fade: aligned drift REDUCES reversal capture (up to -40%)
    - mean_reversion: aligned drift REDUCES reversion expectation (-35%)
    - vwap_reversion: same pattern as mean_reversion (-30%)
    - market_making: drift magnitude increases adverse selection penalty
    - ignition_momentum: same uplift as momentum family
    - atr_breakout: same uplift as momentum family
    
    route_expectancy: drift floor now applies to ALL PullbackContinuation
    (was excluding maker entry; maker pullback benefits from drift too).
    
    All 425 tests pass, 0 failures.
  132. a2d43c1
    fix: resolve all 6 pre-existing test failures
    Show commit body
    - vwap_buffer: serialize env-mutating tests with shared Mutex to
      prevent parallel set_var race; use unique symbols per test
    - position_reconcile: replace module-local test lock with shared
      exchange_cache_test_lock from balance_cache; use unwrap_or_else
      to recover from poison instead of cascading PoisonError
    - protection_flow: add exchange_cache_test_lock guard to
      remediation test that was mutating shared caches without
      coordination with position_reconcile tests
    - own_orders_cache: freshness_tests now use the same shared lock
    
    Test suite: 425 passed, 0 failed (was 419 passed, 6 failed).
  133. 5714153
    fix: close 4 decoupled gates blocking high-conviction taker trades
    Show commit body
    - entry_route: relax taker spread cap to 200 bps (breakout) / 150 bps
      (momentum_ride) when activation_score >= 80, was hard 60 bps
    - mandate: skip spread-vs-stop and negative-edge gates for HC taker
      activations (breakout/momentum_ride with score >= 80)
    - execution_realism: propagate relaxed spread_cap_bps (200/150) for HC
      taker routes instead of config default 60
    - strategy_pipeline: bypass edge_floor block for HC taker activations
    
    These gates contradicted the readiness bypass from 3d09720, silently
    re-blocking wide-spread breakout/momentum_ride signals downstream.
  134. 844525a
    fix(website): Tier2 Why-No tables and Live/Position always stacked
    Show commit body
    - Replace auto-fit multi-column breakdown grid with vertical flex stack
      to avoid squeezed columns on narrow flex children.
    - Stack Live Route and Position Context vertically (no lg:grid-cols-2).
    - Bump data-tier2-board marker to v3-full-width-stacked-tables.
  135. 3d09720
    feat: momentum_ride readiness bypass + activation-aware sizing
    Show commit body
    Fix 1 — Extend high-conviction taker bypass to MomentumRide:
    - drift.rs: add MOMENTUM_RIDE_DRIFT_BLEND_FACTOR (0.45) and
      momentum_ride_drift_aware_move_floor
    - strategy_readiness_report: recompute blended move for MomentumRide
      (0.45 blend vs default 0.30), pass BreakoutTakerContext with
      spread_cap_bps=150 for momentum_ride (vs 200 for breakout)
    - readiness_gate: BreakoutTakerContext gains spread_cap_bps field,
      evaluate_pair_readiness uses per-context cap instead of hardcoded
      BREAKOUT_SPREAD_ANCHOR_MAX_BPS_F
    
    Proven failure mode: INIT/USD 2026-04-15 activation 10,016, drift
    3,851 bps/min, blocked by SurplusBelowFloor despite significant move.
    Dozens of high-conviction momentum_ride signals similarly blocked.
    
    Fix 2 — Thread activation_score into sizing:
    - outcome.rs: add activation_score field + builder
    - strategy_pipeline.rs: propagate activation_score from V2 pipeline
    - capital_allocator.rs: add activation_score to SizingRequest,
      logarithmic conviction multiplier (score >200 → up to 1.5x)
    - flow_execution.rs / runner.rs: pass activation_score to allocator
    
    This closes the gap where the detection engine generated strong signals
    (activation scores 500–20,000) but the sizing engine was unaware.
  136. 48358f1
    fix(website): Tier2 Why-No always vertical stack + minmax grid for breakdowns
    Show commit body
    - Remove lg:flex-row on pie vs top redenen (no squeezed middle column)
    - Funnel tables: auto-fit minmax(260px,1fr) for readable columns
    - data-tier2-board marker for verifying deployed bundle
  137. a55086a
    docs: site Git repo as SSOT; KapitaalBot-Website as monorepo mirror
    Show commit body
    Align WEBSITE_SOURCE_OF_TRUTH and AGENTS with dedicated website repository
    for version control and deploy; clarify two-tree avoidance.
  138. 3ccd959
    feat: breakout taker readiness bypass for wide-spread microcaps
    Show commit body
    The readiness gate structurally blocked breakout taker entries on
    wide-spread microcaps (40-150 bps spread) because:
    1. The generic Momentum expected-move formula (~42 bps) was too
       conservative to cover taker costs (~100 bps)
    2. The spread cap (80 bps) blocked even when the breakout thesis
       predicted 500+ bps of move
    
    Proven failure: ENJ/USD 2026-04-15 — activation score 537, drift
    172 bps/min, +28% move captured by nobody because every breakout
    evaluation was killed by readiness_SurplusBelowFloor and
    readiness_SpreadTooWide.
    
    Changes:
    - drift.rs: add breakout_drift_aware_move_floor (blend factor 0.50
      vs generic 0.30), capturing more of the drift-implied move for
      breakout economics
    - readiness_gate.rs: add BreakoutTakerContext with relaxed spread
      cap (200 bps) for breakout activations with score >= 80
    - strategy_readiness_report.rs: when activation is
      BreakoutImmediate/BreakoutConfirmed, recompute economics with
      the higher drift blend and pass breakout taker context to the
      readiness gate
    
    Guardrails preserved: surplus floor unchanged (-25 bps), position
    sizing unchanged, dead man's switch unchanged, all safety gates
    unchanged. Only the move estimate and spread cap are relaxed for
    high-conviction breakout activations.
  139. 83416cf
    fix(safety): auto-clear stale needs_protection exit_only rows
    Show commit body
    Clear stale `exit_only` rows with `needs_protection` only after a minimum age and only when position exposure is flat with no open entry orders. This removes historical safety residue without relaxing active protection constraints.
  140. e19fc75
    fix(website): single Why-No-Trade card, tables label-first, no nested card chrome
    Show commit body
    - One bordered section for pie + top redenen + funnel/decision/redenen/shadow
    - Breakdowns as tables (Label | Aantal | %) so counts are never label-less
    - Entry-blocked table moved below as separate card; removed inner bordered panel
  141. 51001e6
    fix(safety): clear legacy reasonless exit_only blockers on startup
    Show commit body
    Normalize stale legacy `exit_only` rows that have no blocker reason only when there is no non-zero position and no open entry order. This removes sticky safety leftovers without weakening active protection paths.
  142. 379ebf5
    fix(website): Tier2 Why-No-Trade full width, route/context row below
    Show commit body
    - Stack Why-No-Trade section first; Live Route + Position Context in 2-col grid under it
    - Funnel columns use responsive grid (1/2/4 cols) instead of squeezed inline-flex
    - Dashboard page container max-w-none for full content width
  143. a5808db
    feat(watchdog): alert on no-order throughput collapse with blockers
    Show commit body
    Add a no-order watchdog signal that detects sustained signal/admission flow without any buy orders or fills in a rolling window, and persist top admission blocker reason in watchdog snapshots. This keeps safety intact while surfacing strategy-gating bottlenecks early.
  144. f57683e
    fix(safety): auto-clear stale pinned_invalid_l3 exit_only rows
    Show commit body
    Clear stale `exit_only + pinned_invalid_l3` safety rows at live startup only when latest ingest shows L3 activity and decision-side exposure is flat with no open entry orders. This preserves safety for active risk while removing sticky blockers that can suppress valid entries after recovery.
  145. c20fd48
    fix(website): Tier 2 Data dashboard layout + entry-blocked table
    Show commit body
    - Add /[lang]/dashboard/tier2 with Tier2OperationalBoard (scroll-safe columns)
    - API GET /api/data/tier2-bundle (tier2/admin cookie) reads tier2_data_bundle.json
    - Surface risk_capital.entry_blocked_recent in a readable table
    - Nav link Data (Tier 2) for tier2 and admin; redirect /dashboard/tier2 -> /nl/dashboard/tier2
  146. c40b53b
    fix(kraken): enforce add_order deadline RFC3339 offset contract
    Show commit body
    Apply Kraken WS v2 add_order deadline semantics in one canonical builder with hard 500ms-60s clamp and 5s default, and use it consistently for market and limit add_order paths (including OTO). Keep longer local intent TTL behavior client-side while guaranteeing wire deadlines stay within exchange-accepted bounds.
  147. 65a017e
    test(execution): regression guard against legacy fixed MTP stop_loss bps
  148. 45fc347
    feat(execution): data-driven static SL/TP for path tape (vol + spread)
    Show commit body
    Replace fixed per-class stop_loss_bps/take_profit_bps/panic_pnl_bps overrides
    with measured_static_stop_magnitude_bps using vol_at_hold_bps (Ignition),
    live spread_bps, and expected_move_bps. Mild class multipliers only tilt
    the composite, not standalone bps constants.
    
    Thread spread_bps into exit_config_for_path_tape; log vol and spread on
    PATH_TAPE_EXIT_ROUTING. Add unit tests for monotonicity and class ordering.
  149. 19df9b8
    fix(kraken): buffer WS add_order deadline offset to avoid EGeneral:deadline
    Show commit body
    Kraken validates deadline as 0.5–60s after *their* receive time. An absolute
    RFC3331 built from local Utc::now()+60s can exceed 60s at the exchange when the
    host clock runs ahead of Kraken, causing mass ENTRY rejections.
    
    Default subtract 12s (env KRAKEN_ADD_ORDER_DEADLINE_BUFFER_SECS); clamp effective
    offset to [5, 59] seconds.
  150. bfaed4b
    feat(observability): export per-symbol safety reasons and entry-block scopes
    Show commit body
    - public_status_snapshot.json + tier2_data_bundle.risk_capital: safety_symbol_blockers
      from symbol_safety_state (mode, primary_reason, quiet/hard windows); scope
      is always this_symbol_only for these rows.
    - entry_blocked_recent: deduped ENTRY_BLOCKED% funnel events (24h) with
      scope hint (all_symbols_new_entries vs single-symbol paths).
    - CONTRACT_VERSION 1.3
  151. 0599f92
    fix(export): align status/market snapshots with epoch-bound ingest run_id
    Show commit body
    Public status counts and freshness already used `status_run_id` (epoch_run_id
    or freshest ingest). Top-level `run_id`, run metadata, and market pair summaries
    still used freshest ingest only, which can diverge from the epoch-bound lineage
    and look like a data mismatch on the website.
    
    Shadow/event-buffer queries on the decision pool now use latest decision
    observation run_id instead of the ingest run_id.
  152. 5b0d740
    docs(observability): enforce ingest run_id contract for raw counts and tier2
    Show commit body
    - Rename per_symbol_counts_rolling_window_secs param to ingest_market_run_id and
      document split-architecture misuse (execution run_id → false epoch/UI).
    - Document build_tier2_data_bundle run_id and export status_run_id as ingest-only.
  153. d49dad2
    fix(epoch+export): align ticker/trade criterion and snapshot freshness anchor
    Show commit body
    Two run_id mismatch fixes:
    
    1. Epoch ticker criterion: symbols with few Kraken ticker updates but
       active trades (>= 10) were failing the ticker criterion, degrading
       the epoch. Now accepts ticker >= 10 OR trade >= 10 for both
       criteria, consistent with the universe activity gate.
    
    2. Export decision fallback: research snapshot staleness was computed
       from snapshot_ts (creation time), but the 5-minute delay_until
       window consumed most of the 600s max_age budget. By the time the
       snapshot became visible + the export ran, it was already >600s from
       creation. Now uses delay_until as the freshness anchor so the clock
       starts when the snapshot is actually released, not when created.
  154. 4be3602
    chore(config): deprecate L3_ENABLE flag (L3 ingest runs independently)
    Show commit body
    L3_ENABLE only gates the legacy L3 probe path, not the L3 ingest feed
    which provides l3_queue_metrics, queue_ahead_notional, and L3 quality
    scores. Marking deprecated so it's clear this flag has no effect on
    L3 data availability or execution quality.
  155. 19f7764
    feat(config): activate per-family edge lifetime, VWAP live feed, flow parallel
    Show commit body
    ENABLE_PER_FAMILY_EDGE_LIFETIME: IgnitionMomentum heap TTL 15s,
    Breakout 60s instead of global 240s default. Stale breakout
    opportunities no longer linger in the heap competing with fresh signals.
    
    ENABLE_VWAP_LIVE_FEED: real VWAP deviation replaces microprice proxy
    for the vwap_reversion thesis. More accurate mean-reversion signal.
    
    FLOW_PARALLEL_ENABLE: concurrent flow execution drains. With the
    expanded universe (76+ symbols) and more tradable candidates, sequential
    processing becomes a bottleneck for time-sensitive breakout entries.
  156. 55d812d
    feat(config): activate ENABLE_ROUTE_STATE_STORE and CONSISTENCY_WATCHDOG by default
    Show commit body
    ENABLE_ROUTE_STATE_STORE (C: proven limiting): without this, the
    position_monitor gets no route linkage advice, and trail tightening /
    defensive exit paths from route management are skipped. Critical for
    breakout/momentum execution quality.
    
    CONSISTENCY_WATCHDOG_ENABLED: operational safety net that detects
    WS/ingest staleness and can trigger soft reconnects. Prevents the
    data_stale cascade seen in the 04:35 UTC incident.
  157. 7fb4779
    fix(ignition): reduce hysteresis and candle threshold for faster detection
    Show commit body
    Ignition FSM was too slow to detect massive movers:
    - min_state_duration: 120s -> 30s (2 min hold prevented fast transitions)
    - transition_cooldown: 60s -> 15s (1 min gap between state changes)
    - confidence_floor: 0.6 -> 0.5 (strong signals were rejected)
    - insufficient candle threshold: 10 -> 3 (10 min cold start was too long)
    
    With the old settings, a coin pumping +87% would stay classified as
    Quiet for up to 2 minutes after enough candles accumulated, then wait
    another 60s for cooldown. With the new settings, detection can happen
    within ~30s of signal onset with 3 minutes of candle history.
  158. 836474d
    fix(regime): recalibrate trade_density scale and thresholds
    Show commit body
    trade_density was divided by 5.0 before feeding detect_regime, making
    the 0.5/2.0 thresholds unreachable (raw values 0.01-0.19, normalized
    0.002-0.038). Result: 59/64 symbols classified MeanReversion, blocking
    breakout/momentum routes entirely.
    
    Changes:
    - Remove /5.0 divisor from all 5 RegimeMetrics construction sites
    - Recalibrate detect_regime thresholds to raw trades/second scale:
      trade_density_high: 2.0 -> 0.15, trade_density_low: 0.5 -> 0.01
    - Reorder regime priority: HIGH_VOLATILITY no longer requires
      !trade_density_high (was dead branch due to threshold)
    - Map LOW_LIQUIDITY -> SpreadFarming (not MeanReversion), so truly
      illiquid symbols get conservative passive routes instead of blocking
      breakout for the 99% that aren't actually illiquid
  159. 3468773
    fix(universe): broaden execution eligibility to ticker OR trade activity + L2
    Show commit body
    The ticker-only gate (ticker_count >= 10) excluded ~184 actively-traded
    symbols that had full L2/L3 data and 100+ trades but sparse ticker
    updates (Kraken ticker WS only fires on quote change). This blocked
    massive movers like TERM/USD (+135%) and SAROS/USD (+93%) from the
    execution universe entirely.
    
    New gate: (ticker >= 10 OR trade >= 10) AND l2 >= 50. Data analysis
    confirmed: all 184 added symbols have avg 146 trades and >116K L2 rows;
    zero junk/spam symbols pass the combined gate.
  160. 23136f6
    fix(reconcile): add orphaned exit order cleanup for parent-terminal cases
    Show commit body
    When the exit_lifecycle monitor loop crashes or the trailing stop fills
    but the TP cancel fails (WS race), exit orders (exit_tp_maker,
    exit_sl_static, oto_conditional_trailing_stop) can persist in acked_open
    status while their parent entry order is already terminal. These orphans
    hold the per-symbol execution lock indefinitely.
    
    New `cancel_orphaned_exit_orders` function queries exit orders whose
    parent_order_id references an entry in terminal state (filled/canceled/
    rejected/expired), sends cancel to exchange if still open, and marks
    them cancel_requested in DB.
    
    Wired into startup cleanup and every evaluation cycle in both the
    primary live loop and exec-only loop.
  161. e762953
    feat(ingest): add public WS feed liveness tracking and reconnect observability
    Show commit body
    Both ticker and trade WS feeds now maintain atomic last-message timestamps
    and cumulative reconnect counters. Backoff resets on clean stream end
    (previously stayed at max backoff after any disconnect).
    
    New public API: ticker_feed_age_secs(), trade_feed_age_secs(),
    ticker_reconnect_count(), trade_reconnect_count() — available for
    watchdog, integrity matrix, and pushover alerting.
    
    DATA_INTEGRITY_MATRIX log now includes ticker/trade feed age and
    reconnect counts for immediate visibility into public WS health.
  162. 377e114
    fix(slippage): replace order_size_quote=1.0 placeholder with representative estimate
    Show commit body
    The slippage model used order_size_quote=1.0 (1 USD) for market-impact
    calculations at all pre-sizing call sites. With real L2 depth of e.g.
    $5000, the impact ratio (1/5000) was negligible, systematically
    underestimating taker slippage by ~100-150 bps.
    
    Adds configurable `SLIPPAGE_ESTIMATE_ORDER_SIZE_QUOTE` (default 150 USD)
    used at all 6 pre-sizing call sites (slippage estimator, readiness gate,
    route expectancy, capturable move). The actual allocation comes later
    from the capital allocator, but this representative value gives realistic
    market-impact estimates for edge ranking and readiness decisions.
  163. f1e97e4
    fix(exit_lifecycle): add Kraken deadline to TP maker limit orders
    Show commit body
    TP maker limits were placed without a Kraken `deadline`, making them
    effectively GTC on the exchange. If the process crashes or restarts
    while exit_lifecycle is monitoring, these orders persist indefinitely,
    hold the per-symbol execution lock (`acked_open` in coverage statuses),
    and block new entries on that symbol.
    
    All three TP limit placement sites now set `entry_limit_deadline=true`,
    which adds a 60s Kraken-side deadline. The internal MAKER_TP_FILL_TIMEOUT
    (30s) handles normal flow; the exchange deadline is a crash safety net.
  164. 0fbded8
    fix(website): dashboard data layout — min-w-0 grids, chart shell, fewer squeezed columns
    Show commit body
    - HomeDashboard: full-width root; status + throughput grids min-w-0; chart header stacks on small widths; trades block 2 cols until 2xl (was 4 on xl); StatusCard/ingest/tiles min-w-0; blocking text max-w-full
    - ThroughputChart: fixed-height wrapper so ResponsiveContainer gets stable dimensions
    - HomeLanding live embed: min-w-0 overflow-hidden around HomeDashboard
    - TradingDashboard + tier2 extended cards: min-w-0 on grids/cards
  165. a7f9fd7
    docs(website): restore WORKSPACE_MIRROR.md in Krakenbot mirror
  166. d7a430c
    fix(website): /over hub routes, CMS pages, and full-width layout
    Show commit body
    - Add /[lang]/over, /over/wat-is-kapitaalbot, /over/fundme (reuses FundMe page)
    - CMS markdown: over-*, wat-is-kapitaalbot-*; register slugs in KNOWN_ROUTES
    - Nav: explicit Over-hub links; avoid duplicate CMS entries
    - Layout: w-full min-w-0 max-w-none on CMS, FundMe, tier2 shell to match main column width
  167. fdfea86
    chore(website): vendored live Next app under KapitaalBot-Website; archive standalone
    Show commit body
    - Replace nested KapitaalBot-Website git clone with rsync mirror of KapitaalBot/website (live PM2 source)
    
    - Remove ignore of whole folder; ignore node_modules/.next/env under KapitaalBot-Website
    
    - Add backups/*.tar.gz and *.bundle to gitignore; backups on disk for rollback
    
    - Docs: docs/WEBSITE_SOURCE_OF_TRUTH.md; AGENTS + cursor rule updated
    
    Note: production still deploys KapitaalBot/website until server script migrates.
  168. cd7bfe8
    fix(kraken-ws): cap add_order deadline offset at Kraken max 60s
    Show commit body
    ENTRY_LIMIT_TTL_SECS default 90 exceeded WS v2 deadline window (500ms–60s),
    causing EGeneral:Invalid arguments:deadline on limit+entry_limit_deadline.
    Wire offset uses min(TTL, 60). Document messages outbound row in f64 inventory.
  169. fc7af16
    fix(execution): wire PreparedOrderSubmit qty/limit as Decimal to Kraken WS
    Show commit body
    Removes f64 round-trip before add_order; keeps instrument quantization
    for JSON number_float and aligns DB quantity_base with wire bytes.
  170. e1f5587
    fix(kraken-ws): omit market add_order deadline to avoid exchange clock skew rejects
    Show commit body
    Kraken validates deadline as 500ms–60s offset from engine receive time.
    Client-computed Utc::now()+5s can be in the past if the host clock lags
    exchange time, causing EGeneral:Invalid arguments:deadline. Omitted
    deadline uses Kraken default (5s from their receipt) per WS v2 docs.
  171. 09dc482
    fix: align net edge with dynamic slippage; Kraken WS qty as JSON float
    Show commit body
    - route_expectancy: subtract entry_slip/exit_feas slips in expected_net_edge_bps;
      keep fees + spread_impact from compute_route_cost only.
    - messages: ser_kraken_float serializes Decimal order_qty/limit/trigger as f64 JSON
      numbers (Kraken rejects string / non-number_float).
    - Unit test: AddOrderParams serializes order_qty and limit_price as JSON numbers.
  172. b1a9d87
    fix(route): breakout vol expansion uses ignition ratios + trend fallback
    Show commit body
    - Add ignition_expansion_boost: vol_acceleration, range_expansion, density_shift
      (scale-free) additively to vol_5m/spread so wide-spread alts are not zeroed.
    - Pre-ignition: small trend boost when vol/spread is crushed but micro trend is strong.
    - Breakout direction: when micro is flat at 0 bps, use signed trend_strength
      (Ignition persistence) for Up/Down instead of Neutral only.
    - Repair test MarketFeatures/CurrentRunMarketRow fixtures (avg_queue_ahead_notional_bid),
      FromPrimitive in position_reconcile tests, remove unused mut in route_expectancy test.
  173. 78ed218
    feat(slippage): replace static model with dynamic L2/L3-based slippage estimator
    Show commit body
    Previous model: base + vol*0.3 + spread*0.05, clamp 0-50 bps, exit = entry*1.1.
    Result: avg model 75 bps vs 3 bps realized. 35% of fills capped at 105 bps.
    
    New dynamic model (estimate_slippage_dynamic):
    - Maker entry: base 0.5 bps + composite queue-risk (cancel_rate /
      (turnover + 1) * queue_scale) + L2 market-impact (order_size / depth
      * spread * 0.3). Soft cap at spread/2 * 1.5 (microstructure cap, not
      physical limit), hard cap 15 bps.
    - Taker entry: L2 ask-side market-impact (order_size / ask_depth *
      spread * 0.5) + vol * 0.05. Hard cap 25 bps.
    - Fallback when no L2/L3: existing static model with reduced caps
      (maker 10, taker 20 bps).
    
    Queue-risk is a composite signal:
    - cancel_rate / (turnover + 1) = instability score
      (high cancel + low turnover = unstable; high cancel + high turnover =
      active market, self-healing = lower penalty)
    - queue_ahead_notional / order_size = queue scale amplifier
    - order_size_quote = 1.0 normalized placeholder (pipeline can rescale
      linearly when real allocation is known)
    
    Exit slippage now strategy-differentiated:
    - Maker exit (Liquidity/Volume): entry * 0.5 (planned exit, minimal)
    - Taker exit (Momentum): entry * 1.0 (market order, comparable)
    
    Wiring:
    - route_expectancy.rs: L2 registry lookup at SlippageInputs construction
    - readiness_gate.rs: new SlippageL2L3Context struct, slippage_for_pair_with_context
    - strategy_readiness_report.rs: both slippage_for_pair call sites upgraded
      with L2 registry + L3 context
    - Legacy estimate_slippage / estimate_slippage_for_entry_mode retained
      with #[allow(dead_code)]
  174. fd68e9f
    fix(observability): fix slippage variance join via decision_trace_id
    Show commit body
    The fills↔CDV slippage koppeling liep via trading_funnel_events.decision_vector_id,
    maar die kolom bestaat niet in trading_funnel_events. Daardoor was de
    lateral join altijd leeg en kwamen er 0 steekproef-fills uit.
    
    Fix: join candidate_decision_vectors via execution_orders.decision_trace_id,
    wat wel gevuld is (169/169 orders in 24h). Beide queries bijgewerkt:
    slippage_variance_aggregate_24h en slippage_variance_examples_24h.
  175. 4e5b379
    feat(l3): implement queue_ahead_notional_bid end-to-end
    Show commit body
    Adds total notional at best-bid level (qty × price from L3 book) as a
    real queue-position proxy throughout the pipeline, replacing the
    hardcoded 0.0/None that made entry_filter, queue_decision and
    capturable_move queue-blind.
    
    Data path:
    - DB migration: l3_queue_metrics.queue_ahead_notional_bid NUMERIC,
      run_symbol_state.avg_queue_ahead_notional_bid NUMERIC
    - L3QueueMetricsRow: new field queue_ahead_notional_bid
    - writer.rs: INSERT includes queue_ahead_notional_bid (L3_INSERT_COLS 16→17)
    - run_symbol_state full-refresh + incremental: AVG(queue_ahead_notional_bid)
    - emit_metrics in l3_feed.rs: book.bid_notional_at_best() → row field
    - L3SymbolStats + both query functions: include avg_queue_ahead_notional_bid
    - RunSymbolStateRow: include avg_queue_ahead_notional_bid
    
    Consumer wiring:
    - CurrentRunMarketRow: new field avg_queue_ahead_notional_bid
    - analyze_run_from_state: filled from RunSymbolStateRow (state path)
      and from L3SymbolStats (partial/24h paths)
    - PartialDatasetRow: new field, filled from L3SymbolStats
    - MarketFeatures: new field avg_queue_ahead_notional_bid, set from row
    - market_features_from_row: passes row.avg_queue_ahead_notional_bid
    - strategy_pipeline (V1 + V2): queue_decision now uses L3 notional
      with L2 depth_near_mid as fallback
    - strategy_pipeline entry_filter: queue_ahead_notional = Some(qa)
      instead of None — Momentum 3.0 ratio now enforced
    - route_expectancy: CapturableMoveInputs uses features.avg_queue_ahead_notional_bid
    - strategy_readiness_report: all 3 CapturableMoveInputs sites use
      r/p.avg_queue_ahead_notional_bid
    - PairReadinessRow: new field propagated through all constructors
    
    Semantics: bid_notional_at_best = sum(qty) * best_bid_price at the
    best level — conservative upper bound; when queue is large relative
    to order size, the bot correctly improves price or blocks entry.
  176. bc05b95
    fix: exclude closed/flat positions from degraded truth lock
    Show commit body
    get_position_truth now filters on status != 'closed' AND
    base_position != 0. Previously, 19 symbols with balance_recovery
    origin remained permanently locked even after position was fully
    closed (base_position=0, status=closed) because stale degraded
    flags were never cleared. RAVE/USD was actively blocked ~1500
    times/minute despite having zero exposure.
  177. 8402cc7
    fix(decimal): P2 legacy cleanup — l2_feed, messages, margin_paper
    Show commit body
    l2_feed: qty zero-compare now uses Decimal::from_str_exact + is_zero()
    instead of parse::<f64>() + > 0.0 pattern. Eliminates redundant double
    parse.
    
    messages: AddOrderParams, TriggerParams, ConditionalParams, AmendOrderParams
    fields changed from f64 to Decimal. auth_ws dec_to_wire helper removed —
    Decimal values flow directly. Wire serialization uses Decimal's string
    representation (Kraken WS v2 compatible).
    
    margin_paper_trades: entry_price and exit_price changed to Decimal.
    SQL cast changed from double precision to numeric.
  178. c3e0eeb
    fix(decimal): migrate gates and pipeline thresholds to Decimal (P1)
    Show commit body
    readiness_gate: PairReadinessResult 6 fields and all gate threshold
    constants migrated to Decimal. Comparisons use Decimal arithmetic.
    
    tradability: spread_now_bps → Decimal with Decimal threshold.
    
    ignition/store: SymbolBuffer OHLC fields → Decimal. on_trade accepts
    Decimal price directly from trade feed.
    
    config/mod.rs: execution-critical config fields (equity_quote,
    spread caps, sizing thresholds) → Decimal. Analytics-only configs
    stay f64 with documentation.
    
    strategy_pipeline + route_engine: spread/size gate values compared
    as Decimal where they gate live orders.
  179. b6545c3
    fix(decimal): eliminate f64 from execution core internals (P1)
    Show commit body
    protection_flow: stop price, qty, notional calculations in Decimal.
    ProtectionOutcome payloads use Decimal. ~30 boundary conversions removed.
    
    position_monitor: MonitoredPosition 8 fields migrated to Decimal.
    Trail/SL/breakeven calculations in Decimal. normalize_price now Decimal.
    
    exit_lifecycle: ~55 boundary conversions eliminated. resolve_initial_exit_qty,
    qty tracking, fill price, SL/TP levels all Decimal internally.
    
    ignition_exit: 16 struct fields across ModeParams, IgnitionExitConfig,
    HybridMarketContext, HybridTargets migrated. ~25 dec_f sites removed.
    
    position_reconcile: exchange_base_total now Option<Decimal> directly
    from balance_cache. Internal qty comparisons in Decimal.
    
    exposure_reconcile: removed from_f64_retain round-trips for balance
    and price values already provided as Decimal upstream.
  180. 225636c
    fix(decimal): migrate trade_flow_window + spread_bps to Decimal (P1)
    Show commit body
    trade_flow_window.rs: price/qty params and volume accumulators now
    Decimal. record_trade_global accepts Decimal directly from trade feed.
    
    kraken_public.rs: spread_bps helper uses Decimal math end-to-end.
    Trade recording passes t.price/t.qty as Decimal (f64 retained only
    for vwap_buffer which is not yet migrated).
  181. 3f565c9
    fix(decimal): migrate price_cache PriceSnapshot to Decimal (P1)
    Show commit body
    PriceSnapshot fields (bid, ask, last, bid_qty, ask_qty) now Decimal.
    PRICE_HISTORY ring stores Decimal mid-prices. Mid-tick subscriber
    callbacks receive Decimal. Wire ingest from Kraken ticker passes
    Option<Decimal> directly instead of lossy to_f64() conversion.
    
    All 25 caller files updated with .to_f64() bridges where they still
    use f64 internally. balance_cache equity rollup now uses Decimal
    prices directly (no f64 intermediary).
  182. dc9a5aa
    fix(decimal): migrate horizon_movers ring to Decimal mid-price (P0)
    Show commit body
    HorizonWindow ring now stores mid-price as Decimal instead of f64.
    record_trade and sample_l2_mid accept Decimal. Derived analytics
    (realized_move_bps, vol_proxy) remain f64 — non-critical derived.
    
    live_runner.rs: computes L2 mid directly in Decimal from book snapshot
    instead of lossy to_string().parse::<f64>() round-trip.
    kraken_public.rs: passes t.price (already Decimal) directly.
  183. 43a401d
    fix(decimal): migrate sizing + capital_allocator outputs to Decimal (P0)
    Show commit body
    sizing.rs: all size/notional-returning functions now return Decimal.
    Internal sigmoid/exp/clamp math remains f64 (no Decimal equivalent),
    converted to Decimal at output boundary.
    
    capital_allocator.rs: equity_quote, notional_quote, and SlotEntry
    storage migrated to Decimal. Config ratios and analytics inputs
    (edge_bps, confidence, spread_bps) stay f64.
    
    All callers updated: strategy_pipeline, flow_execution, live_runner,
    runner, probes.
  184. 0ca06bd
    fix(decimal): migrate balance_cache from f64 to Decimal (P0)
    Show commit body
    BalanceEntry.balance, BalanceState.total_equity_usd and all public API
    return types changed from f64 to rust_decimal::Decimal. Wire ingest now
    parses Kraken balance strings directly to Decimal (with f64 fallback for
    non-string JSON values). Equity rollup uses Decimal arithmetic with a
    temporary f64 bridge for price_cache (migrated in Phase B1).
    
    All 14 caller files updated: modules already using Decimal internally
    consume values directly; execution modules still on f64 use .to_f64()
    bridges that will be removed when those modules migrate in Phase C.
  185. 2bfbe99
    fix(l3): migrate L3 serde and orderbook internals from f64 to Decimal
    Show commit body
    L3Order and L3OrderEvent: limit_price and order_qty now use Decimal
    via de_decimal::deserialize (same RawValue-based lossless deserializer
    used by the L2/ticker/execution serde boundary). This eliminates float
    precision loss on the L3 wire path, which is the P0 item in the
    DECIMAL_F64_POLICY_AND_INVENTORY.
    
    L3OrderBook internals:
    - PriceLevel: BTreeMap<String, f64> -> BTreeMap<String, Decimal>
    - OrderedPrice: scaling now uses Decimal arithmetic instead of f64*1e8
    - best_bid() / best_ask(): return Option<Decimal> instead of Option<f64>
    - bid_depth_at_best() / ask_depth_at_best(): return Decimal
    - Add bid_notional_at_best() / ask_notional_at_best(): Decimal notional
      (qty × price) at the best level, for the upcoming queue_ahead_notional
      feature
    
    QueueMetricsAccumulator: record_bid_add / record_ask_add now accept
    Decimal (from wire) and convert to f64 internally for the rolling-window
    rate arithmetic — documented as non-critical derived analytics.
    
    Callers updated:
    - l3_feed.rs: best_bid/best_ask already Option<Decimal>, remove
      redundant and_then(Decimal::from_f64_retain)
    - l3_probe.rs: no change needed (order_qty now Decimal, accepted by
      updated record_bid_add signature)
    
    Inventory: mark src/l3/messages.rs and src/l3/orderbook.rs as OK.
  186. 1e34647
    fix(pipeline): propagate pretrade metrics from v2 pipeline to execution CDVs
    Show commit body
    The v2 pipeline (run_strategy_pipeline_v2_inner) was missing
    .with_pretrade_metrics() when constructing Outcome objects, causing
    fill_probability, expected_surplus_bps, expected_entry_slippage_bps and
    expected_exit_slippage_bps to remain None. This propagated through the
    edge heap → flow_execution intent → runner CDV insert, leaving all
    execution_precheck candidate_decision_vectors with NULL slippage_bps_estimate
    (88/88 = 100% NULL in 24h). The dashboard "Slippage: verwacht vs. gerealiseerd"
    panel depends on this field for its fills → funnel → CDV join chain, producing
    0 matched samples.
  187. 87d6fca
    fix(l3_feed): route add/modify/delete events correctly to queue metrics accumulator
    Show commit body
    Previously every L3OrderEvent was dispatched as record_bid_add/record_ask_add
    regardless of the event type field, causing:
    - queue_turnover_bid/ask: ~3x overstated (counted modify+delete qty as new liquidity)
    - refill_rate_bid/ask: ~3x overstated (all events counted as order adds)
    - cancel_rate_bid/ask: always 0.0 (delete events never reached bid_deletes/ask_deletes)
    - trade_rate_bid/ask: always 0.0 (modify events never reached bid_modifies/ask_modifies)
    - estimated_fill_time_bid/ask_ms: always NULL (derived from trade_rate == 0)
    
    Fix: match on e.event.as_str() ("add"/"modify"/"delete") for both sides,
    mirroring the correct routing already present in src/probe/l3_probe.rs.
    
    All downstream aggregations (run_symbol_state, pair_summary_24h,
    training_ingest) read from l3_queue_metrics and will automatically
    reflect correct values after this fix.
  188. 3ec4409
    fix(observe): correct pair_summary_24h aggregation bugs
    Show commit body
    Two bugs in persist_pair_summary_24h:
    
    1. avg_trade_density was divided by a hardcoded 24*3600 s constant even
       though runs are typically ~1 hour, making the density ~24x too low.
       Now fetches actual run duration from observation_runs and divides by
       that (minimum 60 s to guard against near-zero on very short runs).
    
    2. avg_queue_turnover and avg_refill_rate were hardcoded NULL — l3_queue_metrics
       was never joined. Now aggregates AVG(queue_turnover_bid) and
       AVG(refill_rate_bid) per run+symbol from l3_queue_metrics.
       avg_est_fill_ms remains NULL because estimated_fill_time_bid_ms is
       structurally not computed by the L3 pipeline.
  189. 356ed50
    Batch L2 snap inserts to prevent bulk writer channel saturation
    Show commit body
    L2 snaps were the only write type using individual INSERT per row in the
    bulk writer. With 579 symbols producing L2 data, these sequential DB
    round-trips saturated the shared MPSC channel (4096/4096), blocking L3
    feed processing via backpressure. This caused L3 data stagnation.
    
    Batch L2 inserts (batch size 100, same as ticker/trade) using multi-row
    INSERT with QueryBuilder, matching the pattern already used for L3.
    Reduces DB round-trips by ~100x during L2 bursts.
  190. 9270de3
    Make L3 advisory-only for epoch validity status
    Show commit body
    L3 data on Kraken is inherently sparse — only ~47% of symbols produce L3
    activity in any 15-min window. This caused criteria_l3_ok=false and chronic
    ExitOnly epochs, blocking all new entries even when ticker/trade/L2 are at
    100%.
    
    L3 is already handled per-symbol via hard-blocks (l3_resync_max), and
    DataIntegrityMatrix explicitly documents L3 as "not required for
    system_live_ready". Remove criteria_l3_ok from epoch status determination
    while keeping it computed and logged for observability.
  191. 0c0fff0
    feat: entry limit deadline + post-expiry taker reeval for ignition/breakout
    Show commit body
    Stale maker orders now die server-side via Kraken deadline (ENTRY_LIMIT_TTL_SECS,
    default 90s). After expiry, price-sensitive families (mean reversion, passive) let
    the signal die. Ignition + breakout setups get a single-attempt taker re-evaluation
    with fresh market data (mid decay < TAKER_REEVAL_MAX_DECAY_BPS, spread < 60 bps,
    net edge still positive after taker fee).
    
    Key changes:
    - add_order() accepts entry_limit_deadline flag; computes Kraken deadline for
      entry limit orders, exit orders always pass false
    - PostExpiryPolicy enum (LetDie / ImmediateTakerReeval) on ExecutionIntent
    - SubmitAndWaitOutcome::EntryExpiredUnfilled variant; runner detects expired-by-
      deadline via InternalOrderState.expired_by_deadline flag set by ws_handler
    - submit_and_wait timeout aligned with TTL + 10s buffer
    - attempt_taker_reeval() in flow_execution: fresh price check, decay/spread/edge
      gates, single taker market order
    - FallbackPolicy::ConvertToTaker killed (always Reject) — was dead code
  192. 651e5b5
    fix: eagerly bind exchange_order_id on matched orders
    Show commit body
    When an execution event is matched to a DB order that has NULL
    exchange_order_id (typical for OTO children matched via symbol/side or
    ord_ref_id fallback), immediately set the exchange_order_id. Without
    this, subsequent status-only events (e.g. `filled` without symbol/side)
    cannot resolve via get_order_by_exchange_id and remain permanently
    unmatched.
    
    Applies to both ws_handler (live path) and raw_execution_backfill
    (async reconciliation).
  193. 749e25d
    chore: set Cargo package author to Raymond Davelaar
  194. 595f918
    fix: pre-persist exit orders before placing on exchange
    Show commit body
    Exit orders (SL, trailing-stop) are now created in the DB with status
    pending_submit BEFORE sending to Kraken. After ack, status is updated
    to acked_open with exchange_order_id. This prevents ghost orders on the
    exchange that have no DB record when the post-ack persist fails or the
    process crashes between placement and persistence.
    
    Applies to:
    - exit_lifecycle: static SL and manual trailing-stop paths
    - ignition_exit: initial trailing-stop protection
    
    Made-with: Cursor
  195. 4fab10a
    fix(rag): stricter public guardrails, allow ops commands, strip code fences
    Show commit body
    - System prompt: no programming code or hostnames; ops shell/systemctl OK; say unknown if unsure
    - Strip markdown fences and obvious domain/IP patterns from answers
    
    Made-with: Cursor
  196. 66f01cb
    feat: capture ord_ref_id from Kraken executions channel
    Show commit body
    Kraken WS v2 provides ord_ref_id on execution reports to link OTO
    secondary orders to their parent. Add the field to ExecutionReport,
    persist it in raw_private_executions (new migration + upsert column),
    and expose it on RawPrivateExecutionRow for downstream matching.
    
    Made-with: Cursor
  197. 90b3381
    docs(rag): dedicated kapitaalbot_rag DB only; never RAG on decision/ingest pools
    Show commit body
    Made-with: Cursor
  198. 99743d5
    fix: OTO trailing-stop child matching — remove cl_ord_id.is_none() guard
    Show commit body
    Kraken WS v2 conditional close children may carry a Kraken-generated
    cl_ord_id that differs from the bot's internal child identifier.  The
    previous guard skipped the OTO-child fallback lookup whenever cl_ord_id
    was present, causing 88% of OTO fills to fall through to
    external_manual_backfill phantom orders (22/25 unbound, 59 phantoms).
    
    Changes:
    - ws_handler: always attempt OTO child fallback after cl_ord_id +
      exchange_order_id lookups fail (removes cl_ord_id.is_none() gate)
    - raw_execution_backfill: same guard removal
    - get_unbound_oto_child_by_symbol_side: include 'reconcile' status so
      children advanced by other processes remain matchable
    
    Made-with: Cursor
  199. 9a98ff6
    fix(rag): tier1 retrieval includes tier2 chunks; document RAG env and systemd
    Show commit body
    - Vector search for tier1/tier2 used tier=tier1 only; indexer marks most allowlist docs tier2
    - systemd: load /etc/trading/kapitaalbot-rag.env for RAG_DATABASE_URL
    
    Made-with: Cursor
  200. acfe8df
    fix: reduce missing_mid and missing_fee on fills
    Show commit body
    missing_mid:
    - Widen price_cache::snapshot_fresh max_age from 2s to 5s for fill mid lookup
    - Add fallback to order_row.orderbook_mid_snapshot (captured at submit time)
    - Add orderbook_mid_snapshot to ExecutionOrderRow and all SELECT queries
    
    missing_fee:
    - When Kraken WS event has no fees array (e.g. terminal 'filled' with
      non-zero qty diff), estimate fee from fill notional * taker tier rate
    - Logs FILL_FEE_ESTIMATED for traceability
    
    Made-with: Cursor
  201. 24ad342
    docs: add FASE_0B to all indices, DE/FR language cross-links, sync EN date
    Show commit body
    Made-with: Cursor
  202. 065be1f
    docs: idiomatic DE/FR for core docs (01-08), rewrite DE 06-08, cross-links in indices
    Show commit body
    Made-with: Cursor
  203. 40221f9
    feat(scripts): extend why_no_trade report with tier2-aligned aggregates (CDV, path tape, safety, infra, optional ingest)
    Show commit body
    Made-with: Cursor
  204. f180e4e
    fix(scripts): skip repeated psql_pool identity in why_no_trade_report
    Show commit body
    Made-with: Cursor
  205. 8539ea9
    feat(scripts): why-no-trade funnel report with optional HTML bar charts
    Show commit body
    Made-with: Cursor
  206. d48b6dd
    chore(env): document L3_RESYNC_MAX=8 in .env.example
    Show commit body
    Made-with: Cursor
  207. b9f429f
    chore: remove production SSH host from repo (privacy)
    Show commit body
    - deploy.sh requires DEPLOY_HOST; rollback/setup_server_structure need arg or DEPLOY_HOST
    - Docs and Cursor rules reference operator runbook, not a fixed user@host
    - CHANGELOG: generic productieserver wording
    
    Made-with: Cursor
  208. d61ec8e
    docs: remove light rect overlay from architecture sequence diagram
    Show commit body
    Mermaid rect rgb(200,220,240) read as a bright stain on dark theme; replace with Note and plain steps.
    
    Made-with: Cursor
  209. 8adcad4
    docs(05): replace invalid mermaid decision_tree with flowchart TD
    Show commit body
    Mermaid 11 has no decision_tree diagram type; use standard flowchart and
    plain labels to avoid parse errors on the docs site.
    
    Made-with: Cursor
  210. aa5086d
    docs: fix Mermaid v11 compatibility (notes, edges, blank lines)
    Show commit body
    - Remove HTML breaks from stateDiagram notes; simplify Expired transition label
    - Epochs flowchart: arrow label via -->|text|; drop blank lines inside blocks
    - graph TD: remove blank lines between node groups
    - sequenceDiagram: no blank line before alt block
    
    Made-with: Cursor
  211. 0908e67
    docs: fix MODULE_INVENTORY links to 00_MODULE_INVENTORY.md
    Show commit body
    Made-with: Cursor
  212. d77a05f
    docs: implement full Git-style core documentation set
    Show commit body
    - Integrated MODULE_INVENTORY.md as 00_MODULE_INVENTORY.md with navigation.
    - Rewrote 01-08 core docs with Git-style formatting and Mermaid diagrams.
    - Added consistent navigation bars and internal anchors to all documents.
    - Created centralized DOC_INDEX.md as the main documentation portal.
    - Generated localized versions (EN, DE, FR) for the index and EN for full set.
    - Ensured technical accuracy based on the cleaned-up codebase.
    
    Made-with: Cursor
  213. c204aa9
    docs(systemd): link to 01_ARCHITECTURE and 08_OPERATIONS in intro
    Show commit body
    Made-with: Cursor
  214. a7b156b
    docs: complete core documentation set (01-08) and index
    Show commit body
    Made-with: Cursor
  215. ea7fd55
    docs: surface MODULE_INVENTORY in DOCS_TARGET_STRUCTURE index and table
    Show commit body
    Made-with: Cursor
  216. e3f3603
    docs: add 02-08 layer guides + FASE_0B_RUNTIME (VWAP, store, TOD)
    Show commit body
    Made-with: Cursor
  217. 0e7bd48
    docs: complete doc phase — 01_ARCHITECTURE, structure status, AGENTS links
    Show commit body
    Made-with: Cursor
  218. ec5596e
    docs: add MODULE_INVENTORY with primary/secondary function per module
    Show commit body
    Made-with: Cursor
  219. b17b9bf
    cleanup(phase-c): demote 16 diagnostic logs from info to debug
    Show commit body
    position_monitor.rs (9 demotions):
    - POSITION_MONITOR_REFRESH_BEGIN (fires every 30s)
    - POSITION_MONITOR_DIAG_DB_ROWS (fires every 30s)
    - POSITION_MONITOR_DIAG_CSS_LEASED (fires every 30s)
    - POSITION_MONITOR_DIAG_PROTECTION_ORDERS (fires every 30s)
    - POSITION_MONITOR_DIAG_SKIP qty_f64<=0 (per position per 30s)
    - POSITION_MONITOR_DIAG_SKIP entry_price<=0 (per position per 30s)
    - POSITION_MONITOR_DIAG_SKIP css_leased (per position per 30s)
    - POSITION_MONITOR_DIAG_EVAL (per position per 30s)
    
    exposure_reconcile.rs (8 demotions):
    - RECONCILE_MSP_UNAVAILABLE_FALLBACK_DB (2 variants)
    - BALANCE_SYNC_RESERVED_OPEN_ENTRY_SKIP (per symbol)
    - BALANCE_SYNC_SKIPPED_NO_SYMBOL_MATCH (per symbol)
    - NOTIONAL_MSP_FALLBACK_TO_DB (3 variants)
    - RECONCILE_POSITION_STATE periodic (per symbol per cycle)
    - EXIT_PLAN_RECONCILED periodic (per symbol per cycle)
    - EXPOSURE_DUST_SKIP periodic delta (per symbol per cycle)
    
    All warn! and actionable info! logs (exits, fills, protection,
    orphan recovery, trailing tighten) remain at their current levels.
    
    Made-with: Cursor
  220. d4a7362
    cleanup(phase-b): remove 54 unused scripts (72-hour rule)
    Show commit body
    Removed categories:
    - 7 one-off DBA index scripts (indexes already applied)
    - 3 date-stamped research scripts (research_flow_liquidity_20260316)
    - 3 edge_survival_audit scripts (self-contained audit, completed)
    - 4 run-specific/orphan SQL (run71, exit_strategy, verify_l3)
    - 6 one-off maintenance SQL (decontaminate, drop_overlap, brin, preflight)
    - 5 completed setup/migration scripts (disable_old_bot, set_password, epoch_fase1)
    - 4 old evidence collection scripts (l2_l3_plan, open_ssot, 400_symbol)
    - 7 orphan SQL with zero references (regime_*, health_monitor, etc.)
    - 6 one-off analysis/monitoring scripts (edgeboard_diagnostics, stage2, sev0)
    - 9 other unused scripts (replay_refresh, flow_capture, run_report, etc.)
    
    Kept: all scripts referenced from AGENTS.md, .cursor/rules, CI,
    systemd services, cross-script calls, or modified within 72 hours.
    
    Made-with: Cursor
  221. db04263
    cleanup(phase-b): docs-reset — remove 196 obsolete documents
    Show commit body
    Remove all documentation except:
    - DECIMAL_F64_POLICY_AND_INVENTORY.md (referenced from AGENTS.md, .cursor/rules)
    - FORWARD_RETURNS_OBSERVABILITY.md (referenced from AGENTS.md)
    
    Add DOCS_TARGET_STRUCTURE.md stub defining the 8 core documents
    (01_ARCHITECTURE through 08_OPERATIONS) to be written as a mandatory
    follow-up step after cleanup and final validation.
    
    Removed: archive/, superseded/, audit/, reports/, strategy_audit/,
    analysis/, runbook/, de/, en/, fr/ subdirectories and 170+ dated
    analyses, changelogs, investigation reports, and planning artifacts
    that no longer reflect the current codebase.
    
    Source code comments referencing deleted docs will be updated when the
    8 new documents are written.
    
    Made-with: Cursor
  222. 29f14ac
    cleanup(phase-a3): integrate below_cost_min and instrument_tradable into logging
    Show commit body
    - Add below_cost_min and instrument_tradable to MONITOR_EXIT_NOT_ACTIONABLE
      and POSITION_MONITOR_DUST_ORPHAN log calls in position_monitor.rs
    - Remove #[allow(dead_code)] from both fields in PositionActionability
    
    These diagnostic fields are now read by logging, eliminating the dead_code
    warnings while providing richer actionability diagnostics.
    
    cargo check: 0 warnings
    
    Made-with: Cursor
  223. 7efc938
    cleanup(phase-a2): remove dead functions, types, and config flags
    Show commit body
    - Remove SymbolLockReason enum + check_symbol_execution_lock_reason +
      infer_lock_reason from exposure_reconcile.rs (incomplete, unused)
    - Remove evaluate_shadow_trades + log_blocker_matrix from
      shadow_evaluator.rs (legacy per-run path; evaluate_all_pending remains)
    - Remove BookLevel, BookData, BookMessage (f64 versions) from messages.rs
      (replaced by BookLevelRaw/BookDataRaw/BookMessageRaw with Decimal/RawValue)
    - Remove BookMessageRaw.channel field (serde ignores unknown fields)
    - Remove quantize_wire_f64 from instruments.rs (replaced by quantize_wire
      with Decimal)
    - Remove survival_stage field from CandidateDecisionVector (incomplete
      feature, never populated)
    - Remove enable_vwap_strategy() and enable_tod_overlay() config flags
      (gates without consumers; underlying features retained for Fase 0)
    
    Retained: lookup_route_context (actively used in position_monitor.rs)
    
    cargo check: 0 warnings
    
    Made-with: Cursor
  224. 10ac730
    cleanup(phase-a1): remove dead modules without functional relevance
    Show commit body
    - Remove ignition/regime_contract.rs (never connected to runtime)
    - Remove route_state/strategy_onboarding.rs (test harness only)
    - Remove fill_feedback snapshot/aggregation code (FillFeedbackSnapshot,
      snapshot(), maker_fill_rate, edge_survival_rate, slippage_bias_direction)
      while keeping the active recording path (record_fill_feedback)
    - Remove unused ExpectedFill.expected_fill_time_ms field (set but never read)
    - Remove unused FillStyle::Undetermined variant (never constructed)
    - Remove all #[allow(dead_code)] from fill_feedback types
    
    Fase 0 items (vwap_window, tod_multiplier, route_state scaffold,
    timing_metrics) intentionally retained for future integration review.
    
    cargo check: 0 warnings
    
    Made-with: Cursor
  225. d6cb3f1
    wire position_linkage via OnceLock slot + fix last semantic log lines
    Show commit body
    - spawn_position_monitor now takes a RouteStoreSlot (Arc<OnceLock<...>>)
      that gets filled after bootstrap_live_preloop_runtime; the monitor
      reads it lazily on each 30s tick, enabling position_linkage advisory
      when ENABLE_ROUTE_STATE_STORE=true
    - fix 2 cosmetic log lines in exposure_reconcile.rs that said "positions"
      when referring to merged exposure rows
    - 0 warnings
    
    Made-with: Cursor
  226. 347f0f5
    fix: hard guard — reject buy trailing-stop as exit/protection on Kraken spot
    Show commit body
    Root cause: sell trailing-stop triggered for XMN, selling 1821.4936 of
    1821.49362. Remaining dust (0.00002) kept positions row open. Reconciliation
    imported the triggered sell as order 1758 (side=sell). exit_lifecycle treated
    this as a "short entry" and placed a buy trailing-stop (OKVBJI) as protection.
    
    On Kraken spot there are no short positions. A sell is always an exit, never
    an entry. A buy trailing-stop as "exit_sl" is nonsensical.
    
    Guards added in three locations:
    - exit_lifecycle: bail if exit_side == "buy"
    - protection_flow (both paths): reject if !is_long or side == "buy"
    - position_monitor orphan recovery: reject if protect_side == "buy"
    
    Also cancelled the erroneous buy TSL on exchange and closed the XMN dust
    position (0.00002 < ordermin 670).
    
    Verified against Kraken docs: spot trading has no margin/short capability
    unless explicitly enabled with margin=true flag.
    
    Made-with: Cursor
  227. d83deec
    fix: observe mode now uses dynamic instrument cache instead of hardcoded 50-symbol fallback
    Show commit body
    The observe path in main.rs called default_observe_symbols() which returns
    a hardcoded 50-symbol list, while the execution path uses the live Kraken
    instrument cache (all USD pairs). This created two different universe truths.
    
    Root cause: instruments::preload_all() was never called before the observe
    runner, so resolve_observe_symbols() always found an empty cache and fell
    back to the hardcoded list.
    
    Fix: preload instrument cache before resolving observe symbols. The log
    now shows the actual source (instrument_cache vs hardcoded_fallback) and
    whether degraded mode is active.
    
    Made-with: Cursor
  228. 916a865
    fix: balance_cache key matching + own_orders_cache stale entry cleanup
    Show commit body
    Two verified root causes for XMN/USD EOrder:Insufficient funds:
    
    1. balance_cache::kraken_asset_key_matches_base("XMN", "XMN") returned
       FALSE — the function stripped "X" prefix (Kraken legacy convention)
       from "XMN", yielding "MN" != "XMN". Fix: check direct match FIRST
       before attempting prefix stripping. Affects all assets whose name
       starts with X/Z/XX/XZ.
    
    2. own_orders_cache::upsert_updates never removed filled/canceled orders.
       O5LNBU (filled buy) persisted in cache, inflating
       exchange_open_orders_count and causing incorrect ReservedFundsConflict
       classification. Fix: remove orders with terminal status on update;
       defensive filter in all query functions.
    
    Verified against Kraken WS v2 docs:
    - executions snapshot contains only OPEN orders (snap_orders=true)
    - Filled/canceled/expired are terminal states per exec_type enum
    
    Made-with: Cursor
  229. 35222e4
    fix: cap protection qty to exchange balance — prevent EOrder:Insufficient funds
    Show commit body
    Root cause verified via Kraken REST API: bot DB base_position (1821.49362)
    exceeds actual exchange balance (1821.4936) by 0.00002 due to fill-aggregation
    rounding. Kraken spot correctly rejects sell orders exceeding available balance.
    
    position_truth epsilon (0.182) is too coarse to catch this — the difference is
    within the sync tolerance but the exchange enforces exact qty.
    
    Fix: both protection_flow and position_monitor orphan recovery now cap the
    protection order qty to min(db_qty, exchange_balance) when the balance cache
    is populated, preventing the qty > balance rejection.
    
    Verified against Kraken docs:
    - EOrder:Insufficient funds = "Client does not have the necessary funds"
    - Open orders tie up funds (but XMN has 0 open orders on exchange)
    - OTO conditional close creates OPPOSITE side (corrects earlier analysis)
    
    Made-with: Cursor
  230. bf4cf61
    refactor: fix misleading position terminology in exposure model
    Show commit body
    Rename semantically incorrect "position" uses to accurate terms:
    - bootstrap_max_positions → bootstrap_max_slots (capacity limit)
    - load_positions_from_balance → load_exposure_from_balance_cache
    - load_positions_from_exchange_balances_db → load_exposure_from_exchange_balances_db
    - load_positions_from_executions → load_exposure_from_executions_cache
    - PositionLifecycle → TradeLifecycle (trade state, not DB row)
    
    Add canonical model documentation to exposure_reconcile module
    header explaining the distinction between exchange balances,
    managed exposure, and tracked positions.
    
    Made-with: Cursor
  231. 631f839
    fix: position monitor orphan recovery retry + side validation
    Show commit body
    - Orphan recovery now retries up to 3 times with 120s cooldown
      instead of permanently giving up after first attempt
    - Add wrong-side protection detection: warn when protection order
      side doesn't match position direction (buy trailing-stop on long)
    - Also detect wrong-side protection on CSS-leased positions
    - Always log POSITION_MONITOR_REFRESHED regardless of count
    - Add diagnostic logging for each position evaluation step
    
    Made-with: Cursor
  232. 01eabfd
    diag: add position_monitor diagnostic logging for orphan investigation
    Show commit body
    Temporary diagnostic logs to trace exactly why XMN/USD orphan
    detection is not firing despite being in the positions table.
    Logs: DIAG_DB_ROWS, DIAG_CSS_LEASED, DIAG_PROTECTION_ORDERS,
    DIAG_EVAL per position, DIAG_SKIP with reason.
    
    Made-with: Cursor
  233. ee9d9c4
    wire position_linkage into position_monitor + eliminate all 74 warnings
    Show commit body
    - position_linkage now actively drives position management decisions:
      DefensiveExit triggers market exit for losing positions with
      expired/stale routes, TightenDefensive tightens trailing stops
      when edge decays beyond 60% threshold
    - Route context (edge, signal age, advice) logged in POSITION_MONITOR_STATUS
    - spawn_position_monitor accepts optional RouteStateStore reference
    - All 74 cargo warnings resolved: unused imports removed, unreachable
      pattern suppressed, dead code annotations added for API surface
      enums/structs/functions that form the architectural extension points
    
    Made-with: Cursor
  234. d43ea2b
    fix: horizon_from_secs missing H15m arm caused key collisions
    Show commit body
    The wildcard fallback mapped all max_hold_secs > 600 (including 900s
    H15m routes) to H3m, producing incorrect RouteStateKeys and potential
    collisions with actual 3-minute routes. Now explicitly handles H15m
    (<=900s), caps anything above 900s at H15m, and only defaults to H3m
    for None.
    
    Made-with: Cursor
  235. 4d0f3ec
    log taker_fallback_unviable in timing_metrics snapshot
    Show commit body
    Include the unviable counter in the periodic log line to eliminate
    the dead-code warning on TimingMetricsSnapshot::taker_fallback_unviable.
    
    Made-with: Cursor
  236. c934f2e
    wire remaining timing_metrics: update_tier2 + record_execution
    Show commit body
    Pipeline bridge now calls update_tier2_economics for existing rows
    (enabling taker fallback computation and timing metrics) instead of
    always upserting fresh rows.  record_execution(Taker) is called from
    flow_poller on successful order submission.
    
    Warnings 88 → 75.
    
    Made-with: Cursor
  237. a98ea46
    wire timing_metrics periodic snapshot logging
    Show commit body
    Spawn 60s background task (when store enabled) that logs the full
    TimingMetricsSnapshot: routes evaluated/expired by reason, maker
    window misses, taker fallback viability %, execution maker/taker
    split, max signal age, and median age bucket.  Counters are driven
    by the store's update_tier1 / update_tier2_economics calls.
    
    Warnings 88 → 79.
    
    Made-with: Cursor
  238. b1fd55a
    wire timing_selection into eval cycle for observability
    Show commit body
    After pipeline→store feed, call select_top_routes(5) and log the
    timing-aware ranking alongside the heap-based ranking.  Output
    includes symbol, strategy family, and timing-adjusted score for
    each top route.  Runs only when store is non-empty and enabled.
    
    Warnings 92 → 88.
    
    Made-with: Cursor
  239. b7713be
    wire edgeboard RAM cache → RouteStateStore bridge
    Show commit body
    Add EdgeSignalCache::inner_read() for read-only snapshot access.
    edgeboard_bridge::sync_ram_cache_to_store() converts cached signals
    to EdgeboardSignal and ingests into the store.  Periodic background
    task syncs at edgeboard_snapshot_interval_secs cadence.
    
    Warnings 99 → 92.
    
    Made-with: Cursor
  240. 00bacb1
    wire pipeline outcomes → RouteStateStore (Tier 2 feed)
    Show commit body
    New module route_state::pipeline_bridge maps Vec<Outcome> to
    RouteStateKey + RouteStateRow.  After each eval cycle's heap merge,
    the store is fed (with prune_expired) when ENABLE_ROUTE_STATE_STORE
    is active.  Outcomes with empty symbols are skipped.  Includes unit
    tests for positive/negative edge executable status.
    
    Warnings 106 → 99.
    
    Made-with: Cursor
  241. 1bc47d5
    wire RouteStateStore + Tier 1 driver into live bootstrap
    Show commit body
    Add ENABLE_ROUTE_STATE_STORE feature flag (default false).
    When enabled, bootstrap_live_preloop_runtime creates a shared
    Arc<parking_lot::RwLock<RouteStateStore>> and installs the
    tier1_driver mid-tick subscriber (which now coexists with Loop A
    via the multiplexed subscriber added in the previous commit).
    
    LivePreloopRuntime carries the optional store for downstream
    pipeline/edgeboard/timing wiring.
    
    Made-with: Cursor
  242. b5a65f6
    multiplex price_cache mid-tick subscriber
    Show commit body
    Replace single OnceLock<Arc<Fn>> with OnceLock<RwLock<Vec<Arc<Fn>>>>.
    Multiple subscribers can now register; each is called in registration
    order on every ticker update.  register_mid_tick_subscriber always
    returns true (backward compatible — Loop A no longer skips on second
    registration).  Enables Tier 1 driver and Loop A to coexist.
    
    Made-with: Cursor
  243. d164964
    vwap_reversion: use real VWAP deviation when available
    Show commit body
    When ENABLE_VWAP_LIVE_FEED is active, vwap_reversion::compute uses
    features.vwap_deviation_bps (from rolling trade buffer) instead of
    features.micro_bps (L2 microprice proxy).  Falls back to micro_bps
    when VWAP data is unavailable (flag off, insufficient ticks, or lock
    contention).
    
    No impact on order sizing or risk math — this only affects the
    move-thesis expected-move estimate for VwapReversion family.
    
    Made-with: Cursor
  244. f63e3ba
    wire VwapBuffer into trade WS + add vwap_deviation_bps to MarketFeatures
    Show commit body
    V2: record_trade call added in run_trade_ws_once alongside existing
    trade_flow_window and horizon_movers consumers.
    
    V3: MarketFeatures gains vwap_deviation_bps: Option<f64>, populated
    via vwap_buffer::vwap_deviation_bps_sync (non-blocking try_read on
    the tokio RwLock).  Returns None when ENABLE_VWAP_LIVE_FEED=false
    or insufficient ticks (<3).
    
    All 25 struct literal sites updated with vwap_deviation_bps: None.
    Resolves 2 dead-code warnings (vwap_window::compute_vwap and
    vwap_deviation_bps now have live callers via vwap_buffer).
    
    Made-with: Cursor
  245. 0229164
    add VwapBuffer module with ENABLE_VWAP_LIVE_FEED flag
    Show commit body
    Per-symbol rolling VWAP buffer (src/exchange/vwap_buffer.rs) following
    the trade_flow_window pattern: global OnceLock + Arc<RwLock>, per-symbol
    VecDeque<VwapTick>, prune on write.
    
    Delegates pure VWAP math to analysis::vwap_window.  Gated by
    ENABLE_VWAP_LIVE_FEED (default false) — record_trade is a no-op and
    vwap_deviation_bps_for_symbol returns None when disabled.
    
    New flag replaces the ghost enable_vwap_strategy() which has zero
    callers.  Window configurable via VWAP_WINDOW_MS (default 300s).
    
    Made-with: Cursor
  246. fbd0a96
    multi-region TOD session curves for ranking and move thesis
    Show commit body
    Ranking curve (tod_multiplier.rs): liquidity/tradability model with
    explicit Asia (00-08), EU (08-12), EU+US overlap (13-16), US main
    (17-20), dead zone (21-23) sessions.  Peak at EU+US overlap (1.15),
    trough at Asia 02-03 (0.80), weekend factor 0.85.
    
    Move thesis curve (tod_overlay.rs): volatility/amplitude model.
    Peak at EU+US overlap (1.20) and US session (1.15).  Asia hours
    get moderate vol (1.10) for stablecoin/alt events.  EU morning
    lowest (0.90) reflecting orderly institutional flow.
    
    These curves serve different purposes and are never stacked on the
    same candidate (anti-double-counting in route_expectancy.rs).
    
    Made-with: Cursor
  247. d967796
    fix: derive utc_hour and utc_weekday from same timestamp
    Show commit body
    Before this fix, utc_hour was captured at MarketFeatures construction
    time while weekday was computed fresh at scoring time in
    route_expectancy.  Around the UTC midnight boundary these could
    disagree on the calendar day, causing the weekend factor (0.85) to
    be dropped or applied incorrectly.
    
    Add utc_weekday: Option<u8> to MarketFeatures, set from the same
    Utc::now() call as utc_hour at all 4 production sites.  The ranking
    TOD path now reads both from features instead of mixing timestamps.
    
    Made-with: Cursor
  248. b5cee90
    wire ranking-level TOD multiplier on all candidates
    Show commit body
    When ENABLE_RANKING_TOD=true, analysis::tod_multiplier applies a
    liquidity/tradability-based hour+weekday correction to time_adjusted_score
    for ALL route candidates.  Prevents double-counting by skipping candidates
    already using the TodOverlay family move thesis (volatility-shaped curve).
    
    Addresses: flat ranking differentiation, entries at suboptimal session hours.
    Resolves 3 dead-code warnings (tod_multiplier module now has live callers).
    
    Made-with: Cursor
  249. cfc9d36
    wire per-family edge lifetime in GlobalEdgeHeap merge
    Show commit body
    When ENABLE_PER_FAMILY_EDGE_LIFETIME=true, the heap merge uses
    timing_profile per-family TTLs (ignition 15s, scalping 10s,
    market making 600s, etc.) instead of a single EDGE_LIFETIME_DEFAULT_SECS.
    Addresses stale-edge problem for fast strategies and premature expiry
    for slow strategies. Defaults to off (existing behavior unchanged).
    
    Also adds ENABLE_RANKING_TOD config flag for Cluster 2 (next commit).
    
    Made-with: Cursor
  250. 6bf0875
    instruments: remove hardcoded GLOBAL_MIN_NOTIONAL_USD from PositionActionability
    Show commit body
    Dust/actionable classification is now determined entirely by exchange
    instrument constraints (qty_min, cost_min, tradability status).
    
    Removed:
    - GLOBAL_MIN_NOTIONAL_USD constant ($0.50)
    - below_global_floor field from PositionActionability
    - Hardcoded $1.0 threshold for is_dust in below_qty_min/below_cost_min branches
    
    Simplified logic: below_qty_min → is_dust (exchange will reject the order,
    therefore it's non-actionable dust by definition). Same for below_cost_min.
    No dollar-based heuristics remain in the assessment path.
    
    Made-with: Cursor
  251. 378b48d
    position_monitor: fix orphan recovery timing and return tracking
    Show commit body
    - Don't attempt recovery when price cache is empty (startup race);
      defer to next refresh cycle with POSITION_MONITOR_ORPHAN_DEFERRED
    - Same for missing instrument data
    - Only mark recovery as "attempted" when an order was actually sent
    - attempt_orphan_recovery returns bool to track success
    
    Made-with: Cursor
  252. 8a053a1
    instruments + position_monitor: generic position actionability assessment and orphan recovery
    Show commit body
    FASE B — Generic instrument-aware position assessment:
    - Added PositionActionability struct to instruments.rs with full constraint evaluation:
      qty_min, cost_min, global notional floor, instrument tradability
    - assess_position() on InstrumentConstraints: determines if a position is actionable,
      dust, or below minimums, with exact qty_deficit calculation
    - assess_position_actionability() convenience wrapper using cache + price_cache
    
    FASE C — Orphan detection now distinguishes actionable vs dust:
    - Dust orphans (below qty_min AND below notional floor): logged as INFO
      POSITION_MONITOR_DUST_ORPHAN, no longer pollute WARN signal
    - Actionable orphans: remain WARN with full instrument assessment data
    
    FASE D — Automatic orphan recovery:
    - attempt_orphan_recovery(): generic recovery for unprotected positions
    - If position is actionable: places trailing-stop at 0.4% (40 bps)
    - If position is below qty_min but recoverable: buys minimum top-up to reach
      qty_min, then places trailing-stop on full position
    - Safety guards: recovery_topup max $15 notional, one attempt per symbol per
      session, guard_prewire_notional on all orders, instrument constraints on all
      qty/price normalization
    - All orders labeled as orphan_recovery_topup / orphan_recovery_tsl in DB
    
    FASE E — market_exit_position hardened:
    - Now validates position actionability before attempting exit
    - Uses normalize_exit_qty + quantize for wire qty
    - Logs MONITOR_EXIT_NOT_ACTIONABLE when position is below exchange minimums
    
    Made-with: Cursor
  253. 7235a85
    position_monitor: add hard 1% loss cap, stale position exit, and 15-min regime shift
    Show commit body
    Three critical position management rules added to the background monitor:
    
    1. Hard 1% loss cap (HARD_MAX_LOSS_BPS = -100): immediate market exit when
       unrealized loss exceeds -100 bps. Non-negotiable safety floor.
    
    2. Stale position exit: if price moves less than 3 bps over a 5-minute window
       and the position is underwater, exit at market. Prevents holding dead
       positions hoping for recovery that never comes.
    
    3. 15-minute regime shift: after 15 minutes of hold time, losing positions
       are exited at market. Winning positions get their trailing stop tightened
       to 40% of current distance to lock in gains.
    
    All three use a shared market_exit_position() helper that handles reconcile,
    market order submission, protection cancellation, and DB status updates.
    
    Priority order: hard loss cap > stale exit > regime shift > existing TP/trail.
    
    Made-with: Cursor
  254. 37fa1be
    feat: use trades_per_second in expansion classifier (activity spike + illiquidity filter)
    Show commit body
    High trade frequency (≥2/sec) confirms genuine expansion activity (+0.15).
    Wide spread with near-zero trade activity (<0.1/sec) is illiquidity, not
    expansion — penalized -0.30 to suppress false positives.
    
    Made-with: Cursor
  255. 1ede5f4
    fix: remove CONCURRENTLY from expansion migration (sqlx runs in txn)
    Show commit body
    Made-with: Cursor
  256. 4e49354
    feat: Phase 1 expansion classifier — label-only shadow/paper detection
    Show commit body
    Adds expansion regime detection based on RESEARCH pool analysis (42K obs,
    7 days): spread ≥150bps, |microprice_dev| ≥40bps, vol ≥60bps, ignition
    Expansion/Ignition state, and expansion-prone regime (LOW_LIQUIDITY/CHAOS/
    HIGH_VOLATILITY). No execution path changes — labels only.
    
    - New module: analysis/expansion_classifier with classify_expansion()
    - Shadow trades: expansion classification in parity_detail_json + dedicated
      is_expansion/expansion_score columns (migration)
    - CDV detail_json: expansion classification embedded
    - Shadow evaluator: expansion shadows use 5min TP/SL window (vs 90s normal)
    - Structured logging: EXPANSION_DETECTED per candidate in pipeline
    
    Made-with: Cursor
  257. c07b349
    fix: shadow CLI evaluates all pending shadows, not just one run_id
    Show commit body
    The backfill needs to resolve shadows across all run_ids, not just
    the freshest active. Uses evaluate_all_pending_shadow_trades.
    
    Made-with: Cursor
  258. cbff68f
    readiness: gate surplus spread relaxation on trade density
    Show commit body
    Wide spread + high surplus but no trade activity = illiquid gap, not
    expansion pressure. Surplus-based spread anchor relaxation now scales
    linearly with trade_density: zero relaxation below 0.3 trades/sec
    (~18/min), full factor at 1.5 trades/sec (~90/min). Prevents admitting
    candidates with 140 bps spread on dead books.
    
    Made-with: Cursor
  259. a9266ba
    fix: shadow_id is UUID not i64 in shadow evaluator structs
    Show commit body
    Made-with: Cursor
  260. d87b8b4
    fix: add analyze-shadow-readiness to cli_mode() dispatch table
    Show commit body
    Without this, the binary didn't recognize the mode and fell through
    to default observe startup instead of running shadow evaluation.
    
    Made-with: Cursor
  261. 23fb8f7
    ops: reduce log volume ~42% by downgrading per-symbol diagnostics to trace
    Show commit body
    Downgraded from info to trace:
    - REGIME_STRATEGY_SET (579/min → per-symbol regime diagnostic)
    - STRATEGY_FANOUT (579/min → per-symbol candidate count)
    - ENTRY_DIRECTION (579/min → per-symbol microprice direction)
    - ROUTE_ECONOMY_SHADOW_DIVERGENCE (376/min → readiness shadow)
    
    Retained at info: PATH_TAPE_EXIT_ROUTING, FLOW_SUBMIT, EXIT_COMPLETED,
    POSITION_MONITOR_*, V2_PIPELINE_*, emergency logs — all critical for
    live validation and diagnosis.
    
    Made-with: Cursor
  262. e22bedf
    fix: shadow evaluator dual-pool resolution (decision + ingest)
    Show commit body
    Shadow trades were never resolved (0/4951 pending) due to three bugs:
    1. No automatic worker — evaluate only via CLI, never periodic
    2. CLI routed to INGEST pool but shadow_trades lives on DECISION
    3. Single-pool SQL JOINed shadow_trades with trade_samples, but they
       live on different pools (decision vs ingest)
    
    Fix: rewrite evaluator to dual-pool pattern (same as backfill_shadow_markout_5m),
    integrate into edgeboard refresh worker for automatic periodic resolution,
    and add dedicated dual-pool CLI handler for analyze-shadow-readiness.
    
    Made-with: Cursor
  263. d7c855e
    tune: relax spread anchor surplus factor for high-edge expansion trades
    Show commit body
    HVP was confirmed to block zero positive-edge candidates. The actual
    secondary filter is readiness_SpreadTooWide, blocking 1212 candidates
    with avg 200 bps edge. Surplus relaxation factor raised from 0.20
    to 0.30 and cap from 60 to 80 bps, allowing high-surplus expansion
    trades through wider spreads while keeping base anchor at 80 bps.
    
    Effective threshold at surplus 200 bps: 140 bps (was 120).
    Effective threshold at surplus 300+ bps: 160 bps (was 140).
    
    Made-with: Cursor
  264. 56c0be8
    fix: exclude pure Maker entry during Ignition/Expansion states
    Show commit body
    During Ignition/Expansion, fill certainty matters more than price
    improvement. Pure Maker limit orders in runaway markets typically
    never fill, missing large asymmetric moves (BLESS/USD pattern).
    
    Now filters out EntryMode::Maker when ignition state is Ignition
    or Expansion, keeping Taker and MakerFirstTakerFallback variants
    for immediate execution.
    
    Made-with: Cursor
  265. a052fd1
    fix: position monitor now warns on orphaned positions without protection
    Show commit body
    Previously, positions without a protection order (emergency_stop,
    exit_sl, ignition_sl) were silently skipped. This made orphaned
    positions invisible to operations. Now logs POSITION_MONITOR_ORPHAN_DETECTED
    every 30s refresh cycle when such positions exist, enabling faster
    manual intervention.
    
    Made-with: Cursor
  266. 5b1553a
    fix: CDV fill_prob_snapshot -> entry_fill_probability in forensic review
    Show commit body
    The candidate_decision_vectors table uses entry_fill_probability, not
    fill_prob_snapshot. The latter exists only on execution_orders.
    
    Made-with: Cursor
  267. 2aa57a3
    add: forensic trade review 36h — broad multi-population analysis
    Show commit body
    Three scripts for comprehensive 36h forensic review:
    - forensic_trade_review_36h.sql: DECISION pool (phases A/B/C/H/I/J/K)
    - forensic_forward_returns_36h.sql: RESEARCH pool (phases D/E)
    - run_forensic_trade_review_36h.sh: runner with DB precheck
    
    Covers filled trades, acked-but-unfilled, blocked candidates, shadow
    trades, forward returns horizon analysis, and ex ante detectability.
    
    Made-with: Cursor
  268. e5c2b87
    fix: broaden AcceleratingContinuation gate + widen ThinSlow SL/TP
    Show commit body
    AcceleratingContinuation: remove move_bps < 180 cap and widen spread
    gate from 45 to 80 bps. The move cap was counterproductive — it
    excluded the large expansion moves we want trailing stops for.
    
    ThinSlow: widen SL from -28 to -55 and TP from 40 to 55 bps. Trades
    that legitimately remain ThinSlow (Ineligible tier, very low trade
    count) need more room — -28 bps stopped them out on normal noise
    in coins with 10+ bps spreads.
    
    Made-with: Cursor
  269. a588cf9
    fix: path tape shortcircuit — use soft_min_trades as default when tape gate disabled
    Show commit body
    When TAPE_ENTRY_GATE_ENABLE=false, rolling_trades is None. Previously
    unwrap_or_default() yielded 0, which is always < soft_min_trades (8),
    forcing every trade into ThinSlow and bypassing all other classification
    paths (ignition, spread/move, route_family). This destroyed pipeline
    trailing choices for 34% of trades and locked all exits to static SL -28 bps.
    
    Now uses soft_min_trades as the default so 8 < 8 is false and the full
    classify_path_tape logic is reached.
    
    Made-with: Cursor
  270. a465c55
    add case analysis SQL for FUN/SPELL/DOT/BLESS exit forensics
    Show commit body
    Made-with: Cursor
  271. 6f27549
    add: markout analysis SQL for edge_bias_entry_capture diagnostics
    Show commit body
    Temporary diagnostic script to determine whether edge is lost
    pre-entry (selection/prediction) or post-entry (exits/protection)
    by analyzing realized_move_window_3m/5m/10m markouts.
    
    Made-with: Cursor
  272. 11d6510
    fix: pre-aggregate realized_pnl to prevent partial-fill fan-out in edge report
    Show commit body
    realized_pnl has one row per partial exit fill (no UNIQUE on
    entry_order_id). The previous LEFT JOIN with OR produced multiple rows
    per entry trade when exits had partial fills, causing each partial row
    to carry the full predicted_edge_bps but only a fraction of realized
    PnL. This systematically inflated mean_bias_bps and overcounted
    n_trades in all aggregate sections.
    
    Fix: add a pnl_agg CTE that sums realized_pnl_quote and computes
    qty-weighted avg exit price per entry_order_id, then INNER JOIN once
    per entry trade. Applied consistently across all 6 report sections.
    
    Section 0 now also shows realized_pnl_distinct_entries for quick
    fan-out detection.
    
    Made-with: Cursor
  273. 17de5e1
    feat: make edge/sizing tuning parameters env-overridable
    Show commit body
    Prepare for data-driven tuning based on predicted-vs-realized edge report
    (Fase 4). All parameters keep current defaults; changes only take effect
    when env vars are set.
    
    New overrides:
    - SIZING_EDGE_SIGMOID_K (default 0.02, clamp 0.005-0.10): controls
      position sizing differentiation by edge magnitude
    - ROUTE_WAIT_RISK_TAKER_BASE (default 1.0): base wait risk for taker entries
    - ROUTE_WAIT_RISK_MAKER_BASE (default 2.5): base wait risk for maker entries
    - ROUTE_EXIT_DRAG_GLOBAL_MULT (default 1.0): global multiplier on all
      regime exit drag factors
    
    Existing override ROUTE_MIN_NET_EDGE_BPS (default 0.0) already covers
    the minimum edge threshold.
    
    All env reads cached via OnceLock for zero hot-path overhead.
    
    Made-with: Cursor
  274. ede473d
    feat: add predicted vs realized edge report (CDV + fills + PnL)
    Show commit body
    New SQL report and shell wrapper that joins candidate_decision_vectors
    (predicted edge at admission) with fills and realized_pnl to measure
    systematic bias in the cost model.
    
    Sections:
     0. Data availability check
     1. Per-trade detail (latest 100)
     2. Aggregate bias by route_type
     3. Aggregate bias by entry_mode
     4. Aggregate bias by horizon
     5. Cost component decomposition vs realized
     6. Bias by predicted edge bucket
    
    Runs against DECISION pool with db_target_precheck. Read-only, zero
    runtime impact.
    
    Made-with: Cursor
  275. 9cad674
    feat: enable OTO conditional trailing stop for limit entries
    Show commit body
    Remove the order_type == "market" guard so that limit entries with
    oto_trail_bps > 0 also submit the conditional trailing-stop child via
    Kraken's OTO mechanism. Previously only market orders got atomic OTO
    protection; limit entries fell back to manual post-fill trailing
    placement with a timing window.
    
    cl_ord_id is omitted on the wire when conditional is set (Kraken
    constraint); correlation falls back to order_id + symbol matching
    which the OrderTracker already supports.
    
    Log now includes primary_order_type for observability of limit vs
    market OTO submissions.
    
    Made-with: Cursor
  276. dcbf3db
    safety: arm DMS on dedicated runner WS to protect standalone run-execution-once
    Show commit body
    When using a dedicated WS connection (no shared hub), the dead man's switch
    was not armed. If the process crashes with open orders, those orders would
    remain indefinitely on the exchange.
    
    Arm cancel_all_orders_after(60s) immediately after connecting the dedicated
    private WS, and re-arm on reconnect. The 60s timeout matches the hub pattern.
    For the short-lived one-shot runner, the DMS auto-expires after process exit
    (no explicit disarm needed at every return point).
    
    Made-with: Cursor
  277. 8c87d4d
    fix: thread hub through emergency_protect_with_retry so market_close escalation can succeed
    Show commit body
    emergency_protect_with_retry passed hub: None to both protect_exposure and
    market_close_exposure. Since market_close_exposure requires a hub (returns
    HardFail("market_close_requires_hub") without one), the emergency market
    close escalation path in the run-execution-once runner could never succeed.
    
    Thread the hub parameter from submit_and_wait_for_execution_reports through
    to emergency_protect_with_retry and its downstream calls, so that when a hub
    is available the escalation path works correctly.
    
    Made-with: Cursor
  278. 8fd65a0
    Add DECISION SQL for post-fix recent lineage validation.
    Show commit body
    Made-with: Cursor
  279. fa0360c
    Fix CDV lineage: bypass cap for admission_ok, log execution_precheck failures.
    Show commit body
    Made-with: Cursor
  280. 07b22f2
    Add DECISION-only lineage dashboard audit SQL and server entrypoint.
    Show commit body
    Made-with: Cursor
  281. 0e84c03
    docs(sql): align Status A for historical backfill exit repair cohort
    Show commit body
    - Aggregate header: dual gate still_unrepaired=0 and skipped_with_marker=0;
      clarify repaired_without_first_hard vs anomaly bucket.
    - Detail header: cross-reference aggregate for Status A.
    
    Made-with: Cursor
  282. f82c33a
    fix(sql): status A requires skipped_with_marker=0 for repair cohort
    Show commit body
    Align aggregate header comments and Rust module docs with dual gate:
    still_unrepaired=0 and skipped_with_marker=0; anomaly bucket not status-A-compatible.
    
    Made-with: Cursor
  283. 0f1896e
    docs(sql): cohort-only repair validation + idempotency proof
    Show commit body
    - Replace broad validate_48h with cohort_detail + cohort_aggregate (same basis as repair candidates).
    - Document marker payload keys, idempotency, status A gate, and race handling in repair module.
    - Link candidates header to cohort SQL; extend exit_coverage_semantics marker doc.
    
    Made-with: Cursor
  284. 81eb051
    feat(execution): one-shot decision repair for pre-dispatch terminal backfill cohort
    Show commit body
    - exit_coverage_semantics: repair idempotency event type constant.
    - raw_execution_backfill: shared dispatch_post_fill_exit_terminal_for_order with
      worker vs repair context; structured terminal outcomes.
    - historical_backfill_exit_repair: CLI repair-historical-decision-backfill-exit-once
      reuses dispatch, writes marker, 48h candidate SQL parity.
    - SQL: candidate selection + post-repair 48h validation (replay-aligned first_child).
    
    Made-with: Cursor
  285. 0dea789
    fix(execution): close NULL-child replay gap for terminal fill backfill
    Show commit body
    - ws_handler: when terminal filled has fill_qty=0 but fills ledger is full,
      emit EntryFillInfo so the runner runs post-fill exit.
    - raw_execution_backfill: after filled_terminal_backfill, spawn dispatch
      that runs exit_lifecycle when enabled, with emergency protect fallback.
    - exit_coverage_semantics + replay SQL: treat ignition_sl as first-hard
      protection; replay matches orphan emergency_stop within 30m window.
    
    Made-with: Cursor
  286. 9870b04
    Document partial-fill protection vs exit coverage status split
    Show commit body
    detect_partial_fill_protection already uses PROTECTION_CONFIRMED_EXIT_STATUSES_SQL;
    add explicit comments so it is never merged with EXIT_COVERAGE_STATUSES_SQL for triage.
    
    Made-with: Cursor
  287. b8d6868
    Replay SQL: match first-hard child by strategy_context regardless of terminal status
    Show commit body
    Made-with: Cursor
  288. 4cb3bf8
    Replay SQL: exclude non-entry filled rows from entries CTE
    Show commit body
    Made-with: Cursor
  289. 316e6ef
    Align exit coverage semantics across reconcile, lock, and audits
    Show commit body
    - Add exit_coverage_semantics.rs (EXIT_COVERAGE_STATUSES_SQL includes suspect;
      split PROTECTION_CONFIRMED for fill-log triage; first-hard strategy contexts).
    - Wire load_open_exit_qty, protection_flow IF classification, symbol lock, and
      orphan cleanup to the same coverage set; orphan stale flat+flat rows to canceled
      so lock can clear under Option A.
    - Align safety_invariants_audit and doctrine Section 7 (parent_order_id filter).
    - Add scripts/sql/entry_first_hard_protection_replay.sql for post-deploy replay.
    
    Made-with: Cursor
  290. 254ba60
    fix(reporting): prefer pipeline CDV over execution_precheck per trace
    Show commit body
    Use ROW_NUMBER partitioned by decision_trace_id with ordering:
    route_type present first, non-execution_precheck second, id desc.
    Applied to edge survival audit SQL and per-symbol mandate/executable
    CDV ranking in edge_detection.
    
    Made-with: Cursor
  291. b761dd3
    fix(exposure): narrow statuses for partial-fill protection confirmed log
    Show commit body
    detect_partial_fill_protection must not treat cancel_requested or
    reconcile* rows as EXPOSURE_PROTECTION_CONFIRMED; keep OPEN_EXIT_ORDER
    statuses for open-qty aggregation only.
    
    Made-with: Cursor
  292. c5062b7
    fix(execution): count reconcile exits as open coverage; process fill batch; arm trace event
    Show commit body
    - Treat execution_orders in reconcile/reconcile_required as open for exit-qty
      aggregation and partial-fill protection checks, closing false unprotected gaps
      that delayed periodic emergency protection.
    - Runner: apply all executions reports in a batch (no early break); only arm
      exit phase for the active intent symbol; run ensure_protection for each exit fill.
    - Persist exit_lifecycle_armed on entry order_id in execution_order_events after
      lifecycle registration (static vs native trailing path).
    
    Made-with: Cursor
  293. 6d5b94e
    fix(execution): route_type trace on CDV, pullback edge floor, CDV persist errors
    Show commit body
    - Carry V2 RouteType string on Outcome and ExecutionIntent into flow execution.
    - Runner execution_precheck CDV now sets route_type and prefers route_family_for_bias.
    - Log CDV insert failures instead of silent .ok() drops.
    - PullbackContinuation admission uses +1.5 bps stricter net edge floor.
    
    Made-with: Cursor
  294. 68077ae
    feat(edge-exit): drift acceleration uplift, breakout tie rank, earlier BE/trail
    Show commit body
    - Add momentum_acceleration_uplift_for_edge (1m vs 5m aligned drift + optional
      accel_positive bump), capped at 1.10, for breakout path and breakout/momentum
      move-thesis only (no guard changes).
    - On near-tie scores, prefer BreakoutContinuation over pump/dump fade routes.
    - Lower breakeven activation floor (18 bps) in monitor and static-SL lifecycle;
      tighten trail curve reference (230 bps) for earlier distance reduction on profit.
    
    Made-with: Cursor
  295. 2dd551a
    fix(route-expectancy): suppress move-thesis for NoTrade routes
    Show commit body
    NoTrade uses a null path; dispatching MeanReversion thesis from features
    could inflate move_basis while the route is non-plausible. Short-circuit
    to a zero thesis breakdown and make move_thesis_family_for_route(NoTrade)
    unreachable to preserve semantic alignment.
    
    Made-with: Cursor
  296. 38d5752
    Add CDV regression tests, scoped post-fix validation script, idle funnel label.
    Show commit body
    - Expose pipeline CDV cap and route_family mapping for tests; document recycle cdv_persist=false via RECYCLE_SYMBOL_PIPELINE_CDV_PERSIST.
    - run-execution-once uses RUN_ONCE_STRATEGY_PIPELINE_EVALUATION_CYCLE for explicit Some(_) contract.
    - Funnel rows with zero outcomes set top_route_family to __idle_empty__ (resolved_top_route_family_for_funnel_row).
    - scripts/cdv_postfix_validation.sh: decision precheck + CDV/funnel queries scoped to one execution_run_id.
    
    Made-with: Cursor
  297. 84e92cd
    fix(execution): stop recycle CDV fan-out; cap pipeline CDV rows per symbol/family
    Show commit body
    - Add cdv_persist flag to run_strategy_pipeline_v2*; recycle/Loop-A paths pass false so
      candidate_decision_vectors are not written with NULL evaluation_index from high-frequency
      refresh (same root cause as pullback explosion).
    - Cap successful CDV inserts at 64 per (symbol, route_family) per pipeline invocation.
    - run-execution-once uses evaluation_index Some(0) when persisting CDVs.
    
    Made-with: Cursor
  298. 0bba554
    fix(db): keep execution_eval_cycle_funnel created_at on upsert; add updated_at
    Show commit body
    Made-with: Cursor
  299. 4f4ec79
    fix(decision-migration): guard lineage DDL when ingest_epochs absent
    Show commit body
    Made-with: Cursor
  300. 68f623e
    Add execution↔ingest lineage columns and per-eval funnel metrics
    Show commit body
    - Ingest: ingest_epochs.execution_run_id and execution_universe_snapshots.execution_run_id;
      create_epoch/insert snapshot take optional execution consumer id (live_runner passes it).
    - Decision: execution_eval_cycle_funnel table with pipeline vs heap vs orders per evaluation_index.
    - live_runner: dual-run logging (execution_run_id vs market_run_id), FUNNEL_EVAL_NARROWING,
      upsert funnel row on completed eval and no-heap-execute path; exec-only parity.
    - epoch_queries: snapshot_by_id returns execution_run_id.
    
    Made-with: Cursor
  301. f868edb
    fix(obs): truthful eval cycle logs and readiness/CDV timing proof
    Show commit body
    - Replace misleading early LIVE_EVALUATION_STARTED with LIVE_EVALUATION_CYCLE_ENTERED (ingest + execution-only).
    - Log READINESS_ANALYSIS_BEGIN/END around run_readiness_analysis_for_run_from_state.
    - Emit LIVE_EVALUATION_STARTED only after readiness, before readiness CDV spawn; add CDV_READINESS_PERSIST_SPAWNED.
    - Log FIRST_CDV_WRITTEN once per readiness persist spawn (ingest + execution-only).
    - Update l3_capacity_report drift grep to prefer CYCLE_ENTERED with STARTED fallback for old logs.
    
    Made-with: Cursor
  302. da0a942
    feat(obs): counterfactual_targets_v1 pack for ECO cost and lifecycle previews
    Show commit body
    - Extend CDV detail_json with multi-target counterfactual_targets_v1 (read-only).
    - Fix volume LOW_LIQUIDITY scope to liquidity_vacuum activation (+ legacy route key).
    - Add liquidity_low_liquidity_fee parallel cost_band counterfactual (env-gated).
    - Decouple volume counterfactual quantiles from live calibration flag; keep live mix under CALIBRATION_V1 only.
    - Add pullback_continuation_exit_mismatch_preview and momentum_high_vol_spread_preview targets.
    - Add decision-pool SQL checkpoint helper for time-window aggregates.
    
    Made-with: Cursor
  303. 7fa4278
    Add read-only ECO-1A counterfactual observability for Volume low-liquidity.
    Show commit body
    Compute and persist scoped old-vs-new ECO-1A cost and zone deltas for Volume/LOW_LIQUIDITY even when activation is off, so event density and counterfactual impact can be measured without changing admission or order behavior.
    
    Made-with: Cursor
  304. 6aba72a
    Tune ECO-1A Volume low-liquidity calibration coefficients.
    Show commit body
    Adjust the scoped ECO-1A target profile mix and fee multipliers toward maker-dominant assumptions so the route-local cost representation no longer drifts median costs upward under observed Volume low-liquidity baselines.
    
    Made-with: Cursor
  305. 31dba1e
    Add scoped ECO-1A Volume low-liquidity cost calibration.
    Show commit body
    Introduce a flag-gated, route+regime-local ECO-1A override for Volume in LOW_LIQUIDITY that adjusts only fee/profile-mix cost representation and persists old/new mix and cost-band deltas for explicit observability without broad gate changes.
    
    Made-with: Cursor
  306. 83051fa
    fix ECO enrichment on primary strategy-pipeline CDV path
    Show commit body
    Apply route-cost profile and decomposition enrichment when candidate vectors are persisted from strategy_pipeline, so live CDV rows carry ECO-1/ECO-2 fields under the feature flags.
    
    Made-with: Cursor
  307. bf767e9
    add route-profile cost mixture and cost-band observability for ECO-1/ECO-2
    Show commit body
    This adds flag-gated CDV enrichment for execution-profile weighted costs and band semantics, plus a decision-pool monitor SQL to inspect profile mix, component causality, and near-miss clusters without changing runtime thresholds.
    
    Made-with: Cursor
  308. 2c36e84
    sizing: use instrument effective_min_notional upstream in pipeline
    Show commit body
    Previously the pipeline computed size=$10 (BASE_SIZE_MIN_QUOTE) but
    kraken_adapter silently floored qty to qty_min, creating a 3.6x
    exposure overshoot invisible to risk gate, edge calculation, and
    exit calibration.
    
    Fix: compute effective_min_notional = max(qty_min*price, cost_min, 10)
    in the V2 pipeline sizing step so the entire downstream chain (risk
    gate, plan_execution, edge-per-trade) sees the real notional.
    
    kraken_adapter qty_min floor kept as defense-in-depth with warning.
    
    Made-with: Cursor
  309. c161fcc
    unlock: accept qty_min-forced notional overshoot in order prevalidation
    Show commit body
    When exchange qty_min floors the order quantity above the intended
    notional (e.g. RAVE/USD qty_min=18 at $2.01 = $36 vs intended $10),
    the drift check treated this as FINAL_NOTIONAL_DRIFT_TOO_LARGE and
    blocked the order. This prevented all fills for coins where qty_min
    dominates the sizing floor.
    
    Fix: when order_qty is at qty_min floor, accept the exchange-imposed
    notional overshoot instead of bailing. Logged as QTY_MIN_FLOOR_OVERRIDE.
    
    Made-with: Cursor
  310. 777fbee
    unlock: allow new entries through MSP drift gate when no position exists
    Show commit body
    The drift_detected flag in exposure_reconcile blocked ALL entries
    including for symbols with no existing position. This caused an
    infinite re-evaluation loop (~20ms cycles) for viable candidates
    like RAVE/USD that passed every other gate.
    
    Fix: only block on drift when there IS a position to protect.
    halt_state still unconditionally blocks (hard safety preserved).
    
    Rollback: MSP_DRIFT_BLOCKS_NEW_ENTRY=true restores old behavior.
    Made-with: Cursor
  311. 793c849
    docs: update Decimal inventory — Phase 2 complete
    Show commit body
    instruments.rs, auth_ws.rs and 15 callers migrated. Outbound structs
    remain f64 (safe after quantization, deferred to P2 optional).
    Execution core internals downgraded from P0 to P1 (Phase 3) since
    boundary conversions now protect the wire.
    
    Made-with: Cursor
  312. ba0fd0e
    decimal: handle scientific notation (7e-6) in de_decimal deserializer
    Show commit body
    Kraken sends fee amounts like 7e-6 in execution snapshots. The
    Decimal::from_str family rejects scientific notation. Added f64 parse
    fallback in parse_decimal() — only used when exact parsing fails,
    preserving precision for normal numeric strings.
    
    Fixes PRIVATE_EXECUTION_PARSE_ERROR_DURABLE at startup.
    
    Made-with: Cursor
  313. ed7d26b
    decimal: Phase 2 — instruments + order wire migrated to Decimal
    Show commit body
    InstrumentConstraints fields (qty_min, qty_increment, price_increment,
    cost_min) now store Decimal. InstrumentPair serde uses de_decimal for
    lossless JSON→Decimal parsing from Kraken instrument channel.
    
    normalize_order/normalize_exit_qty use exact Decimal arithmetic —
    eliminates the quantize_wire_f64 string-reparse workaround for these paths.
    quantize_limit_price_for_kraken_wire and quantize_order_qty_for_kraken_wire
    now operate on Decimal.
    
    auth_ws order functions (add_order, add_stop_loss_order,
    add_trailing_stop_order, amend_order, amend_stop_loss_trigger,
    amend_trailing_stop_trigger_pct) accept Decimal for price/qty/trail_bps.
    Outbound JSON structs (AddOrderParams etc.) remain f64 — conversion
    happens via dec_to_wire() after quantization (safe: limited precision).
    
    All 15 downstream callers updated with boundary conversions
    (dec_f helper for f64→Decimal, .to_f64() for Decimal→f64).
    All 14 instrument tests pass.
    
    Made-with: Cursor
  314. 694b392
    docs: update Decimal inventory — Phase 1 serde ingress complete
    Show commit body
    Mark TradeData, TickerData, ExecutionReport/Fee as migrated.
    Split outbound order params (AddOrderParams, TriggerParams, etc.)
    as remaining P0 for Phase 2.
    
    Made-with: Cursor
  315. 950bdd3
    decimal: ExecutionReport/ExecutionFee f64→Decimal at serde boundary
    Show commit body
    All inbound private WS execution fields (order_qty, limit_price,
    cum_qty, cum_cost, last_qty, last_price, fee qty) now deserialize
    to Decimal via RawValue. ws_handler no longer needs Decimal::try_from(f64)
    conversions. compute_fill_price uses pure Decimal arithmetic.
    execution_holdings_cache converts to f64 at consumption (estimator, not SSOT).
    deterministic_proof test helpers use Decimal directly.
    
    Phase 1 of DECIMAL_F64_POLICY_AND_INVENTORY.md migration.
    
    Made-with: Cursor
  316. c2657ac
    decimal: TickerData bid/ask/last/qty f64→Decimal at serde boundary
    Show commit body
    Same RawValue-based lossless deserializer for ticker fields.
    price_cache still receives f64 via .to_f64() at call site (Phase 5).
    DB storage path (ticker_data_to_row) now uses Decimal directly.
    
    Made-with: Cursor
  317. 3ead075
    decimal: TradeData price/qty f64→Decimal at serde boundary
    Show commit body
    Use RawValue-based custom deserializer (de_decimal) to parse
    Kraken JSON numbers directly to Decimal without f64 precision loss.
    Downstream consumers (trade_flow_window, horizon_movers) still receive
    f64 via explicit .to_f64() at call site; trade_data_to_row now uses
    Decimal directly for DB storage path.
    
    Phase 1 of DECIMAL_F64_POLICY_AND_INVENTORY.md migration.
    
    Made-with: Cursor
  318. 93e95b8
    policy: Decimal/f64 invariants for price-qty execution paths
    Show commit body
    Add always-on Cursor rule, AGENTS.md pointer, and SSOT inventory doc
    with phased refactor plan. Motivated by L2 checksum production failure
    from float precision loss; no code refactor in this change.
    
    Made-with: Cursor
  319. e830f33
    promote ROUTE_ECONOMY_SHADOW_DIVERGENCE from debug to info
    Show commit body
    Shadow/compare divergences between the route-specific and generic
    economic paths need to be visible in production logs for Fase E
    validation.
    
    Made-with: Cursor
  320. b96e944
    remove L2 checksum diagnostic logging after mismatch fix confirmed
    Show commit body
    0/277555 mismatches over 10min with the string-precision + depth
    truncation fix. Remove L2_CHECKSUM_DIAG_FIRST_MISMATCH logging and
    checksum_payload_debug() method.
    
    Made-with: Cursor
  321. 8188ba4
    fix L2 checksum: truncate book to subscribed depth after each update
    Show commit body
    Kraken docs: "after each update, the book should be truncated to your
    subscribed depth as you won't receive qty: 0 for price levels that
    fall out of scope." Without truncation, books grew to 41+ levels
    (subscribed depth = 25), causing stale levels to persist and checksum
    mismatches for high-activity pairs (BTC, ETH, TAO, RAVE).
    
    After previous fix brought mismatch rate from 98.7% to 6%, this
    addresses the remaining 4 symbols.
    
    Made-with: Cursor
  322. 6fdaa1c
    fix L2 checksum: single Decimal path from raw strings, no f64 intermediary
    Show commit body
    Root cause of remaining mismatches: f64_to_decimal() and
    Decimal::from_str_exact() produced different Decimal values for the
    same price, causing BTreeMap key mismatches between the numeric book
    and the raw string map. payload_len=14 instead of expected ~300.
    
    Fix: eliminate the dual-path entirely. OrderBook now only uses
    apply_snapshot_from_strings/apply_update_from_strings which create
    Decimal keys via from_str_exact. Both the numeric BTreeMap and the
    checksum string BTreeMap use the same Decimal key. No f64 involved.
    
    Made-with: Cursor
  323. cae1b02
    add L2_CHECKSUM_DIAG_FIRST_MISMATCH diagnostic for checksum debugging
    Show commit body
    Logs the checksum payload, raw string availability, and exchange vs
    local values on the first mismatch per symbol to identify remaining
    checksum issues.
    
    Made-with: Cursor
  324. 965ae90
    fix L2 checksum: use RawValue to preserve exact Kraken wire precision
    Show commit body
    Root cause: Kraken sends price/qty as JSON numbers with trailing zeros
    (e.g. 0.10000000). serde_json parsed these as f64 (0.1), losing the
    trailing zeros. The CRC32 checksum then produced "1" instead of the
    expected "10000000", causing 98.7% mismatch rate.
    
    Fix:
    - Enable serde_json raw_value feature
    - Add BookLevelRaw/BookDataRaw/BookMessageRaw structs that deserialize
      price/qty as Box<RawValue>, preserving the exact JSON number text
    - OrderBook now stores parallel bid_strings/ask_strings BTreeMaps with
      the original wire strings alongside the Decimal values
    - process_ws_message does a dual parse: f64 BookMessage for computation,
      BookMessageRaw for checksum strings
    - checksum_top10() reads original strings instead of Decimal::normalize()
    - extract_raw_strings() bridges RawValue to (Decimal, String, String)
    
    Made-with: Cursor
  325. 8391b39
    fix L2 checksum: preserve original string precision from Kraken WS
    Show commit body
    Root cause: BookLevel deserialized price/qty as f64, losing trailing
    zeros (e.g. "0.10000000" → 0.1). The CRC32 checksum then used
    Decimal::normalize() which further stripped precision, producing
    completely different checksum strings than Kraken expects.
    
    Fix:
    - BookLevel now deserializes price/qty as String (with fallback for
      numeric JSON values)
    - OrderBook stores original strings in parallel BTreeMaps
      (bid_strings, ask_strings) alongside Decimal values
    - checksum_top10() uses original strings for CRC32 calculation
    - checksum_field_str() replaces checksum_field(): operates on raw
      strings instead of Decimal::normalize()
    
    This matches the Kraken WS v2 spec exactly: strip dot, strip leading
    zeros, concatenate asks (ascending) then bids (descending), CRC32.
    
    Made-with: Cursor
  326. 135174f
    promote HORIZON_HOT_STATE_BUDGET log from debug to info
    Show commit body
    Makes horizon memory budget, entry count, and tradability count
    visible in production logs for Phase D validation.
    
    Made-with: Cursor
  327. 1ee7aa0
    add FAST_START_SIZING log line: base, ratio, reduced, final, floor_applied
    Show commit body
    Emits a structured tracing::info on every apply_fast_start_sizing call
    when fast_start_active=true, making the ratio observable even when the
    BASE_SIZE_MIN_QUOTE floor dominates.
    
    Made-with: Cursor
  328. 79f31d8
    Fix review findings: remove spurious ROUTE_SPECIFIC_ECONOMY dependency, correct mirror terminology, reduce initial ring capacity
    Show commit body
    - Remove ROUTE_SPECIFIC_ECONOMY → EXECUTION_L2_BOOK_ONLY dependency (was not in agreed model)
    - Reduce ring VecDeque initial capacity from 4096 to 256 (saves ~94KB/window on init)
    - memory_budget_bytes() now uses capacity() not len() for honest allocation reporting
    - MirrorHealth: rename queue_depth → batch_size, add backlog, flush_duration_ms
    - Track LAST_BACKLOG atomic for failed-write visibility
    - HORIZON_MIRROR_HEALTH log now shows batch_size + backlog separately
    - blocker_chain serialised as pipe-separated in mirror + logs for consistency
    
    Made-with: Cursor
  329. baecb78
    Harden selector foundation: flag validation, bounded buffers, L2-mid sampling, blocker_chain, shadow compare, mirror backpressure, selector latency
    Show commit body
    Phase C hardening:
    - Startup flag validation: invalid combos (e.g. FAST_START without L2_BOOK_ONLY) cause hard bail
    - Bootstrap ∩ L2 intersection logging with pool/l2/intersection counts
    - Silent exclusion reason_codes for bootstrap drops (rank_below_top_n, no_execution_l2)
    
    Phase D hardening:
    - Hard ring buffer cap (MAX_RING_SIZE=8192) prevents unbounded memory growth
    - L2-mid sampling (sample_l2_mid) prevents trade-only freeze in horizon windows
    - blocker_chain in TradabilityState (multi-blocker visibility)
    - Memory budget logging (HORIZON_HOT_STATE_BUDGET)
    - Mirror backpressure metrics: timing, lag, error tracking, health logging
    
    Phase E hardening:
    - Shadow/compare mode: old generic path evaluated alongside route-specific, divergences logged
    - Per-route suppression metrics (ROUTE_SUPPRESSION_METRICS)
    
    Phase F hardening:
    - Selector latency measurement (elapsed_us in return value + logs)
    - Best-score-per-symbol across horizons (HashMap replaces first-match HashSet)
    - Tradability score transparency (components string in candidate output)
    - blocker_chain carried through selector candidates
    
    Migration: add blocker_chain column to horizon_state_mirror
    Made-with: Cursor
  330. ccb04fe
    Wire selector foundation into runtime (Phases C-F)
    Show commit body
    - Fast-start: build_bootstrap_snapshot at startup when FAST_START_ENABLE,
      apply_fast_start_sizing at all three pipeline sizing points, deactivate
      on first full universe refresh
    - Hot state: record_trade in trade WS feed (gated by set_enabled),
      tradability::upsert in evaluation loop, flush_mirror periodic 5s task
    - Route economy: route_specific_economy threaded through readiness analysis
      and all evaluate_pair_readiness call sites
    - Selector V2: select_candidates called in V2 pipeline inner when
      SELECTOR_V2_ENABLE, filters tradable candidates by horizon-first set
    - All flags default false; existing behavior unchanged when flags off
    
    Made-with: Cursor
  331. 1f6f058
    P4: horizon-first selector module
    Show commit body
    - pipeline/horizon_selector: select_candidates() ranks symbols by
      realized_move_bps * tradability_score across all horizons
    - Top-N per horizon union with first_blocker visibility
    - Reads from in-memory hot state only (horizon_movers, tradability,
      l2_book_registry) -- no DB scans in selector path
    
    Made-with: Cursor
  332. 6c2077a
    P3: route-specific economy in readiness gate
    Show commit body
    - evaluate_pair_readiness: new `route_specific_economy` parameter
    - When true: pre-route only checks data_stale (safety); all spread/
      surplus/edge checks move into per-strategy match arms with route-
      specific thresholds (Liquidity 80bps, Momentum 30bps, Volume 50bps)
    - When false: old generic pre-route chain completely unchanged
    - New evaluate_route_specific() helper with per-strategy economic gates
    - All existing call sites pass false (behavioral parity until flag on)
    
    Made-with: Cursor
  333. f8d84e6
    P2: horizon hot state + tradability + mirror
    Show commit body
    - state/horizon_movers: DashMap-backed rolling windows per symbol x
      horizon (1m/3m/5m/10m/15m) with realized_move, range, vol_proxy
    - state/tradability: DashMap-backed per-symbol tradability state
      (spread, l2 freshness, stale detection, first_blocker)
    - state/horizon_mirror: flush_mirror() for 5s micro-batch UPSERT
      to horizon_state_mirror on DECISION pool
    - migration: CREATE TABLE krakenbot.horizon_state_mirror (additive)
    
    Made-with: Cursor
  334. d595d7b
    P1b: bootstrap universe scoring, fast-start sizing, exclusion logging
    Show commit body
    - universe: add build_bootstrap_snapshot() with simplified Pool-layer
      scoring (50% trade + 30% spread + 20% ticker) for fast-start
    - universe: add UNIVERSE_EXCLUSION debug logging at all filter points
      (below_min_activity, cooldown_active, universe_cap_exceeded)
    - sizing: add apply_fast_start_sizing() multiplier for bootstrap phase
    
    Made-with: Cursor
  335. de5b3fd
    P1a: execution-side L2 book-only + registry enhancements
    Show commit body
    - l2_feed::run_l2_feed now takes Option<WriterSender>; when None,
      book updates still populate l2_book_registry but DB writes are skipped
    - live_runner: when ingest_provides_data=true AND EXECUTION_L2_BOOK_ONLY=true,
      spawn L2 WS feed with writer=None (book-only mode)
    - l2_book_registry: add count(), all_symbols(), freshness_ms(), stale_symbols()
    - All existing call sites updated to pass Some(writer)
    
    Made-with: Cursor
  336. 0088a44
    P0: add selector V2 feature flag config fields
    Show commit body
    Seven feature flags for phased selector foundation rollout:
    - SELECTOR_V2_ENABLE (master switch)
    - EXECUTION_L2_BOOK_ONLY (P1: execution-side L2 WS)
    - FAST_START_ENABLE (P1: bootstrap universe)
    - FAST_START_UNIVERSE_SIZE, FAST_START_SIZING_RATIO (P1 params)
    - HORIZON_HOT_STATE_ENABLE (P2: rolling window state)
    - ROUTE_SPECIFIC_ECONOMY (P3: per-route economic checks)
    
    All default false / no behavioral change until explicitly enabled.
    
    Made-with: Cursor
  337. 4737664
    fix(observability): record last L2 book event time for ingest lag proof
    Show commit body
    - Track last book snapshot/delta wall time per OrderBook and store in
      l2_snap_metrics.ts_exchange (Kraken book path has no trusted exchange clock here).
    - Clarify CDV book_age_ms as ticker WS recency, not L2 book age.
    
    Made-with: Cursor
  338. 050736d
    docs(systemd): note enable --now for edgeboard tuning timer
    Show commit body
    Made-with: Cursor
  339. de44f81
    fix: preserve spaces in snapshot_ts when building tuning metrics SQL
    Show commit body
    Made-with: Cursor
  340. ff755df
    feat: hourly edgeboard cost spread tuning (phase 1)
    Show commit body
    - Env EDGEBOARD_COST_SPREAD_FACTOR with Rust clamp [0.5,1.0]; explainability_json.cost_spread_factor
    - RESEARCH tables: edgeboard_cost_tuning_state, edgeboard_cost_tuning_iterations
    - Runner: db precheck, metrics SQL, decide.py state machine, persist transaction, markdown report
    - Optional auto-apply via EDGE_TUNING_AUTO_APPLY + .env.edgeboard_tuning + systemd restart
    - systemd timer/service; ingest/execution load optional env drop-in for promoted factor
    - Portable lock when flock(1) unavailable (macOS)
    
    Made-with: Cursor
  341. 1b38ea1
    fix(shadow): persist parity_detail_json without price_cache via MSP/L2 fallback
    Show commit body
    - resolve_v2_shadow_entry_price: price_cache, then market_state_projection, then ingest l2_snap_metrics
    - insert_v2_shadow_trade(_directed): pass ingest pool + data_run_id; bind entry_price_source
    - Shadow insert on no_price_snapshot_for_maker (previously continued without shadow)
    - insert_shadow_trades_engine_mode_blocked: optional ingest + data_run_id for price resolution
    
    Made-with: Cursor
  342. c35430a
    feat(observability): slippage, hold-time, and fee impact in public trading snapshot
    Show commit body
    Add decision-DB aggregates for dashboard economics: slippage delta vs CDV
    estimate, closed-position hold benchmarks (1m/3m/15m), and fee burn vs
    24h net realized PnL. Document fields in OBSERVABILITY_SNAPSHOT_CONTRACT.
    
    Made-with: Cursor
  343. 4a7f0d0
    fix(route_expectancy): add move_basis_override_bps to evaluate_route_candidate
    Show commit body
    Callers (route_selector, route_selector_v2) already pass the 13th argument;
    committed tree was missing the parameter and body wiring, breaking release build.
    
    Made-with: Cursor
  344. cb3ff3a
    feat(shadow): persist parity_detail_json on shadow_trades insert
    Show commit body
    - Decision migration for parity_detail_json JSONB
    - Wire assess_shadow_parity into legacy L2 and V2 shadow inserts
    - CHANGELOG unreleased note
    
    Made-with: Cursor
  345. 94136ef
    fix(route,cdv): persist edge_source; ignition label; shadow parity tests
    Show commit body
    - CDV INSERT + CandidateDecisionVector.edge_source; strategy_pipeline detail_json
    - Set EdgeSource::IgnitionShifted on legacy ignition enrich and V2 when vol present
    - Shadow parity tests for edge-floor economic mismatch vs comparable_to_live
    - Decision migration: candidate_decision_vectors.edge_source
    - CHANGELOG: unreleased notes for CDV + D8.3 raw_private_executions dedup
    
    Made-with: Cursor
  346. 86964c3
    Fix shadow parity comparable check; defer high-edge trace clone.
    Show commit body
    - assess_shadow_parity: split invariant unmodeled live gates from conditional
      gaps; apply <=1 only to conditional_missing (removes len<=4 tautology).
    - route_selector_v2: compute max expected_net_edge_bps without cloning;
      clone/sort only when top_edge >= 15 for HIGH_EDGE_CANDIDATE_TRACE.
    - Add unit tests for assess_shadow_parity.
    
    Made-with: Cursor
  347. a754cb6
    fix(route_engine): align horizon scaling docs, exit_drag×horizon_factor, passive hf_cap
    Show commit body
    - DRY wait_risk: use RouteHorizon::horizon_factor (same √(T/T_H3m) as before)
    - D4.2: scale exit_drag_bps by horizon_factor so drag grows H1m→H15m like wait_risk
    - Passive spread: apply capped horizon_factor in weak branch (was horizon-blind)
    - Document momentum_time_factor vs horizon_factor; clarify time_adjusted_score math
    - Single hold_secs binding from effective_max_hold_secs; test horizon_factor monotonicity
    - Note candidate matrix uses H3m/H10m only, not full RouteHorizon::ALL
    
    Made-with: Cursor
  348. 889ff1b
    fix(edge_heap): pass net edge into spread weight for execution_realism_score
    Show commit body
    60-80 bps conditional weights were only used in the heap hardkill check;
    realism used spread_bucket_weight(..., None) → 0.0 → 0.05 clamp, so scores
    ignored the intended 0.40-0.70 band. Align realism with spread_bucket_weight_with_edge(edge).
    
    Add regression tests for conditional pass vs fail in the wide-spread band.
    
    Made-with: Cursor
  349. e85a66f
    fix(observe): correct UNIVERSE_DIFF overlap count + apply limit on fallback
    Show commit body
    - Overlap was 2×|A∩B|; use |A|−|A\B| (equals |B|−|B\A| on deduped execution set)
    - Truncate hardcoded fallback when OBSERVE_SYMBOL_LIMIT is set (empty cache / error)
    
    Made-with: Cursor
  350. e92c7a1
    D9.1-D9.10: connect all spot strategy families with dedicated move_thesis modules
    Show commit body
    - Create 7 new move_thesis modules: order_flow_imbalance, atr_breakout,
      ignition_momentum, tod_overlay, liquidity_sweep, scalping, grid
    - Wire all 14 StrategyFamily variants into exhaustive dispatch (no fallback)
    - Extend move_thesis_family_for_route to context-aware selection: market
      features determine when specialized families override the default
    - Add utc_hour field to MarketFeatures for TodOverlay time-of-day thesis
    - Test: all_families_have_dedicated_dispatch verifies no path_fallback
    - Audit: D9.1 ActivatedStrategy reachability confirmed (57 refs)
    - Audit: D9.2 mapping exhaustive, D9.3 ignition route_map clean
    
    Made-with: Cursor
  351. 25dd8e5
    D8.1-D8.3: fill pipeline integrity + exchange_trade_id unique index
    Show commit body
    - D8.1: escalate upsert failure from warn to error with atomic counter
    - D8.2: wire record_fill_feedback in ws_handler fill processing path
    - D8.3: partial unique index on exchange_trade_id (WHERE NOT NULL)
      with conflict handling for duplicate trade IDs with different event UIDs
      (migration: 20260410140000)
    
    Made-with: Cursor
  352. b6ec604
    D7.1-D7.2: shadow direction fix + ShadowParityRecord
    Show commit body
    - insert_v2_shadow_trade_directed: direction parameter replaces LONG hardcode
    - ShadowParityRecord struct for structural parity assessment per shadow
      candidate: direction_correct, execution_eligible, missing_live_gates,
      comparable_to_live, parity_gaps
    - assess_shadow_parity function for building parity records
    
    Made-with: Cursor
  353. 552f342
    D6.1+D6.3: choke_decide distinct readiness_allows + SymbolLockReason enum
    Show commit body
    - choke_decide now receives pipeline readiness from Outcome.decision
      instead of duplicating system_live_ready — readiness_blocked branch
      is now operationally reachable
    - SymbolLockReason enum with ActiveOrders/SafetyStateLocked/
      PositionTruthDegraded/MspHaltOrDrift/UnprotectedExposure
    - check_symbol_execution_lock_reason returns (bool, Option<Reason>)
      for explicit lock explainability
    
    Made-with: Cursor
  354. 5615b70
    D5.1-D5.2: candidate survival_stage + HIGH_EDGE_CANDIDATE_TRACE
    Show commit body
    - CandidateDecisionVector gains survival_stage and edge_source fields
    - HIGH_EDGE_CANDIDATE_TRACE for top-3 candidates when best net_edge >= 15bps
      with full cost decomposition per candidate for disappearance forensics
    
    Made-with: Cursor
  355. 96fd078
    D4.1-D4.6: cost model recalibration + spread conditional 80bps + tape gate log
    Show commit body
    - wait_risk_bps = f(entry_mode, horizon, exit_regime) — structural economic model
    - route-specific exit_drag_bps multiplier (fade/passive differentiated)
    - maker-specific move_fee_ratio relaxation (0.90x floor)
    - L3 penalty differentiated by entry_mode (maker=0.60, taker=0.85)
    - spread_bucket_weight: hardkill raised to 80bps; 60-80bps conditional
      on net_edge > spread*0.5 (temporary safety rail for 60-80bps pass)
    - TAPE_GATE log with tape_count, tape_min, result per symbol
    
    Made-with: Cursor
  356. c367eea
    D3.1-D3.2: edge_source in ROUTE_EDGE_TABLE + edge_score_source passthrough
    Show commit body
    - ROUTE_EDGE_TABLE log includes edge_source label for audit trail
    - ExecutionIntent gains edge_score_source from Outcome passthrough
    - flow_execution propagates source to intent for downstream tracing
    
    Made-with: Cursor
  357. 10fd4f7
    D2.1-D2.4: horizon term structure + EdgeSource + edge_score_source
    Show commit body
    - RouteHorizon expanded to H1m/H3m/H5m/H10m/H15m with sqrt-time scaling
    - EdgeSource enum (V1Linear, IgnitionShifted, AdaptiveScenario*, Bootstrap)
    - RouteCandidate gains edge_source, ignition_input_shifted,
      adaptive_move_input_used, adaptive_scenario_move_bps
    - Outcome gains edge_score_source for downstream audit trail
    - All existing Short/Medium references mapped to H3m/H10m (preserving
      exact max_hold_secs values)
    
    Made-with: Cursor
  358. 730792c
    D1.1-D1.4: instrument-cache primary source for observe universe
    Show commit body
    - resolve_observe_symbols() uses instrument cache as primary source;
      hardcoded 50 symbols become explicit degraded-mode fallback
    - OBSERVE_UNIVERSE_RESOLVED log at startup with source/degraded flag
    - DEGRADED_OBSERVE_MODE warning when fallback is active
    - UNIVERSE_DIFF log in live_runner showing observe vs execution gap
    - FEATURE_INCOMPLETE_REJECT per-symbol log with has_spread/has_micro/l2_count
    - OBSERVE_SYMBOL_LIMIT env added for optional primary cap
    
    Made-with: Cursor
  359. d415139
    fix(retention): bash integer parse for eligible count
    Show commit body
    Made-with: Cursor
  360. 24b8e10
    harden(retention): forensic reconciliation gate, closure_reason, locking
    Show commit body
    Retention script rewritten with forensically hardened safety:
    
    1. RECONCILIATION GATE before any delete:
       - row_count match (Postgres vs sum of manifest row_counts)
       - symbol_count match (distinct symbols in PG vs manifest partitions)
       - checksum_sha256 re-verified on disk per Parquet file
       - parquet file existence and non-zero size check
       - durable reconcile log (.reconcile_log JSONL)
       - per-run pass/fail verdict; failed runs are never deleted
    
    2. ORPHAN SEMANTICS:
       - Migration adds closure_reason column to observation_runs
       - Retention script sets closure_reason='retention_orphan_close'
       - Normal shutdown paths set closure_reason='normal'
       - NULL = legacy/unclassified (backwards compatible)
    
    3. DELETE SAFETY:
       - Delete targets run_id (indexed), not ts_local (avoids cross-run leaks)
       - l2_snap_metrics has 0 FK constraints in/out (verified)
       - Post-delete verification: remaining rows must be 0
       - Idempotent: re-running skips already-deleted runs
    
    4. OPERATIONAL:
       - flock single-instance guard
       - Disk headroom precheck (default 50 GB minimum)
       - Timezone documented: Europe/Amsterdam (server timedatectl)
       - Restart=no on failure (investigate, don't retry blindly)
       - TimeoutStartSec=7200 for large exports
    
    Made-with: Cursor
  361. 72e9325
    feat(retention): automated l2_snap_metrics lifecycle
    Show commit body
    Adds scripts/l2_retention_cycle.sh with a 5-step safe pipeline:
    1. Close orphaned observation runs (ended_at IS NULL, inactive >48h)
    2. Parquet cold export via existing export-parquet-cold
    3. Verify Parquet manifests before any deletion
    4. Batch DELETE in chunks of 500K rows (avoids long locks)
    5. VACUUM ANALYZE to reclaim space
    
    Includes systemd timer (04:30 daily) and --dry-run mode for testing.
    
    Context: l2_snap_metrics grows ~34 GB/day for 600 markets; without
    retention the 616 GB free disk fills in ~18 days. 14-day retention
    keeps data useful for edgeboard training (7-day lookback) with margin.
    
    Made-with: Cursor
  362. 9aba067
    fix(ingest): wire execution universe into periodic MMS + honest spawn log
    Show commit body
    - Share one Arc<RwLock<Vec<String>>> with spawn_periodic_microstructure; clone_from
      snapshot.execution after each commit_snapshot (initial + refresh loop), matching live_runner.
    - spawn_periodic_microstructure returns bool; ingest logs SPAWNED only when true, else NOT_STARTED
      with obs/research/scope fields. Module docs note ingest updates the shared list.
    
    Made-with: Cursor
  363. af094b1
    fix(ingest): spawn periodic MMS capture in ingest runner
    Show commit body
    The ingest runner was missing spawn_periodic_microstructure, causing
    market_microstructure_snapshots on research to go stale. Without fresh
    MMS data, all edgeboard candidates are filtered by the freshness gate
    (max 600s for 15m horizon), resulting in rows_scored=0 and no snapshot.
    
    Also set EDGEBOARD_LOOKBACK_DAYS=7 on server (.env) since data is only
    6 days old — the 90-day default was causing unnecessary full scans on
    the 470M-row l2_snap_metrics table.
    
    Made-with: Cursor
  364. 9c04a32
    fix: spawn edgeboard refresh worker in run-ingest + export provenance fields
    Show commit body
    Root cause: run-ingest mode did not spawn the edgeboard refresh worker,
    causing research snapshots to go stale and the dashboard to fall back to
    a single decision CDV row. This commit:
    
    1. Spawns `edgeboard_refresh_worker` in `ingest_runner.rs` (matching
       observe/live_runner behavior) so research snapshots are produced every
       60s when RESEARCH pool is available.
    
    2. Adds `edge_source_version` per signal row ("edgeboard_research" vs
       "decision_fallback") for dashboard provenance transparency.
    
    3. Adds `snapshot_age_ms` on Tier2EdgeboardSection for snapshot freshness
       visibility in the export payload.
    
    Made-with: Cursor
  365. 6c7080a
    fix: restore ignition_confidence propagation on capital events
    Show commit body
    Re-adds the ignition_confidence assignments removed in f38b0d1. The
    DB migration (20260410130000) is now applied, so the column exists.
    
    Made-with: Cursor
  366. f38b0d1
    Fix server compile mismatch in funnel event fields.
    Show commit body
    Remove non-portable ignition_confidence assignments from capital funnel event writes so deploy builds cleanly against current server schema/state.
    
    Made-with: Cursor
  367. da9aead
    feat: regime follow-ups — parquet export, ignition_confidence, monitoring
    Show commit body
    1. regime_origin added to parquet cold export (struct, SQL, builder, schema)
    2. ignition_confidence on funnel events (DB column + code propagation
       on CAPITAL_ALLOCATION_DECISION events)
    3. regime_missing_monitor.sql — monitoring queries for REGIME_MISSING
       events, regime origin distribution, confidence bands, bootstrap coverage
    
    Made-with: Cursor
  368. f3d1060
    Harden pre-wire notional guards across execution submit paths.
    Show commit body
    Enforce canonical wire-notional validation on entry, exit/protection, and probe add_order flows to prevent unchecked submits from bypassing sizing safety invariants.
    
    Made-with: Cursor
  369. 98e7f8a
    docs: production hardening regime reliability report
    Show commit body
    Full audit: root cause elimination, hard fail-closed, V1/V2 parity,
    regime contract, production readiness acceptance criteria.
    
    Made-with: Cursor
  370. 1f2adeb
    fix(observability): edgeboard visible snapshot + ingest-universe filter for public route board
    Show commit body
    - Prefer latest_visible_edgeboard_* for research rows; fall back if empty
    - Fetch extra rows then filter to run_symbol_state symbols; align bundle run_id with status_run_id (epoch)
    - Add ingest_run_symbols query; renumber exported ranks after filter/truncate
    
    Made-with: Cursor
  371. d39e975
    feat: production-hardening regime reliability
    Show commit body
    Root cause fixes:
    - V1 path: gap-fill ignition states for ALL symbols (synthetic metrics
      for symbols without trade data → engine returns Quiet with confidence 0.3)
    - V2 path: add IgnitionStateMachine computation (was completely missing)
      + same gap-filling as V1
    - Both paths now guarantee ignition_state = Some(...) for every candidate
    
    Hard fail-closed:
    - flow_execution.rs: replace .unwrap_or(Quiet) with hard block + REGIME_MISSING
      funnel event when ignition_state is None
    - runner.rs: same pattern for run-execution-once path
    - shadow_compare.rs: replace Quiet defaults with regime_missing
    
    Production contract:
    - New regime_contract module with RegimeContract struct
    - Validates observed origin, state presence, confidence
    - 3 unit tests for valid/invalid/all-states scenarios
    
    Reporting:
    - COALESCE(..., 'Quiet') → 'regime_missing' in throughput report
    
    Made-with: Cursor
  372. 7d725fc
    docs: add regime truth audit report
    Show commit body
    Documents the full regime provenance analysis: 99.998% of regime data
    was defaulted (not observed), provenance tracking now live, soft
    fail-closed policy recommended, historical data tagged with honest
    origin labels.
    
    Made-with: Cursor
  373. f62136f
    docs: changelog NL/EN/DE/FR + CHANGELOG_ENGINE for public trading UTC-day PnL (e13851f)
    Show commit body
    Made-with: Cursor
  374. dc7b237
    fix: propagate ignition_state to all candidates, not just actionable
    Show commit body
    Previously, ignition_state was only set on RouteCandidate when the
    state was Ignition/Expansion/Continuation (actionable). Quiet,
    Compression, and Exhaustion states from the engine were discarded,
    making observed Quiet indistinguishable from defaulted (no engine data).
    
    Now all candidates + winner receive the engine's state when available,
    enabling regime_origin provenance tracking.
    
    Made-with: Cursor
  375. e13851f
    feat(observability): UTC-day symbol PnL top winners/losers in public trading export
    Show commit body
    - realized_pnl_sum_by_symbol_utc_today + top 3 positive / 3 most negative
    - Document contract in OBSERVABILITY_SNAPSHOT_CONTRACT
    - Ignore KapitaalBot-Website/ clone under engine repo
    
    Made-with: Cursor
  376. 5b518a2
    feat: add regime_origin provenance to funnel events and CDV
    Show commit body
    Distinguish observed vs defaulted ignition_state in live data.
    - New column regime_origin on trading_funnel_events and candidate_decision_vectors
    - Code patches: flow_execution.rs, runner.rs, strategy_pipeline.rs set origin
      based on whether ignition_state was Some (observed) or None (defaulted)
    - Migration: backfills historical data with derived_from_legacy_other / defaulted
    - Validation SQL queries for provenance distribution analysis
    
    Made-with: Cursor
  377. daa7c25
    feat(universe): pre-filter fiat and stable base assets
    Show commit body
    Exclude fiat-base and stablecoin-base symbols before universe construction so non-actionable markets never enter ingest/execution selection for NL/EEA live runs.
    
    Made-with: Cursor
  378. eb79c0b
    refactor(taxonomy): eliminate Other/Unknown from live allocation and reporting
    Show commit body
    Removes all catch-all buckets from live capital allocation, gating,
    and reporting paths. Every candidate now gets an explicit, economically
    meaningful label.
    
    RegimeBucket enum:
    - Removed: Other (catch-all for Quiet+Compression+Exhaustion)
    - Added: Quiet (no ignition, default), Compression (pre-breakout),
      Exhaustion (fading momentum) — each with distinct sizing multiplier
    - regime_bucket() now exhaustively matches all 6 IgnitionStateType variants
    
    Other removals:
    - PairClass::Unknown (dead variant, never constructed)
    - FillStyle::Unknown → FillStyle::Undetermined (descriptive)
    - "unknown" route_family defaults → "no_route", "no_trade", "no_trading"
    - "Unknown" regime defaults → "Quiet" (factual default state)
    - COALESCE(..., 'unknown') in SQL → descriptive labels (no_status, no_side, etc.)
    
    Legacy migration:
    - export.rs maps historical "Other" bucket → "quiet" for dashboard continuity
    
    Sizing multipliers (new):
      Quiet=0.7, Compression=0.85, Ignition=1.0,
      Expansion=1.1, Continuation=0.9, Exhaustion=0.6
    
    Proof: 0 occurrences of RegimeBucket::Other, PairClass::Unknown,
    FillStyle: :Unknown remain in src/**/*.rs
    Made-with: Cursor
  379. 80211f7
    fix(allocator): remove regime-bucket fragmentation, single-pool 98% deploy
    Show commit body
    Root cause: 152k INSUFFICIENT_CAPITAL rejects because regime bucketing
    gave "Other" bucket only 10% of equity (max 1 slot). With $130 equity
    this means $13 budget — barely 1 position while $117 sat unused.
    
    Changes:
    - Single-pool model: total budget = 0.98 * equity (no per-regime split)
    - Default max slots: 5 → 32 (configurable via MAX_CAPITAL_TOTAL_SLOTS)
    - Per-symbol limit = 1 (unchanged, good guardrail)
    - Edge-weighted sizing formula retained (sigmoid * confidence * liquidity)
    - base_size floor aligned to $10 (was $20 in allocator)
    - max_per_trade floor-guarded: max(equity * 0.25, $10)
    - Regime bucket kept as observability tag + mild sizing multiplier
    - 5 unit tests (pool capacity, floor reject, symbol dedup, edge scaling, deploy cap)
    
    Impact (historical replay, 20 recent runs, $130 equity):
    - Old: avg 1.0 positions admitted (budget/slot exhausted)
    - New: avg 3.9 positions admitted (all candidates that have positive edge)
    - Edge capture: 351 bps → 536 bps avg per run (+53%)
    
    Made-with: Cursor
  380. c1460d2
    fix(execution): enforce official EEA restriction universe filter
    Show commit body
    Filter symbols against Kraken's official EEA 'cannot deposit or trade' list before execution universe selection so restricted assets like CHECK/USD are excluded proactively in NL/EEA runs.
    
    Made-with: Cursor
  381. 9ecaf77
    fix: add quote_wall_ts: None to all test MarketFeatures constructors
    Show commit body
    All 370 tests pass.
    
    Made-with: Cursor
  382. 4b566a0
    P1: add thesis_family + expected_move_bps_at_decision to execution_orders
    Show commit body
    Enables post-trade calibration: realized fill outcome vs expected move,
    segmented by thesis family, on the orders table instead of only CDV.
    
    - Migration: 20260410100100_execution_orders_thesis_family.sql
    - Add fields to SubmitMetrics, ExecutionIntent, insert_order
    - Flow execution populates from selected_outcome.route_family and expected_move_bps
    - Legacy callers (emergency_stop, external backfill) pass None
    
    Made-with: Cursor
  383. 1533272
    P1: add quote_ts_source + book_age_ms to CDV for staleness auditing
    Show commit body
    - Add wall_updated_at() to price_cache (returns ticker WS wall-clock timestamp)
    - Add quote_wall_ts to MarketFeatures (populated from price_cache)
    - Add quote_ts_source + book_age_ms columns to candidate_decision_vectors
    - Migration: 20260410100000_cdv_quote_staleness.sql
    - Pipeline populates both fields from price_cache at CDV persist time
    - Enables future regression: net_edge ~ book_age_ms, fill_rate ~ book_age_ms
    
    Made-with: Cursor
  384. 72e055e
    P1: wire edge_latency_bps from signal freshness + realized vol
    Show commit body
    Replace hardcoded exit_drag_latency_bps = 0.0 with diffusive decay model:
      latency_bps = vol_per_sec × sqrt(signal_age_secs) × 0.5
    
    Uses signal_freshness_secs (from price_cache last_trade_age) and
    realized_vol_bps (from ignition store) when available.
    Fallback uses spread as vol proxy when vol is unknown.
    
    Adds signal_freshness_secs and realized_vol_bps to ExitFeasibilityInputs.
    Route expectancy now passes MarketFeatures freshness/vol into exit model.
    Legacy readiness report paths pass None (no regression).
    
    Made-with: Cursor
  385. 0ae2fd6
    P0: unblock V2 execution — sizing floor, headroom guard, edge-based liquidity override
    Show commit body
    Capital allocator fix:
    - Lower BASE_SIZE_MIN_QUOTE 20→10 (match exchange floor)
    - Lower POSITION_SIZE_MIN_QUOTE default 20→10 (risk policy)
    - Runner: skip order when headroom < $10 instead of downsizing to non-executable notional
    
    Conflict lane fix:
    - Add high_edge_liquidity_override: when net_edge_bps >= threshold (default 80),
      allow illiquid candidates through with size penalty instead of hard-blocking
    - Env-configurable: CONFLICT_LANE_HIGH_EDGE_OVERRIDE_BPS, _SIZE_MULT
    - Test coverage for override behavior
    
    Fixes: MON/USD pullback blocked by NOTIONAL_TOO_SMALL ($4.27 < $10),
           MON/USD breakout blocked by position size 7.8 outside policy [20, 1000],
           USELESS/USD breakout blocked by conflict_lane_liquidity_tier despite 127 bps net edge.
    Made-with: Cursor
  386. 334d621
    fix(route): use route-type family for move-thesis dispatch
    Show commit body
    Replace fixed MomentumRide family dispatch in route expectancy with
    route-type mapped family (pullback/breakout/fade/market_making).
    Keeps existing path floor and thresholds unchanged.
    
    Made-with: Cursor
  387. 106ad66
    obs(route): add ROUTE_MOVE_BASIS_TRACE breakdown for H4 measurement
    Show commit body
    - expose move-thesis dispatch breakdown: family move, path floor, winner
    - log per-candidate ROUTE_MOVE_BASIS_TRACE with downstream v1_linear_net_bps and blocker
    - no threshold/economic logic changes
    
    Made-with: Cursor
  388. 61fbbb1
    docs(audit): H4 move-thesis dispatch code audit + calib doc cross-link
    Show commit body
    - Confirm single production path, max(momentum, 0.3*path) formula, callers
    - Classify as refactor gap; per-RouteType skew; minimal fix sketch + metrics
    - Correct pump_fade skew note; link from family research to H4 audit
    
    Made-with: Cursor
  389. 4ca26fd
    docs(audit): V2 edge calibration per family research report
    Show commit body
    Code-backed flow map, H1-H8 assessment, data sources, truth-table spec;
    flags move-thesis Momentum hardcode and V1 vs adaptive dual economy.
    
    Made-with: Cursor
  390. a01951b
    fix(observability): site edgeboard uses decision candidates when research snapshot is stale
    Show commit body
    Why-No-Trade reads append-only decision telemetry; Live Route Board was tied to
    RESEARCH edgeboard_snapshots MAX(snapshot_ts), which can sit unchanged for a long
    time. After EDGEBOARD_EXPORT_DECISION_FALLBACK_MAX_SNAPSHOT_AGE_SECS (default 600s),
    prefer candidate_decision_vectors when non-empty so the board moves with admission
    traffic. Empty decision set keeps the research rows.
    
    Made-with: Cursor
  391. d2ec966
    obs(route): split V1 vs adaptive edge in ROUTE_EDGE_TABLE and candidate
    Show commit body
    - RouteCandidate: v1_net_edge_after_soft_l3_bps, adaptive_override_applied,
      adaptive_ranking_edge_bps, adaptive_admit_reason; document before_l3 as V1 linear
    - route_expectancy: populate V1 post-soft-L3 snapshot; adaptive flags default off
    - route_selector_v2: set adaptive fields on admission override; extend ADAPTIVE_ADMISSION_OVERRIDE log
    - ROUTE_EDGE_TABLE: v1_cost_stack vs linear raw, labeled [V1_linear]/[V1_post_soft_l3]/[Ranking];
      no economic logic change
    
    Made-with: Cursor
  392. 757876a
    feat(route): ROUTE_EDGE_TABLE decomp logs (gross vs tradable vs invalid)
    Show commit body
    - Log three top-10 rankings per adaptive eval when ROUTE_EDGE_DECOMP_LOG=1:
      by_gross_move, tradable_top, invalid_near_tradable_top
    - Cost column reconciles capturable to pre-L3 net; log net pre/post L3 and conf
    - Stage2 blocker probe exports ROUTE_EDGE_DECOMP_LOG=1 by default
    
    Made-with: Cursor
  393. 8a12512
    feat(routes): NEAR_TRADABLE_TOP_CANDIDATE log + probe focus default
    Show commit body
    - Log single best invalid candidate (time_adjusted_score, tie-break edge) in
      adaptive and legacy route analysis; first_hard_blocker = dominant_reason_code.
    - stage2 blocker probe: default BLOCKER_PROBE_FOCUS=route_near_tradable (grep
      NEAR_TRADABLE_TOP_CANDIDATE); BLOCKER_PROBE_FOCUS=full restores wide regex.
    
    Made-with: Cursor
  394. bd065cf
    chore(scripts): blocker-probe prefer LIVE_EVALUATION_AUDIT tradable_count=0 over coarse NO_ORDER
    Show commit body
    Made-with: Cursor
  395. dd4e922
    chore(scripts): blocker-probe default BOOTSTRAP_MIN_CONFIDENCE=0.19 (canary-only)
    Show commit body
    35m canary script unchanged at 0.28; probe-only default to pass marginal live conf
    after net edge is already above min_net.
    
    Made-with: Cursor
  396. 8137c08
    chore(scripts): widen blocker-probe regex for MSP, safety, submit failures
    Show commit body
    Made-with: Cursor
  397. 5a817e6
    feat(execution): blocker probe script + honest funnel + capital reject log
    Show commit body
    - Add scripts/stage2_tiny_cap_blocker_probe.sh: tiny-cap env, tail+grep first
      hard-blocker line, TERM run-execution-live, exit 10 on hit (timeout ceiling).
    - Log CAPITAL_ALLOCATION_REJECTED in flow_execute_single_candidate with
      reject_reason and sizing inputs for first-blocker truth.
    - STANDARD_FUNNEL_COUNTERS: stop hardcoding capital_ok=0; emit n/a for async
      capital/precheck layers until per-eval instrumentation exists.
    
    Made-with: Cursor
  398. 2c60b6a
    fix(scripts): stage2 35m canary forces interval/caps; BOOTSTRAP_EDGE_MODE on
    Show commit body
    Unconditional tiny-cap limits and 300s eval interval so server .env cannot
    widen the proof profile. BOOTSTRAP_MIN_CONFIDENCE still overridable.
    
    Made-with: Cursor
  399. 574bd64
    chore(scripts): Stage 2 tiny-cap 35m canary with strict caps + bootstrap confidence override
    Show commit body
    Adds stage2_tiny_cap_canary_35m.sh: LIVE_VALIDATION_RUNTIME_MINUTES=35,
    tight live_validation + bootstrap position/notional caps, default
    BOOTSTRAP_MIN_CONFIDENCE=0.28 for quick proof of execution-flow unblock.
    
    Made-with: Cursor
  400. 75c88d6
    fix(observability): V2_PIPELINE_COMPLETE reflects post-bootstrap outcomes
    Show commit body
    Log pre-bootstrap execute count as V2_PIPELINE_PRE_BOOTSTRAP; emit
    V2_PIPELINE_COMPLETE after apply_bootstrap_live_edge_to_v2_outcomes with
    final execute/skip counts so live_runner PIPELINE_LATENCY_PROFILE matches
    pipeline completion logs.
    
    Made-with: Cursor
  401. 286860d
    fix(execution): generation gate uses ingest data_run_id for state_generation_id
    Show commit body
    Under dual-DB live execution, refresh_run_symbol_state writes run_symbol_state
    for the epoch data_run_id while the gate read used execution run_id, yielding
    visible_generation_id=None and clearing exec_allowed every cycle.
    
    Read MAX(generation_id) for the same ingest key as refresh. Document contract
    on state_generation_id. Exec-only path unchanged (refresh already uses run_id).
    
    Made-with: Cursor
  402. a14c4ac
    docs: canary closure (2026-04-09) — SSOT §7 + epoch + index
    Show commit body
    Made-with: Cursor
  403. d0de648
    docs: epoch policy — refactor merged, canary (~70m) as runtime proof layer
    Show commit body
    - DOCUMENTATION_EPOCH: LIVE = current binary; SPEC docs = design archive; legacy pre-2026-04-09
    - DOC_INDEX + ENGINE_SSOT: align with completed refactor + pending canary closure for SSOT matrix
    
    Made-with: Cursor
  404. 2b163c5
    docs: align synced site docs with route-centric canon + ingest state SoT
    Show commit body
    - DOC_INDEX, LIVE_RUNBOOK: correct evaluation loop; clarify INGEST_DECISION_SYNC_VISIBLE
    - DB_ARCHITECTURE_STALE_EDGE_SAFE: remove fictive sync; fix State row + generation contract
    - CHANGELOG_ENGINE, LOGGING, VALIDATION_*, EXIT_PATHS, systemd README: canon pointers
    - OBSERVABILITY_SNAPSHOT_CONTRACT: v1.2 semantics + public safety line; tier2 bundle note
    - ARCHITECTURE: flow_poller wording
    - Bump observability CONTRACT_VERSION to 1.2 to match contract doc
    
    Made-with: Cursor
  405. 6c2aa2f
    docs: refresh ENGINE_SSOT for route-centric canon + fix pool topology
    Show commit body
    - Add 2026-04-09 section aligning SSOT with public SPEC/dashboard semantics
    - Reframe runtime decision sources: V2 route chain primary, MSP as gates/context
    - Replace misleading sync_run_symbol_state_to_decision topology; ingest SoT for refreshed run_symbol_state
    - Mark matrix DB snapshots as historical evidence, not live truth
    - Align ARCHITECTURE_ENGINE_CURRENT §2–§3 diagrams and prose with SSOT
    
    Made-with: Cursor
  406. d088f0c
    feat: Opus v4.6 mega-refactor — multistrategy architecture with zero legacy
    Show commit body
    Complete implementation of WP-01 through WP-17:
    
    - RouteStateStore with per-family timing profiles, decay, and persistence
    - 7 per-family move thesis functions replacing shared floor
    - Route-aware cost model (maker/taker differentiated)
    - Per-strategy CHAOS routing replacing blanket noise rejection
    - Family-aware edge heap scoring (persistence + decay)
    - Per-family route lifetime (10s–7200s) replacing global 240s
    - Taker fallback edge computation on maker window expiry
    - VWAP window, TOD multiplier, and VwapReversion move thesis
    - Timing observability (signal-age histogram, expiry/taker counters)
    - Fill feedback infrastructure (ready for ws_handler wiring)
    - Position state machine with route-aware management advice
    - Strategy onboarding with material differentiation enforcement
    - All legacy dual-paths removed, all feature flags collapsed
    - 369 tests pass, 0 failures, zero LEGACY: markers in src/
    
    Made-with: Cursor
  407. d0e88e9
    fix(execution): count all Loop A coalesced ticks in received metric
    Show commit body
    loop_a_tick_received_add ran only after the first blocking recv; try_recv
    drains and timeout-based recv coalescing dropped messages from the
    channel without incrementing. Align with loop_a_tick_dropped (per
    try_send failure) so received + dropped approximates total tick volume.
    
    Made-with: Cursor
  408. 51f48ea
    feat(edgeboard): pooled route semantic gross scale (v5)
    Show commit body
    When route_hist_pooled_to_bucket, apply bounded per-route multipliers
    from ob/tf/mp/rv1 so named routes do not show identical gross/net.
    Env EDGEBOARD_POOLED_SEMANTIC_SCALE_ENABLE (off: legacy identical nets).
    Explainability: statistical_gross_bps, pooled_semantic_gross_scale.
    MODEL_VERSION v5_pooled_semantic_scale; CONFIG_ADDITIONS + edge doc.
    
    Made-with: Cursor
  409. 7c9b504
    docs(edgeboard): edge calculation chain and identical route outcomes (pooling)
    Show commit body
    Made-with: Cursor
  410. b831333
    feat(execution): wire Phase 5 flow observability counters and edgeboard cache metrics
    Show commit body
    - Record eval/recycle/Loop A heap merges and wakes; stale-generation skips;
      Loop A tick received/dropped; snapshot on FLOW_POP_BATCH trace.
    - recycle_symbol_into_heap(..., FlowHeapMergeSource) for merge attribution.
    - Fix duplicate edgeboard_cache_db_fallback count when cache misses.
    - Unit test for counter monotonicity; docs: investigation §8, Phase5 §8,
      pseudocode, changelog, rapport bundle.
    
    Made-with: Cursor
  411. de12066
    docs(systemd): Loop A drop-in example + live journal notes
    Show commit body
    - Add drop-in-examples/krakenbot-execution-continuous-edge-refresh.conf
    - systemd/README + SERVER_RUNTIME: install path, grep proof, no-order semantics
    - DOC_INDEX + CHANGELOG unreleased
    
    Made-with: Cursor
  412. b56b991
    fix: time-stop imminent window for short deadlines; align remediation test
    Show commit body
    - Imminent uses last min(15s, half of deadline) when deadline<=15s; avoids
      deadline-15<0 marking entire window imminent from t=0 (break-even blocked).
    - double_transition test: 2s window + 1100ms sleep for stable ordering.
    - remediation test expects only non-reduce cancels per position_reconcile policy.
    
    Made-with: Cursor
  413. 0a83943
    test(price_cache): mid_tick_subscriber old/new mid under tokio runtime
    Show commit body
    Made-with: Cursor
  414. c85d918
    docs(execution): close Phase 5 target contract after phases 1–4
    Show commit body
    - PHASE5: contract status, updated current-state table, §8 implementation matrix,
      module table vs repo, design closure §4–5 past tense, §12 delivered checklist
    - BLUEPRINT: Phase 5 section = doc closure + optional follow-ups
    - INVARIANT_MATRIX: generation row reflects implemented generation_state
    - PSEUDOCODE: Loop A repo implementation note
    - CHANGELOG: unreleased docs bullet
    
    Made-with: Cursor
  415. 38adebb
    feat(execution): Loop A material mid refresh + flow slot notify (Phase 3–4)
    Show commit body
    - Add market_edge_refresher: bounded channel, coalescing, LOOP_A_MAX_PIPELINE_PER_SEC
    - price_cache: single mid-tick subscriber (old/new mid after ticker update)
    - flow_poller: optional slot Notify in biased select; notify after each drain
    - Config/env: CONTINUOUS_EDGE_REFRESH_ENABLE, EDGE_REFRESH_MATERIAL_BPS,
      LOOP_A_MAX_PIPELINE_PER_SEC, FLOW_SLOT_NOTIFY_ENABLE
    - Wire live + exec-only bootstrap; FlowWakeReason::LoopARankingRefresh reserved
    - Docs: BLUEPRINT + CONFIG_ADDITIONS; CHANGELOG unreleased bullet
    
    Made-with: Cursor
  416. 0070f23
    feat(edgeboard): Phase 2 RAM cache + optional read path (continuous execution)
    Show commit body
    - EdgeSignalCache + live_signal types; refresh fills cache when EDGEBOARD_RAM_CACHE_ENABLE.
    - load_symbol_boosts_resolved: fast path only if DB visible MAX(snapshot_ts) matches cache.
    - Pipeline and all call sites pass edgeboard_cache_read_enable; run_edgeboard_refresh gains fill_ram_cache arg.
    - Blueprint Phase 2 implementation pointer; CONFIG_ADDITIONS read paths updated.
    
    Made-with: Cursor
  417. 8cbfc52
    feat(ops): optional EXECUTION_MAX_RUN_DURATION_HOURS for planned clean restarts
    Show commit body
    - Wall-clock cap from main eval loop start; log EXECUTION_MAX_RUN_DURATION_REACHED; same shutdown path as normal exit.
    - Applies to run-execution-live and run-execution-only; independent of LIVE_VALIDATION_RUNTIME_MINUTES.
    - Document in RESTART_DOCTRINE, CONFIG_ADDITIONS, systemd unit comments.
    
    Made-with: Cursor
  418. 00aabfb
    feat(execution): Phase 1b observability — in-flight gauge and duplicate-pop counter
    Show commit body
    - pop_batch returns suppressed_slots when in_flight exclude caps the batch.
    - flow_parallel_metrics: AtomicUsize in-flight, AtomicU64 duplicate_pop_prevented total.
    - Trace FLOW_POP_BATCH at trace level for log pipelines; blueprint Phase 1b implementation pointer.
    
    Made-with: Cursor
  419. 5b797cf
    feat(execution): Phase 1b parallel flow drain behind FLOW_PARALLEL_ENABLE
    Show commit body
    - Add flow_parallel_enable and flow_parallel_limit (env, clamped 1..=10).
    - Shared in_flight symbol set excludes concurrent pops; semaphore caps workers.
    - Refactor poller body into drain_single_heap_entry; sequential path unchanged when flag off.
    
    Made-with: Cursor
  420. b7cfb6f
    feat(execution): Phase 1a flow heap — generation snapshot, tagged merge, pop_batch, wake-on-merge
    Show commit body
    - Add GenerationAdmissionState + publish from eval (live + exec-only paths)
    - HeapEntry.admission_generation; merge_pipeline_outcomes_with_generation + FlowHeapMergeResult
    - pop_batch + stale-generation skip in poller with recycle refresh
    - FLOW_MAKER_LIMIT, FLOW_POP_BATCH_SIZE, FLOW_WAKE_ON_HEAP_MERGE_ENABLE in Config
    - recycle uses flow_maker_limit and tagged merge
    - Fix physical-separation gate to compare visible ingest generation (was ineffective)
    - Add implementation-ready design docs under docs/
    
    Made-with: Cursor
  421. 2b565f5
    chore(gitignore): ignore diagnostics venv and plots for clean deploy tree
    Show commit body
    Made-with: Cursor
  422. 0245562
    docs: pipeline V2 call sites audit and capital/flow env matrix
    Show commit body
    Made-with: Cursor
  423. 8b9b153
    perf(latency): event-driven flow poller, parallel exec reads, batched pipeline writes
    Show commit body
    - Flow poller: skip idle poll_ms sleep after work or startup; wake+timer only when heap idle
    - Recycle worker: deadline-based coalesce (12ms max) replaces fixed 30ms sleep
    - Entry-halt map: tokio RwLock + async helpers (no parking_lot in async hot path)
    - flow_execute_single_candidate: join MSP+symbol_safety; join ignition+tape+L2+heap age; join refresh+pinned queries
    - strategy_pipeline: parallel flush_trading_funnel_batch with insert_v2_shadow; join persist CDV with shadow on capital skip
    - L2 checksum telemetry: parking_lot Mutex for shorter hot-path locks
    - WriterSender::try_send_or_spawn; kraken ticker/trade WS avoids blocking on full channel
    
    Made-with: Cursor
  424. 4a2904f
    fix(edgeboard): rank every route without hard history prefilter
    Show commit body
    - match_returns_for_route: prefer route-aligned historical returns when
      n>=MIN_SAMPLE; otherwise pool the full tier bucket (no route-only wall)
    - explainability: route_hist_bucket_n, route_hist_route_aligned_n,
      route_hist_pooled_to_bucket
    - Document load_latest_mms_merged as universe-only (no trade/route gate)
    - Unit tests for pooled path and empty history
    
    Made-with: Cursor
  425. e19e6e6
    fix(edgeboard): do not prefilter candidates on route_passes
    Show commit body
    - Score and rank all symbols × routes × horizons; route gate is tie-break + explainability only
    - Keeps match_tiers history route filter for per-route edge semantics
    
    Made-with: Cursor
  426. d3ae6d0
    perf(latency): overlap analyze+edgeboard; recycle reuses run snapshot
    Show commit body
    - V2 pipeline: tokio::join load_symbol_boosts with analyze_run_from_state when no precomputed report
    - Shared TokioRwLock cache updated each eval; flow recycle/poller passes Arc to pipeline (skips duplicate ingest analyze when cache hit)
    - LivePreloopRuntime carries pipeline_current_run_cache for main loop scope
    
    Made-with: Cursor
  427. bb78c80
    perf(pipeline): batch trading_funnel_events writes (Fase E funnel)
    Show commit body
    - insert_events_batch: single INSERT for ordered rows
    - V2 loop buffers signal/admission/conflict funnel rows per symbol
    - Preserves event order and fields; same semantics as sequential inserts
    
    Made-with: Cursor
  428. ba53d91
    feat(obs): log edgeboard load duration, snapshot age, boost coverage
    Show commit body
    - load_symbol_boosts returns latest visible snapshot_ts (RESEARCH)
    - EDGEBOARD_LOAD_SYMBOL_BOOSTS_MS + tradable boost counts for isolation
    
    Made-with: Cursor
  429. 71fba1f
    perf: decision pipeline latency (dedup analyze, parallel I/O, rayon route)
    Show commit body
    - Thread Option<Arc<CurrentRunReport>> readiness → route → pipeline; skip
      duplicate analyze_run_from_state on hot path; pass refresh generation_id into
      analyze for coherent snapshot (CurrentRunReport.generation_id).
    - Parallelize analyze_run_from_state with tokio::join (not try_join); log
      ANALYZE_RUN_DURATION_MS; ROUTE_EVAL_DURATION_MS (v1+v2); PIPELINE_LOOP_DURATION_MS.
    - Rayon par_iter for route pair evaluation + deterministic sort by symbol
      before persist/logs (v1 route_selector + adaptive v2).
    - Log PIPELINE_LATENCY_BREAKDOWN with cycle_generation_id, report_generation_id,
      route_build_duration_ms, pipeline_duration_ms, pipeline_logic_ms (main + EO loops).
    - run-execution-once: capture RefreshOutcome, pass snapshot through route/pipeline.
    
    Metrics: compare PIPELINE_LATENCY_PROFILE.pipeline_logic_ms and
    EVALUATION_CYCLE_DURATION_MS before/after deploy; Fase E (batch funnel/CDV)
    deferred until profiling shows dominance.
    
    Made-with: Cursor
  430. 3228fdd
    perf(herstel): single-query max_ids, dedupe state_updated_at, eval drift log
    Show commit body
    - Consolidate MAX(id) across four raw tables in one round-trip
    - ReadinessReport carries state_updated_at; live_runner drops duplicate query
    - EVAL_CYCLE_WALL_GAP_MS for interval jitter visibility
    - systemd LimitNOFILE; runbook for PG/Redis/OS; EXPLAIN template for watermarks
    
    Made-with: Cursor
  431. 43c9df2
    docs+cli: freshness audit blueprint, ingest SoT for run_symbol_state checks
    Show commit body
    - Add check-ingest-state (ingest pool) and pool_target= lines; keep check-decision-state for decision copy diagnostics
    - Fix STATE_SYNC_REQUIRED reason to ingest_run_symbol_state_missing; rewrite STATE_SYNC_CONTRACT and ENGINE_SSOT pool truth
    - Add FRESHNESS_STALENESS_AUDIT_REPORT and FRESHNESS_MEASUREMENT_BUNDLE
    
    Made-with: Cursor
  432. 4ad4f6f
    Strengthen Cursor rule: no concessions without absolute necessity
    Show commit body
    Made-with: Cursor
  433. 82d48cc
    Add Cursor rule: default to technically correct solutions
    Show commit body
    Made-with: Cursor
  434. 46591a4
    Add per-horizon edgeboard training labels and restore labeled unions.
    Show commit body
    Extend edgeboard_training_examples with nullable ret_1m/3m/10m/15m columns; populate from L3 bootstrap (multi-horizon mids), live bias (3m/10m where present), and shadow (5m only until multi-markout exists). load_history unions training rows per label column with IS NOT NULL guards. Bump MODEL_VERSION to horizon_training_v4.
    
    Made-with: Cursor
  435. dda7826
    Harden edgeboard multi-horizon ranking and freshness gating.
    Show commit body
    Remove 5m-only training asymmetry, enforce route/freshness hard gates, and tighten default microstructure capture cadence to reduce stale ranking artifacts.
    
    Made-with: Cursor
  436. 8039bd1
    Remediate reserved-funds conflicts on insufficient-funds protection errors.
    Show commit body
    When emergency close gets EOrder:Insufficient funds, reconcile exchange open orders, cancel conflicting non-reduce orders via deterministic remediation, and retry in the next cycle with explicit exchange-order diagnostics.
    
    Made-with: Cursor
  437. bcb5cc8
    Harden protection retry behavior for uncertain exchange state.
    Show commit body
    Avoid reporting a submitted protection state before verification, and escalate to forced market-close when BalanceInconclusive persists beyond the protection SLA so open exposure cannot remain unprotected.
    
    Made-with: Cursor
  438. 85fae15
    Regenerate changelog ledger after coverage-script fix
    Show commit body
    Refresh the ledger from current history so docs-and-changelog sees the latest non-tip commit set while HEAD and one-parent lag handling stays deterministic.
    
    Made-with: Cursor
  439. 5859559
    Fix changelog coverage check fixed-point lag
    Show commit body
    Allow one-parent lag only when HEAD updates the generated ledger file and adjust expected row count accordingly, so docs-and-changelog CI no longer loops on self-generated ledger commits.
    
    Made-with: Cursor
  440. 94482f4
    Regenerate changelog ledger after ledger-fix commit
    Show commit body
    Refresh coverage ledger from current HEAD so previous ledger-fix commit is included and docs-and-changelog coverage check can pass on this run.
    
    Made-with: Cursor
  441. 6acfad0
    Refresh changelog ledger after latest docs commits
    Show commit body
    Regenerate commit ledger again so CI coverage includes the recent edgeboard report update commit referenced by docs-and-changelog checks.
    
    Made-with: Cursor
  442. c84befb
    Update changelog commit ledger for CI coverage check
    Show commit body
    Regenerate the commit ledger so docs-and-changelog validation includes the latest edgeboard and exposure policy commits and no longer fails coverage enforcement.
    
    Made-with: Cursor
  443. 6200355
    Update edgeboard report with live run findings
    Show commit body
    Replace the template report with latest server diagnostics output and add plots folder guidance with generated artifact names for reproducible reruns.
    
    Made-with: Cursor
  444. 0e808f8
    Add edgeboard reality-check diagnostics toolkit
    Show commit body
    Provide a schema-first diagnostics script, daily health SQL monitor, and report template to assess confidence collapse, variance penalties, prediction-realization fit, cost realism, edge decay, and sample-size reliability with explicit missing-table fallbacks.
    
    Made-with: Cursor
  445. 7df0fbd
    Soften dominant edgeboard penalties and dedupe snapshot keys
    Show commit body
    Relax confidence pressure for high variance, low sample, and partial feature coverage while preserving deterministic reason semantics, and dedupe (symbol, route_name) before insert so snapshot runs cannot fail on duplicate partition keys.
    
    Made-with: Cursor
  446. d586f90
    Relax dominant edgeboard confidence penalties
    Show commit body
    Soften the three most frequent penalty paths by reducing variance harshness, lowering low-sample severity, and easing partial-coverage confidence/cost penalties while keeping deterministic reason codes and env-tunable guardrails.
    
    Made-with: Cursor
  447. 75c28a3
    Apply 24h drawdown-triggered exposure policy
    Show commit body
    Default exposure now tracks MAX_DEPLOYED_EQUITY_FRACTION (98% by default) and only caps to 50% when rolling 24h realized PnL drops to -3% of equity or worse, with automatic restore as soon as drawdown recovers above that threshold.
    
    Made-with: Cursor
  448. a320b68
    Make multi-horizon edgeboard route keys unique
    Show commit body
    Encode horizon into edgeboard route_name so per-horizon candidate rows no longer collide on the existing unique key and all 1m/3m/5m/10m/15m route variants are persisted in the same snapshot.
    
    Made-with: Cursor
  449. e180a8c
    Expose per-horizon edge curve and exit bias in Tier2 edgeboard
    Show commit body
    Add candidate-level 1m/3m/5m/10m/15m expected net-edge fields, preferred horizon, and quick_tp/tsl bias tagging so operators and executor logic can compare horizon-specific expectations instead of treating 5m as a proxy.
    
    Made-with: Cursor
  450. 7ed8b27
    Only hard-block MSP drift when halt state is active
    Show commit body
    Keep MSP fail-closed for explicit halt states, but avoid rejecting entry-eligible symbols on drift-only signals without a halt. This prevents repeated msp_admission_block loops on symbols with non-blocking drift metadata.
    
    Made-with: Cursor
  451. f3c6c45
    Unblock trading from stale exposure and MSP dust drift
    Show commit body
    Exclude stale open-entry rows from fallback open-notional accounting and only treat MSP drift as material when quantity meets instrument minimum or estimated value exceeds a minimum USD threshold. This prevents ghost exposure from blocking new entries after fills flatten risk.
    
    Made-with: Cursor
  452. 40ccddc
    Ignore stale open-entry rows in fallback exposure notional
    Show commit body
    When positions are empty, total_open_notional now excludes stale buy-side open-order rows unless they are recent or confirmed open in own_orders snapshot. This prevents ghost acked_open rows from permanently tripping global_exposure after sells/fills have already flattened risk.
    
    Made-with: Cursor
  453. e6d5759
    Add multi-horizon edgeboard edges (1m/3m/5m/10m/15m)
    Show commit body
    Generate edgeboard candidates/snapshots across five forward-return horizons using horizon-specific labels and persist matching outcomes per candidate label, so Tier2 can expose edge by horizon for TP-vs-TSL decisions.
    
    Made-with: Cursor
  454. 32e20d7
    Expose Tier2 edgeboard candidates with per-symbol routes
    Show commit body
    Extend Tier2 edgeboard payload with candidate-level route arrays and sort/filter metadata so the dashboard can render a dedicated leaderboard with full gross/cost/net route breakdown for the top snapshot set.
    
    Made-with: Cursor
  455. d169e4d
    Force admission for high-confidence non-negative edge
    Show commit body
    Add a configurable breakeven-confidence override in strategy admission so non-negative edge candidates with >=90% confidence are not blocked by conflict-lane soft constraints, while keeping negative-edge hard blocks intact.
    
    Made-with: Cursor
  456. bf63002
    Unblock positive-edge entries at execution gates
    Show commit body
    Align spread realism with post-cost surplus, add bounded stale price-cache fallback, and auto-uplift final order qty to satisfy exchange minimum notional after wire quantization to avoid prevalidation drops.
    
    Made-with: Cursor
  457. 0802443
    Reduce false execution skips for positive-edge candidates
    Show commit body
    Allow bounded spread-cap relaxation for positive post-cost surplus, permit stale price-cache fallback within a capped window, and remove pre-correction notional hard-fail so market sizing can auto-correct to exchange minimums before submit guards.
    
    Made-with: Cursor
  458. 7ac5a36
    Add server disk guardrails with cron and startup headroom checks
    Show commit body
    Install recurring disk headroom enforcement and retention wiring, plus fail-fast ExecStartPre checks in ingest/execution units to prevent low-space starts from cascading into WAL recovery failures.
    
    Made-with: Cursor
  459. 89a8f1a
    Enable edgeboard cold-start ranking fallback
    Show commit body
    Allow candidate ranking to proceed when route-specific history is sparse by falling back to global history and retaining route-gate metadata in explainability.
    
    Made-with: Cursor
  460. 717874c
    Harden edgeboard candidate generation with L3 and join fallbacks
    Show commit body
    Add history join fallback on symbol/time, use L3 mid when forward-mid ticker is missing, and prevent live/bootstrap MMS dropouts with L3-backed neutral feature fallback so training continues under sparse trade conditions.
    
    Made-with: Cursor
  461. e75bfd3
    Prioritize live edgeboard visibility and fallback candidate feed
    Show commit body
    Add latest snapshot top-N export (default 500) with gross/cost/net edge fields, add decision fallback list when research snapshot is empty, and harden edgeboard training/history fallbacks for missing observation links, forward-mid, and MMS.
    
    Made-with: Cursor
  462. 35afcf3
    Fix forward-returns insert column/value mismatch
    Show commit body
    Remove extra VALUES placeholder so directional_forward_observations inserts
    match table schema; restores RESEARCH writes needed by edgeboard scoring.
    
    Made-with: Cursor
  463. 81458a9
    Add SQL to grant pg_checkpoint for retention CHECKPOINT batches
    Show commit body
    Made-with: Cursor
  464. b649cd2
    Retention: chunk purges with CHECKPOINT between batches
    Show commit body
    Reduces WAL/recovery spikes when deleting huge L2/L3 per run_id on
    nearly-full disks. RETENTION_PURGE_RUNS_PER_CHECKPOINT (default 3).
    
    Made-with: Cursor
  465. 29f94f7
    Add disk_safe_cleanup.sh for non-DB space recovery
    Show commit body
    Journal vacuum, apt clean, PM2 flush, cargo/npm caches, old log.gz;
    optional cold_parquet test dirs via env flags.
    
    Made-with: Cursor
  466. 40319f1
    Add purge script for observation_runs by started_at window
    Show commit body
    Single ingest transaction; decision mirrors run_ids via COPY with
    conditional deletes when tables exist. Uses psql_pool only; dry-run
    default; PURGE_APPLY=1 to execute.
    
    Made-with: Cursor
  467. abdda8b
    config: default raw retention to 7 days (agreed ops cap)
    Show commit body
    - RETAIN_RAW_DAYS defaults to 7 when env unset (run-retention-cleanup); set RETAIN_RAW_DAYS=0 to disable.
    - .env.example + DB_RETENTION_AND_RAW_RESET document 7-day policy and cron helpers.
    - Add disk_headroom_check.sh and cron_ingest_retention_cleanup.sh for scheduled cleanup.
    
    Made-with: Cursor
  468. 3ea49a8
    edgeboard: phase INFO logs for training refresh observability
    Show commit body
    Prior code logged only once after all training awaits completed (grep showed
    a single tracing call in training_ingest). Add edgeboard_refresh_started,
    per-phase edgeboard_training_refresh_phase, edgeboard_snapshot_started, and
    skip line when research pool is absent.
    
    Made-with: Cursor
  469. 19ea951
    edgeboard: L3 bootstrap training rows and refresh logging
    Show commit body
    - Ingest latest L3 anchors from ingest pool, label 5m mid move via ticker,
      insert edgeboard_training_examples (source=l3_bootstrap) with bounded batches.
    - Env caps: EDGEBOARD_L3_BOOTSTRAP_MAX_INSERTS, EDGEBOARD_L3_BOOTSTRAP_HORIZON_HOURS.
    - Log l3_bootstrap_* counts on edgeboard_refresh_complete; stable source_row_key nanos.
    
    Made-with: Cursor
  470. c874fab
    fix(edgeboard): ingest MMS fallbacks, bias backfill default, L3 resync decay
    Show commit body
    Edgeboard training now resolves microstructure from ingest l2_snap_metrics when
    RESEARCH market_microstructure_snapshots are missing or all-null. Snapshot runs
    try EDGEBOARD_SCOPE_MODE first, then any MMS scope, then live ingest L2 plus
    short-window trade imbalance. Coverage class accepts direct/ticker L2 book +
    microprice without RV when tape is thin.
    
    EDGE_BIAS_BACKFILL_ENABLE defaults on so realized_move_window_5m populates for
    live Edgeboard training unless explicitly disabled.
    
    L3 resync streak resets when queue metrics show activity again (live + ingest
    loops), avoiding monotonic climb to hard_block for intermittent tape; active
    l3_resync_limit hard blocks are left unchanged.
    
    Made-with: Cursor
  471. 5d147d8
    fix(execution): ignore late sell fills on flat spot rows
    Show commit body
    Treat backfilled close fills as no-op position updates when the DB row is already flat or negative, so stale exit replays cannot reopen impossible short exposure in the long-only spot engine.
    
    Made-with: Cursor
  472. 875ead0
    fix(execution): bind oto child fills to bot orders
    Show commit body
    Persist Kraken OTO trailing-stop children before reconciliation so exchange-only lifecycle events no longer fall into external backfill rows, and surface clearer regime/reject observability for live triage.
    
    Made-with: Cursor
  473. 95e5e18
    fix(exchange): keep low-price maker quotes above zero
    Show commit body
    Preserve positive increment-aligned limit prices when precision rounding would collapse them to zero so low-priced live entries can pass exchange prevalidation.
    
    Made-with: Cursor
  474. dc024bc
    fix(execution): bind flow repipeline to ingest data run
    Show commit body
    Keep flow-poller and recycle-worker re-pipelining on the active ingest data run so live split-run execution does not fall back to empty state maps while recycling candidates.
    
    Made-with: Cursor
  475. f29d63c
    fix(pipeline): use ingest data_run_id in zero-tradable fallback
    Show commit body
    Read V2 fallback state from the active ingest data run instead of the execution run id so split ingest/live setups do not drop to an empty state map when tradable routes collapse to zero.
    
    Made-with: Cursor
  476. d76a469
    feat(observability): export edgeboard dashboard signals
    Show commit body
    Expose delayed Edgeboard health and top-signal summaries through the existing observability snapshots so operators can verify RESEARCH fill quality and steering direction from the dashboard.
    
    Made-with: Cursor
  477. 4bbe4fc
    refactor(debug): remove dead paths and tighten diagnostics
    Show commit body
    Reduce debug ambiguity in readiness, reconcile, exit, and CDV/edge plumbing so live-trading incidents rely less on brittle string checks and unused surface.
    
    Made-with: Cursor
  478. 9ea5941
    refactor(execution): split live runner orchestration
    Show commit body
    Isolate startup, warmup, preloop bootstrap, and readiness accounting helpers so the live execution path is easier to debug without changing runtime sequencing or decision behavior.
    
    Made-with: Cursor
  479. ccd100e
    fix(safety): clear expired L3 hard blocks in resync accounting
    Show commit body
    - When hard_block_until has passed, reset mode to normal and drop hard_block_until
    - Prevents stuck hard_blocked mode after expiry in ingest/live runners
    
    Made-with: Cursor
  480. fa45772
    feat(execution): handle amended, restated, expired in ws_handler
    Show commit body
    - Sync quantity from exchange on amend/restate; log order_events
    - Map expired to canceled lifecycle (Kraken TIF)
    
    Made-with: Cursor
  481. 3f40453
    feat(execution): backfill unmatched admin raw_private_executions
    Show commit body
    - fetch_unmatched_admin_parsed_batch for lifecycle exec_types
    - Resolve orders, apply ack/cancel/reject/amend/status updates, mark matched
    - Orphan rows (no DB order) marked matched to drain backlog safely
    
    Made-with: Cursor
  482. 1c46d12
    feat(execution): emit MSP flat after dust position close
    Show commit body
    - Add dust_msp_sync helper: zero positions + ReconcileResult for UI/runtime
    - Use on exposure startup and periodic reconcile dust paths
    - Emit after phantom position correct-to-flat
    
    Made-with: Cursor
  483. b7da6a4
    fix(state): align MSP protection_required with dust semantics
    Show commit body
    Treat positions at or below exchange qty_min as non-protectable in market_state_projection so projection truth matches runtime dust handling. This removes false critical protection_required states for residual balances that exposure_reconcile already classifies as BelowExchangeQtyMin.
    
    Made-with: Cursor
  484. a786124
    fix(execution): honor exchange filled status on trade events
    Show commit body
    Force execution_orders to terminal filled when Kraken trade reports already carry order_status=filled, which prevents precision drift from leaving fully completed orders stuck in partially_filled. Extend raw execution backfill rows with order_status so historical unmatched trade rows can make the same correction.
    
    Made-with: Cursor
  485. 2dda98e
    fix(execution): normalize terminal filled events after tracker misses
    Show commit body
    Add DB fallback correlation for execution reports when the in-memory tracker no longer knows the order, and normalize status-only filled completion events so orders do not remain partially_filled while exchange truth says filled. Extend raw execution backfill to reconcile historical filled terminal events without fill payload and mark those raw events matched.
    
    Made-with: Cursor
  486. a3592d6
    fix(msp): clear protection-present when no protection is required
    Show commit body
    Do not keep protection_present/protection_order_qty active once reconciled exposure is back to zero. This keeps market_state_projection fail-closed and prevents stale protection rows from looking healthy after the exit has already completed.
    
    Made-with: Cursor
  487. a73d4e8
    fix(execution): fail closed on stale protection and prioritize fill backfill
    Show commit body
    Prioritize unmatched trade/fill private execution events so real exchange fills do not starve behind non-fill events, and start the backfill worker with an explicit log marker. Also make protection checks fail closed: ignore stale MSP protection flags without concrete order evidence and downgrade required protection rows that only carry unverified protection state.
    
    Made-with: Cursor
  488. 646435c
    fix(edgeboard): bootstrap refresh immediately and surface training coverage
    Show commit body
    Start the shared edgeboard refresh worker with an immediate bootstrap run so live and observe modes do not wait a full interval before snapshots exist. Also log training-ingest coverage stats and make the edgeboard-snapshot CLI target explicit about its decision/ingest reads.
    
    Made-with: Cursor
  489. d6c42b1
    feat(export): trading_funnel_events parquet cold + operator bundle script
    Show commit body
    - Add export-parquet-cold --dataset trading_funnel_events (--older-than-days),
      partitioned like CDV with NULL symbol coalesced to __null__ for paths/validation.
    - CLI logs ingest vs decision DB target for export-parquet-cold.
    - Add scripts/export_parquet_cold_bundle.sh (db_target_precheck --both, all Phase-5 datasets).
    - Refresh parquet plan and DOC_INDEX.
    
    Made-with: Cursor
  490. aa6e881
    feat(export): parquet cold export for ticker, L2, and L3 ingest tables
    Show commit body
    - Add export-parquet-cold datasets ticker_samples, l2_snap_metrics, l3_queue_metrics
      mirroring trade_samples (partitioning, ZSTD, manifest, COUNT validation).
    - Factor shared ingest run selection, partition paths, and row-count validation.
    - Update parquet archival plan and DOC_INDEX for Phase 5 ingest-cold status.
    
    Made-with: Cursor
  491. ff49734
    chore: gitignore cold_parquet export dirs; document parquet plan status
    Show commit body
    Phase 5 archival is partial (CLI export exists); ignore generated trees so
    server git status stays clean. Cross-link plan in DOC_INDEX.
    
    Made-with: Cursor
  492. c561e3c
    fix(execution): seed market run id for ingest split before epoch bind
    Show commit body
    When ingest_provides_data, bound_run_id_atomic stayed 0 until after the first
    epoch binding. Universe refresh then used execution_live run_id for ingest-pool
    reads (no L2 rows), and epochs could list the wrong run_id — FEATURE_READY
    saw l2_rows=0.
    
    - Pre-seed atomic with epoch_data_run_id (same as ingest_epochs market run)
    - Universe refresh / L3 block: fallback to latest ingest observation if atomic unset
    - Refresh create_epoch: fall back to atomic before execution run_id
    - Remap data_run_id when bound_epoch.run_id wrongly equals execution run_id
    - Log data_run_id vs bound_epoch_run_id on FEATURE_READY_SIGNAL
    
    Made-with: Cursor
  493. 12b74b2
    feat(edgeboard): training ingest, execution_realism shadows, live sort boost
    Show commit body
    Add RESEARCH edgeboard_training_examples and run_edgeboard_refresh (ingest
    then snapshot). Trusted execution_realism shadow rows and live bias capture
    feed training; dual-pool markout backfill and L3 context from ingest.
    
    Decision: realized_move_window_5m on edge_bias_entry_capture; markout_5m_bps
    on shadow_trades (shadow_evaluator). Flow inserts shadow rows on
    ENTRY_BLOCKED_EXECUTION_REALISM.
    
    V2 pipeline accepts optional research pool for non-negative edgeboard boost on
    tradable sort priority; run-execution-once passes None.
    
    Document in ENGINE_SSOT, RESEARCH_OBSERVABILITY, CHANGELOG_ENGINE.
    
    Made-with: Cursor
  494. de8d6bf
    fix(execution): cap initial exits to spendable balance
    Show commit body
    Normalize initial SL and TP quantities against the live base balance so post-fill protection paths do not reject with insufficient funds when exchange-available size is slightly below fill size.
    
    Made-with: Cursor
  495. 1065724
    fix(execution): recover reduce qty from persisted orders
    Show commit body
    Use decision DB order quantities when ownOrders omits qty fields so reconcile does not halt protection and TP follow-up for bot-managed exits.
    
    Made-with: Cursor
  496. 2cb1ef9
    fix(execution): ignore short execution-only exposure
    Show commit body
    Treat negative execution-cache net positions as incoherent spot state so open exit sells cannot re-enter reconcile as synthetic short exposure and trip protection loops.
    
    Made-with: Cursor
  497. ee40079
    fix(execution): drop dust before protection reconcile
    Show commit body
    Treat sub-minimum balance-backed positions as dust before checking existing exit coverage so legacy reduce-only orders cannot keep symbols like BAL in protected exposure state.
    
    Made-with: Cursor
  498. c10a8d3
    fix(execution): derive dust from instrument min size
    Show commit body
    Use exchange instrument qty_min as the dust criterion in exposure reconciliation so undersized positions like BAL no longer survive via USD-notional heuristics.
    
    Made-with: Cursor
  499. 1e889c3
    fix(execution): persist exchange balance snapshots exactly
    Show commit body
    Delete assets missing from the latest balances snapshot before upserting so stale exchange_balances rows cannot resurrect phantom exposure after positions or orders disappear.
    
    Made-with: Cursor
  500. e5371bb
    fix(execution): let stale cleanup ignore reserved entry balances
    Show commit body
    Reuse reserved open-entry detection in stale bot order cleanup so aged open buys can be canceled even when Kraken balance snapshots still reflect their reserved base amount.
    
    Made-with: Cursor
  501. 9de87fe
    fix(execution): ignore reserved entry balances as exposure
    Show commit body
    Do not treat balance-derived base holdings as startup exposure when coherent ownOrders shows they are fully explained by still-open unfilled buy entries.
    
    Made-with: Cursor
  502. 81578cf
    fix(execution): cancel stale bot orders without positions
    Show commit body
    Cancel bot-owned Kraken open orders that are older than the stale threshold, still present in ownOrders, and no longer back any live position so reserved funds are released safely.
    
    Made-with: Cursor
  503. 701fb50
    fix(execution): restore reconciles from exchange order ids
    Show commit body
    Allow ownOrders snapshot reconciliation to fall back to persisted exchange_order_id and cl_ord_id so already-open Kraken orders can recover even after their local submit tracker is gone.
    
    Made-with: Cursor
  504. 1544d26
    fix(execution): keep open orders alive after submit timeout
    Show commit body
    Treat acked-open submit timeouts as follow-up required instead of timing them out as lost so maker orders can stay live while later fills reconcile normally.
    
    Made-with: Cursor
  505. bc96e34
    fix(execution): reconcile protected MSP exposure with balance truth
    Show commit body
    When startup reconcile sees a protected MSP position, persist the exchange/reconciled quantity from the live balance snapshot instead of echoing the MSP quantity back into state, so stale protected exposure no longer inflates global exposure checks.
    
    Made-with: Cursor
  506. 1421beb
    fix(execution): drop raw l2 depth scan from state hot path
    Show commit body
    Keep live state analysis on state-backed inputs by treating depth_near_mid as optional in the hot path, avoiding multi-minute raw L2 aggregates that stall evaluation cycles.
    
    Made-with: Cursor
  507. 156cde1
    fix(ingest): add raw window indexes for live refresh
    Show commit body
    Add concurrent DBA coverage and a matching ingest migration for `(run_id, ts_local)` raw-table indexes so live universe/activity queries stop stalling on large recent-window scans.
    
    Made-with: Cursor
  508. 4895d8c
    execution: apply global headroom downscale to full submit path
    Show commit body
    After INTENT_DOWNSIZED_TO_CAP_HEADROOM, shadow the intent reference so
    prepare_order, capital gates, DB persist, OrderTracker, and WS submit all
    use the same notional/size as the pre-DB exposure checks.
    
    Made-with: Cursor
  509. d12d6cd
    fix(execution): keep global_liveness true when ingest feeds are fresh
    Show commit body
    Epoch completed_at can age past the window during long evaluations; align
    matrix global_liveness with readiness data_stale when using split ingest.
    
    Made-with: Cursor
  510. f6ff6c2
    fix(execution): fold ingest L2 snapshot age into execution realism
    Show commit body
    When public WS price_cache is quiet, l2_snap_metrics on the ingest pool
    still proves recent market structure. Min(trade, quote, l2_snap) vs limit.
    
    Made-with: Cursor
  511. f19041a
    fix(execution): gate market liveness on min(trade_age, quote_age)
    Show commit body
    Illiquid pairs can lack recent trade prints while L1 ticker updates remain
    fresh. Block only when both trade recency and quote age exceed the limit.
    
    Made-with: Cursor
  512. 3d0682f
    fix(exposure): apply stale balance skip to MSP total_open_notional
    Show commit body
    Mirror the DB-branch 1% rule using exchange_base_spot_sum_for_base so
    inflated MSP reconciled qty does not dominate global exposure when spot
    balance is conclusive and contradicts; None remains inconclusive.
    
    Made-with: Cursor
  513. a8df340
    fix(execution): separate realism edge age from decay first_seen
    Show commit body
    - Add FlowHeapState.symbol_edge_realism_heap_merge_at updated on heap insert/score-improve
    - Keep symbol_edge_first_seen or_insert semantics for global_edge_heap_score only
    - flow_execute_single_candidate derives edge_age_ms from realism map for edge_expired check
    
    Made-with: Cursor
  514. e79838f
    fix(execution): state_updated_at query uses data_run_id for ingest-attached runs
    Show commit body
    Ticker/trade ts_local rows use ingest lineage run_id; querying execution
    run_id returned NULL and kept state_age_secs None.
    
    Made-with: Cursor
  515. 7c51c0d
    perf(execution): state_updated_at from ticker+trade ts_local only (avoid L2/L3 max scan)
    Show commit body
    Four-way GREATEST(MAX(ts_local)) scanned huge L2/L3 tables and stalled evaluation.
    
    Made-with: Cursor
  516. 32f7832
    perf(execution): drop per-symbol ts_local subqueries from run_symbol_state_rows_for_run
    Show commit body
    Correlated GREATEST(MAX(ts_local)) per row blocked live evaluation for minutes.
    Keep run-level state_updated_at from raw ts_local; observability per-symbol
    can use a indexed/materialized path later.
    
    Made-with: Cursor
  517. c4b166b
    fix(execution): derive eligibility freshness from raw ts_local GREATEST
    Show commit body
    state_updated_at used MAX(run_symbol_state.updated_at), which could be
    missing or not reflect market sample time. Use GREATEST of per-table
    MAX(ts_local) on ingest raw tables; plumb per-symbol market_data_last_ts
    into RunSymbolStateRow and CurrentRunMarketRow for observability.
    
    Made-with: Cursor
  518. c5bcb1f
    fix(readiness): soft system_live_ready from market coverage; relax integrity AND; expand fanout
    Show commit body
    Made-with: Cursor
  519. f580d46
    feat(execution): opt-in BOOTSTRAP_EDGE_MODE live microstructure proxy
    Show commit body
    - Config: BootstrapEdgeConfig + env (BOOTSTRAP_EDGE_MODE default false, thresholds, caps, allowlist)
    - pipeline/bootstrap_live_edge: single source compute_microstructure_edge_score_components;
      gross/cost/net bps + confidence; v2 post-process; dedupe Execute per symbol; skip if data_stale
    - Outcome: bootstrap_live telemetry + BOOTSTRAP_LIVE_EDGE_SOURCE
    - flow_execution: extra per-trade notional cap when bootstrap telemetry set
    - live_runner: pipeline config wiring, slot cap, startup log
    
    Made-with: Cursor
  520. 90a2d90
    fix(research): rename prepartition unique constraint before partitioned edgeboard_candidates
    Show commit body
    PostgreSQL requires constraint names to be unique within the schema; after
    RENAMING the base table the old UNIQUE still held uq_edgeboard_candidates,
    blocking the new partitioned table from using the same name.
    
    Made-with: Cursor
  521. e2a9503
    docs(runbook): Edgeboard deploy proof, migration checks, observe vs ingest
    Show commit body
    Made-with: Cursor
  522. 322c8c2
    feat(research): Edgeboard partitioning, background snapshot, InitialCalibration
    Show commit body
    - Partition edgeboard_candidates by month; ensure_partition helpers + SQL script
    - Rename base DDL to versioned migration (sqlx requires numeric prefix)
    - Observe: tokio::spawn periodic edgeboard snapshot (DB-only; EDGEBOARD_SNAPSHOT_INTERVAL_SECS)
    - Cold start: match n>=1 when n<5 unavailable; dominant_reason InitialCalibration
    - Health SQL: route_distribution, avg_sample_size, outcome_coverage_ratio
    - Allocator-ready fields documented in edgeboard mod
    
    Made-with: Cursor
  523. 00a2e0c
    feat(research): Edgeboard deterministic ranking on RESEARCH pool
    Show commit body
    - Add edgeboard.sql migration: candidates, snapshots, meta, outcomes
    - Implement buckets, coverage (feature_quality), routes, fallback tiers 0-3,
      shrinkage/cost/net/confidence, leakage-safe history join on observation_id
    - Atomic snapshot run (truncate minute), delay_until +5m, expires_at +24h
    - CLI: edgeboard-snapshot (EDGEBOARD_SCOPE_MODE), edgeboard-outcomes --snapshot-ts
    - scripts/sql/edgeboard_health.sql for §17 metrics and monitoring
    
    Made-with: Cursor
  524. 4b0519a
    fix(observability): ticker L1 imbalance + adaptive RV windows for MMS hot columns
    Show commit body
    - price_cache: store bid_qty/ask_qty from Kraken ticker; update_with_qty
    - extract: imbalance from ticker when L2 missing or L2 top sizes zero; multi-window RV with strict/relaxed min ticks; feature_quality keys
    - feature_snapshot_health.sql: per-column NULL rates (section 8)
    - docs: runbook + FORWARD_RETURNS_OBSERVABILITY RV/imbalance semantics
    
    Made-with: Cursor
  525. 7cdf2e7
    docs(runbook): activation_only vs periodic MMS, validation vs production, done criteria
    Show commit body
    Made-with: Cursor
  526. ef7d057
    fix(db): allow decision/research DFO table name overlap for pool split
    Show commit body
    Made-with: Cursor
  527. 6eabe25
    fix(research): create krakenbot schema before observability tables
    Show commit body
    Made-with: Cursor
  528. 8d7194c
    feat(observability): RESEARCH pool migrations, forward returns, microstructure
    Show commit body
    - Add migrations/research for directional_forward_observations, market_microstructure_snapshots, feature_definitions
    - Wire observability capture in live_runner; execution_universe vs broad_market periodic scopes
    - Update db_target_precheck, psql_pool, trading_env for RESEARCH
    - Add health SQL scripts and runbook; deprecate decision-table DFO path via migration comment
    
    Made-with: Cursor
  529. e475bb8
    chore(obs): drop noisy DROP in calibration SQL (fresh psql session)
    Show commit body
    Made-with: Cursor
  530. 912a954
    feat(obs): larger-sample directional_forward calibration SQL + runner
    Show commit body
    - scripts/sql/directional_forward_calibration_report.sql: counts, insufficient rate,
      completed stats with stddev_samp + IQR, strategy×direction×regime inventory +
      HAVING min_bucket, hypothesis blocks for 1m–5m ranks and momentum_ride long/short.
    - scripts/directional_forward_calibration_report.sh: trading_env, decision precheck,
      psql_pool decision, MIN_COMPLETED_N gate (default 200, ALLOW_SMALL_SAMPLE=1 override).
    - AGENTS.md: pointer to runner + SQL path.
    
    Made-with: Cursor
  531. d1f3b33
    fix(obs): resolve entry_mid for directional forward capture (cache alias + mid)
    Show commit body
    - entry_mid_at_signal: snapshot bid/ask fallback, BTC/XBT pair alias.
    - Log DIRECTIONAL_FORWARD_SIGNAL_CAPTURE count when evaluation_cycle is set.
    
    Made-with: Cursor
  532. f011507
    feat(obs): Phase 1 directional forward-return rows from readiness activation
    Show commit body
    - Add decision migration for directional_forward_observations with unique key
      (run_id, evaluation_cycle, symbol, activated_strategy, direction).
    - Capture DirectionalSignalMeta in readiness build after real activate_strategies
      for BreakoutImmediate/Confirmed, MomentumRide, VolatilitySurge when evaluation_cycle is set.
    - Live runner: insert pending on decision pool and finalize at signal+900s via price_cache;
      DIRECTIONAL_FORWARD_OBS_ENABLE=0 disables. CLI run-execution-once passes no cycle (no rows).
    
    Made-with: Cursor
  533. 13a232f
    fix(readiness): split entry current spread vs rolling exit cost; CDV readiness spread fields
    Show commit body
    - Latest L2 spread per symbol for entry/gating; rolling window avg for exit drag and spread_net.
    - MarketFeatures: spread_bps (entry) + rolling_spread_bps (cost model).
    - detail_json: readiness_entry_spread_bps, readiness_rolling_spread_bps for DB proof.
    
    Made-with: Cursor
  534. bfc194b
    fix(readiness): rolling L2 spread for cost model, SpreadTooWide anchor, route feature split
    Show commit body
    - Per-symbol rolling AVG(spread_bps) from l2_snap_metrics for cost; state cumulative for slippage baseline.
    - readiness_gate: block tradable when spread_bps > SPREAD_ANCHOR_MAX_BPS (80) with SpreadTooWide.
    - market_features_from_row: spread_bps vs avg_spread_bps aligned with readiness for route_expectancy.
    
    Made-with: Cursor
  535. 234c5e3
    fix: resolve v2_marginal_pending to admitted/rejected in CDV after V2 eval
    Show commit body
    admission_source was always written as v2_marginal_pending but never updated
    to the final outcome. Now resolves to v2_marginal_admitted or v2_marginal_rejected
    after the marginal slot cap, using a two-step UPDATE (admitted first, remaining
    as rejected). Applied on both eval paths in live_runner.
    
    Made-with: Cursor
  536. 9c761e8
    fix: resolve orphaned open-order deadlock in check_symbol_execution_lock
    Show commit body
    Stale/orphaned orders (e.g. acked_open with no fill after crash) permanently
    blocked new entries because the open-order count check ran before ghost
    exposure reconcile. Now detects orphaned orders using a triple proof:
    DB position flat + exchange balance flat + order age > threshold (default 5m).
    Reconciles proven orphans to terminal status before the final lock decision.
    
    Proven case: RAILS/USD order #1232 stuck acked_open since 2026-03-29.
    
    Made-with: Cursor
  537. 3a1eab3
    feat: add admission_source column to candidate_decision_vectors
    Show commit body
    Tracks admission provenance per CDV row: readiness (tradable=true),
    v2_marginal_pending (marginal band, V2 eval pending), hard_blocked.
    Migration adds nullable TEXT column; Rust struct + INSERT updated.
    
    Made-with: Cursor
  538. 78d6b78
    feat: add MAX_MARGINAL_ADMITTED_SLOTS cap for marginal-band trades
    Show commit body
    Caps the number of concurrent marginal-admitted execute candidates per
    evaluation cycle (default 2, env-overridable). Readiness-admitted
    candidates are never capped. Logs MARGINAL_SLOT_CAP when trimming.
    
    Made-with: Cursor
  539. 50fab30
    feat: fanout gate passes marginal symbols when EDGE_ENGINE_V2 active
    Show commit body
    When V2 is enabled, symbols in the marginal band (MarginalForV2 blocker)
    pass through the fanout gate alongside tradable symbols. Adds source
    logging (readiness vs marginal_v2) and marginal_passed counter.
    Applied to live_runner (both eval paths) and run-execution-once runner.
    
    Made-with: Cursor
  540. f7d5348
    feat: add marginal band to readiness gate for V2 admissibility tiebreaker
    Show commit body
    Introduces SURPLUS_MARGINAL_FLOOR_BPS (default -25, env-configurable).
    Symbols with surplus in [marginal_floor, 0) get blocker MarginalForV2
    instead of hard SurplusBelowFloor — eligible for V2 tail-aware admission.
    Adds marginal field to PairReadinessResult/Row and fanout_marginal_symbols helper.
    
    Made-with: Cursor
  541. 58a23eb
    feat(ingest): L3 pipeline heartbeat metrics and RSS peak telemetry
    Show commit body
    - Add L3PipelineMetrics: WS/fan channel fill samples, slow sends (>5ms),
      WS-to-state apply lag, L3 feed reconnect and connection-failure totals,
      updates-before-snapshot counter for coarse sequence sanity.
    - Stamp L3EventEnvelope with ws receive time; optional metrics on client/fan paths.
    - INGEST_L3_PIPELINE_HEARTBEAT each minute with writer bulk queue depth;
      decision DB count for primary_reason=l3_resync_limit_reached.
    - WRITER_METRICS bulk: mpsc buffer used/capacity; WriterSender introspection helpers.
    - RESOURCE_TELEMETRY: track process_rss_peak_kb on Linux.
    - symbol_safety_state::count_rows_with_primary_reason for dashboard-friendly totals.
    
    Made-with: Cursor
  542. 8f4291a
    fix(ingest): bound L3 channels and throttle L3 metrics emits
    Show commit body
    Replace unbounded L3 event queues with bounded mpsc and await sends so
    the WS reader backpressures when downstream is slow. Fan-in merge uses
    a bounded channel as well.
    
    Throttle per-symbol L3QueueMetrics writes on stream updates to 200ms
    (snapshots and 5s interval unchanged) to reduce writer queue pressure
    that previously let unbounded queues grow to OOM.
    
    Made-with: Cursor
  543. aa6298d
    route: path observability log + pullback/passive confidence floor alignment
    Show commit body
    - Log PATH_CONFIDENCE_OBSERVABILITY with path_confidence, min_confidence_for_route,
      rejection, L2 flags, trend_strength, spread_stability, trade_density.
    - Clamp builder confidence to min_confidence_for_route in pullback and passive only.
    
    Made-with: Cursor
  544. cf9f936
    fix: align TrailingStopPathSimulation trace with actual cost model
    Show commit body
    The diagnostic trail_bps in EdgeChainTrace still included the removed
    TSL_TRAIL_BASE_BPS constant, making best/median/worst scenario output
    more pessimistic than the real exit_drag computation. Removed the
    constant from the trace formula and deleted the now-unused constant.
    
    Made-with: Cursor
  545. ec49c13
    fix: use mode-correct fee_bps for move/fee gate and fix trace exit_fee (audit Findings 5+6)
    Show commit body
    fee_bps for MoveBelowFees validation now uses entry_fee + exit_fee
    (respects maker-TP-exit discount). Previously always assumed taker
    exit, making the gate slightly too strict for TP routes.
    
    Removed shadowed entry_fee_bps/exit_fee_bps locals in the trace block
    that had a dead-code bug (exit_fee always taker regardless of branch).
    The trace now reuses the correct variables from the main computation.
    
    Simplified net_edge formula: both entry modes now use the same
    entry_fee_bps + exit_fee_bps path instead of duplicated branches.
    
    Made-with: Cursor
  546. 0a5b671
    fix: make cost breakdown fees mode-aware (entry_fee/exit_fee) (audit Finding 3)
    Show commit body
    Renamed CostBreakdown fields from maker_fee_bps/taker_fee_bps to
    entry_fee_bps/exit_fee_bps. Readiness call sites now pass mode-correct
    fees: Momentum gets (taker, taker), Liquidity/Volume gets (maker, taker).
    
    Previously readiness always charged maker+taker regardless of strategy,
    under-charging Momentum routes by taker-maker bps (~15 bps typical).
    strategy_pipeline.rs already had mode-correct dual evaluation.
    
    Made-with: Cursor
  547. 4463cf5
    fix: remove TSL double-count from trailing-stop exit_drag in route expectancy (audit Finding 4)
    Show commit body
    wait_risk_bps = READINESS_TSL_DRAG_BPS (2.0) already carries the
    TSL/time reserve. The trailing-stop branch of exit_drag also added
    TSL_TRAIL_BASE_BPS (2.0), double-charging 2 bps on every trailing
    route. Removed TSL_TRAIL_BASE_BPS from the exit_drag computation;
    vol-aware trail component and half exit_feas drag remain.
    
    Made-with: Cursor
  548. 4f80b18
    fix: eliminate taker spread double-count in slippage estimator (audit Finding 1)
    Show commit body
    TAKER_SPREAD_FACTOR was 0.50, embedding half-spread in entry_slippage.
    spread_net / spread_impact already carries the same half-spread cost
    in cost_breakdown and route_expectancy. Both were subtracted, charging
    a full spread instead of half a spread for all Momentum/taker routes.
    
    Set TAKER_SPREAD_FACTOR = 0.0 so slippage only captures base + vol
    adverse-fill risk. spread_net / spread_impact remains the sole carrier
    of spread crossing cost. Also resolves Finding 2 (exit_slippage
    inherited entry spread component, overlapping with exit_drag).
    
    Made-with: Cursor
  549. b9fb4d3
    fix(readiness): activation-scoped TSL drag for ride/breakout continuation rows
    Show commit body
    Use 1 bps tsl_drag in cost_breakdown when SelectedStrategy::Momentum and
    activate_strategies selects MomentumRide/BreakoutImmediate/BreakoutConfirmed;
    keep 2 bps for other Momentum activations and all non-Momentum rows.
    
    Align apply_activation_gated_drift_floor_to_pair_readiness with the same
    short-horizon reserve when recomputing blended economics.
    
    Made-with: Cursor
  550. d125209
    fix(economics): remove double-counted exit slippage from surplus and route expectancy
    Show commit body
    Made-with: Cursor
  551. ddb964b
    feat: Kraken margin short paper lane (DECISION persistence + TP/SL resolver v1)
    Show commit body
    - Add ExecutionSurface::KrakenMarginShortPaper and select_entry_route_for_surface
    - Add evaluate_mandate_kraken_margin_short_paper (spot mandate unchanged)
    - Persist krakenbot.margin_paper_trades after CDV when PAPER_MARGIN_SHORT_LANE_ENABLE=true
    - Resolver v1: first-touch TP/SL on trade_samples (labeled tp_sl_v1_approximation in detail_json)
    - Env: PAPER_MARGIN_SHORT_LANE_ENABLE (default false)
    
    Made-with: Cursor
  552. 97c9d63
    feat: activation-gated drift floor economics (phase 1)
    Show commit body
    - Drift floor applies to readiness CDV only for momentum_ride, breakout_immediate,
      breakout_confirmed via drift_blend_applies_to_activated_strategy
    - Baseline readiness uses unblended move; live_runner applies floor after activation
    - route_expectancy: route-template gate (excludes pump-fade/dump-reversal paths)
    - detail_json: drift_proxy_* horizon fields; DRIFT_BLEND_FACTOR unchanged
    
    Made-with: Cursor
  553. c478787
    docs: horizon strategy tuning audit run 1968 (DECISION cohort + competition exports)
    Show commit body
    Made-with: Cursor
  554. 3f55d80
    docs: TRADING_SIGNAL_CDV_AUDIT_RUN1968 deliverable (DECISION live SQL)
    Show commit body
    Full FASE 0–7 audit text keyed to krakenbot_decision via server psql_pool;
    steady-state filter on activation_context_json engine_warmup.
    Notes server repo 94d1a88 vs local observability commit bb341ce deploy gap.
    
    Made-with: Cursor
  555. bb341ce
    CDV trading-signal audit: observability logs and ENGINE_SSOT §6
    Show commit body
    - Warn once (SIGNAL_FRESHNESS_ABSENT) when capturable_move runs without freshness.
    - Warn once (TRAIL_BPS_FALLBACK_USED) when trail_bps_v1 uses stand-in ATR.
    - Log STRATEGY_ACTIVATION_HV_CHAOS_FLOOR when relaxed consistency floor applies.
    - Document CDV JSON fields, surplus/mandate gates, persistence, trail split,
      continuation gap, and server-measured run 1968 facts in ENGINE_SSOT.md.
    
    Made-with: Cursor
  556. 94d1a88
    perf(db): bound l2_raw_feature_ready scans for live loop
    Show commit body
    Full COUNT(*) on l2_snap_metrics for a long-lived ingest run_id could block
    run-execution-live for minutes after epoch bind. Use LIMIT-capped subqueries
    for the row threshold and for distinct symbols-with-spread.
    
    Made-with: Cursor
  557. 622eac3
    fix(execution): do not stall live loop when snapshot truth is not Healthy
    Show commit body
    Periodic reconcile ran on every iteration while evaluation_count==0 (% 10 == 0).
    If private snapshot liveness was not Healthy, the loop continued before epoch
    binding and pipeline, so evaluation_count stayed 0 forever (no CDV, no
    LIVE_EVALUATION logs). Skip protection submits without truth, but proceed to
    epoch binding and evaluations. Align exec-only path the same way (remove fake
    bump+continue on unhealthy).
    
    Made-with: Cursor
  558. 70850f8
    fix(position): sync open long base to exchange balance both ways
    Show commit body
    When balances snapshot is fresh, reconcile_open_positions_truth_on_load
    now sets base_position to the wallet-reported spot quantity on any material
    drift (raise or lower), not only when balance exceeds DB.
    
    - Add positions::set_long_position_qty_from_exchange_truth for shared UPDATE
    - Emit POSITION_TRUTH_EXCHANGE_SYNC funnel events (replaces upward-only path)
    - Keep recent-fills quiet period; keep in-flight buy guard only for upward sync
    
    Exchange spot balance remains SSOT; fills_confirmed can be overridden by
    balance_recovery when they disagree.
    
    Made-with: Cursor
  559. 05e57a1
    activation: regime-specific consistency floor for momentum_ride in HV/CHAOS
    Show commit body
    Add MIN_CONSISTENCY_FOR_RIDE_HV_CHAOS = 0.22 for HIGH_VOLATILITY and CHAOS
    (non-Noise) regimes when abs_drift_1m >= 1.5 bps/min. The normal floor of
    0.30 applies in all other regimes and for weak drift.
    
    Motivation: tail analysis showed 85/89 no_opportunity rows in the top-1%
    expected_gross_capture tail had momentum_ride as the nearest eligible
    strategy, rejected solely for direction_consistency_low. In HV/CHAOS
    regimes, micro-direction switches are intrinsic; requiring 0.30 consistency
    over-filters genuine high-drift entries. The 1.5 bps/min drift gate prevents
    the relaxation from widening low-drift or marginal paths.
    
    Files: src/pipeline/strategy_activation.rs, CHANGELOG.md
    Made-with: Cursor
  560. 658d73f
    chore(docs): regenerate CHANGELOG commit ledger; sync counts (422/421)
    Show commit body
    - Ledger includes all commits through tip-1; multilingual header counts updated.
    
    Made-with: Cursor
  561. 9bff6ce
    docs(changelog): activation Lane A entry (d7717b4); bump RAG embed bundle
    Show commit body
    - Document feat(activation) breakout split + consistency/maker relax in CHANGELOG.
    - Sync multilingual ledger counts (421 commits / 420 table rows after this commit).
    - docs/SITE_DOCS_AND_CHATBOT_PIPELINE.md: markdown bold for DOC_STATUS/DOC_ROLE.
    - rag-backend/EMBED_BUNDLE_VERSION: 8 -> 9 (CHANGELOG/docs touch per CI).
    
    Made-with: Cursor
  562. d7717b4
    feat(activation): split breakout drift floors; Lane A relax (confirmed, ride, passive)
    Show commit body
    - Replace MIN_DRIFT_FOR_BREAKOUT with MIN_DRIFT_FOR_BREAKOUT_IMMEDIATE (2.35) and
      MIN_DRIFT_FOR_BREAKOUT_CONFIRMED (2.10); confirmed 3m gate still uses * 0.65.
    - Relax momentum ride consistency floors and maker passive max |drift| caps.
    
    Made-with: Cursor
  563. 0b6f662
    scripts: add monitor_cdv_warmup_steady.sh for decision CDV warmup metrics
    Show commit body
    Made-with: Cursor
  564. 1ce75c3
    chore(ci): regenerate changelog ledger; sync counts; bump RAG EMBED bundle
    Show commit body
    Regenerates docs/reports ledger for anchor..HEAD so verify_changelog_coverage passes.
    Updates multilingual ledger counts (417 commits / 416 rows). EMBED_BUNDLE_VERSION 8
    required when docs/ or CHANGELOG.md change per verify_rag_bundle_bump.sh.
    
    Made-with: Cursor
  565. 989a87e
    fix(cdv): persist market_regime and align live activation with readiness
    Show commit body
    - Add market_regime to StrategyActivation; apply_activation_to_cdv sets regime column.
    - INSERT candidate_decision_vectors now includes regime (was omitted, always NULL in DB).
    - Live CDV path uses per-pair p.regime for activate_strategies instead of synthetic detect_regime(zeros).
    
    Made-with: Cursor
  566. eefab1b
    fix(live): bind ingest_epochs.run_id to ingest observation run for feature readiness
    Show commit body
    When run-execution-live runs alongside krakenbot-ingest, raw L2 lives under
    observation_runs.mode=ingest, but epochs were created with execution_live run_id.
    Epoch binding then pointed l2_raw_feature_ready at the wrong run (0 L2 rows).
    
    - Add epoch_queries::latest_ingest_observation_run_id
    - Use epoch_data_run_id for initial and refresh create_epoch / universe snapshots
      when ingest_provides_data; refresh re-queries latest ingest run
    
    Made-with: Cursor
  567. 54ed6fb
    fix(live): stop mandate-edge panic loop; route ingest_epochs via ingest pool
    Show commit body
    - Replace raw panic when mandate=execute positive CDV but zero orders with
      execution_truth error, atomic mismatch counter, and consistency_watchdog_snapshots
    - detect_positive_mandate_execute_edge_candidates: mandate_decision=execute,
      admitted, execute_candidate, exclude readiness_* reason codes
    - create_epoch / universe snapshot / update_epoch_status use ingest_pool
      (same as lineage) so krakenbot.ingest_epochs resolves on ingest DB
    
    Made-with: Cursor
  568. ed3640d
    fix(rag): parse DOC_STATUS/DOC_ROLE with optional Markdown bold
    Show commit body
    SITE_DOCS_AND_CHATBOT_PIPELINE used **DOC_STATUS:** lines; _extract_meta
    only matched bare DOC_STATUS:, so chunks fell back to BACKGROUND. Align
    four pipeline docs with other allowlisted docs (bare headers) and make
    regex accept both forms. Bump EMBED_BUNDLE_VERSION for reindex.
    
    Made-with: Cursor
  569. 0afc889
    fix(maintenance): use psql_pool.sh in db_inventory_compare
    Show commit body
    Replaces raw psql against INGEST/DECISION URLs so assert_no_raw_psql and
    AGENTS policy are satisfied; identity prelude remains on stderr per pool.
    
    Made-with: Cursor
  570. 5ce273d
    fix(ledger): f-string anchor in coverage line; regen; sync 412-count copy; EMBED 6
    Show commit body
    Made-with: Cursor
  571. 5c65b02
    docs: viertalige site/RAG-pipeline (nl/en/de/fr); ledger zonder HEAD-rij; RAG taal per map
    Show commit body
    Made-with: Cursor
  572. a90f935
    docs: changelog ledger (409 commits), RAG CHANGELOG index, CI gates
    Show commit body
    - Add machine-generated docs/reports/CHANGELOG_COMMIT_LEDGER_2026-03-19_to_HEAD.md
    - scripts: generate_changelog_ledger.py, verify_changelog_coverage.sh, verify_rag_bundle_bump.sh, check_public_docs.sh; changelog_coverage.anchor
    - CHANGELOG.md: public website overview (2026-03-20–04-01) + ledger link; SITE_DOCS_AND_CHATBOT_PIPELINE.md
    - rag-backend: index root CHANGELOG.md; EMBED_BUNDLE_VERSION=2; bundle bump enforced in CI
    - .github/workflows/ci.yml: doc gates + cargo fmt/check
    - docs: DOC_INDEX, RAG_BACKEND_SPEC, DEVELOPMENT_RULES, recovery report
    
    Made-with: Cursor
  573. 898ee8b
    feat(activation): regime weights, warmup CDV fields, momentum/maker gates
    Show commit body
    - Persist price_history_span_seconds and engine_warmup (<300s) plus strategy
      counts and raw/adjusted scores in activation_context_json.
    - Apply per-regime strategy weights to raw scores before ranking; document
      pipeline order (activation before route/exit/mandate).
    - MomentumRide: OR gate for standalone 3m and 3m/1m bridge; relaxed 1m/3m
      alignment only for standalone 3m path.
    - Maker passive: reject when |drift_3m| > 1.8; rejected_strategies_json adds
      score_raw.
    - price_cache: price_history_span_seconds for CDV diagnostics.
    
    Made-with: Cursor
  574. fdbe25e
    feat: strategy-space calibration — activation, maker tilt, fill estimate
    Show commit body
    Activation (strategy_activation.rs):
    - Breakout drift floor 3.0→2.35; confirmed 3m gate uses 0.65× factor.
    - Momentum: lower 1m floor 1.0→0.55; add 3m/1m alternate gate; slightly
      lower consistency floors; boost score weights; penalize maker passive
      earlier (|1m|>2.15) and reduce passive score cap (~2.45 vs 3.0 spread term).
    - Mean reversion snapback: weaker 1m floor 2.0→1.55.
    - Volatility surge: vol_proxy 0.8→0.68; vacuum density band widened.
    
    Entry route (entry_route.rs):
    - Maker min fill prob 0.25→0.18 (CDV/readiness pessimism).
    - estimate_maker_fill_probability: L3 flag with zero quality falls back to
      spread/density blend instead of collapsing to 0.
    
    Made-with: Cursor
  575. 1937f8a
    fix(db): skip ingest migrations for read-only observability ingest URL
    Show commit body
    Read-only krakenbot_observability cannot create public schema objects; export
    uses INGEST_OBSERVABILITY_DATABASE_URL with run_ingest_migrations=false.
    
    Made-with: Cursor
  576. e9a1fc7
    feat(db): isolate observability ingest via INGEST_OBSERVABILITY_DATABASE_URL
    Show commit body
    - Optional read-only ingest URL for export-observability-snapshots; warn if unset
    - SQL script for krakenbot_observability role with timeouts and SELECT grants
    - OBSERVABILITY_RUN_HEALTH_TIMELINE_LIMIT (1-50) for run-health export scope
    - Docs: measurement-first baseline and rollback; trading_env + systemd comments
    
    Made-with: Cursor
  577. 555fe6b
    fix: drift returns 0 for windows without sufficient history
    Show commit body
    When history span < 50% of the target window, drift returns 0.0
    instead of a fallback value. This prevents all horizons from
    collapsing to identical values when only short-term data exists.
    1m drift works with ~30s+ of data; 5m needs ~150s; 10m needs ~300s.
    
    Made-with: Cursor
  578. 56c6ead
    fix: gate edge-detection panic on system_live_ready
    Show commit body
    The strict positive-edge-but-no-orders check only fires when
    system_live_ready is true. When the execution universe is empty
    (not yet live-ready), positive-edge CDV rows without orders are
    expected behavior, not a system failure.
    
    Made-with: Cursor
  579. 8277599
    fix: readiness CDV spawn computes full V2 decision chain
    Show commit body
    Both integrated and execution-only CDV spawns now call
    select_entry_route, select_exit_policy, and evaluate_mandate
    after strategy activation. This ensures all live engine CDV rows
    have non-NULL entry_route_selected, exit_policy_selected, and
    mandate_decision — even when the execution universe is empty
    and the pipeline receives 0 tradable candidates.
    
    Made-with: Cursor
  580. 37d69fe
    fix: live engine V2 decision chain + drift horizon differentiation
    Show commit body
    1. Live engine data_run_id fix: route analysis and strategy pipeline
       in the integrated eval loop now use data_run_id (the ingest epoch's
       run_id) instead of the live engine's own run_id for data queries.
       This ensures analyze_run_from_state finds market data, enabling
       entry route, exit policy, and mandate computation in live mode.
    
    2. Drift horizon fix: when no samples fall in the [0.85W, 1.15W] age
       bracket, the fallback now selects the nearest available sample to
       the target age instead of always using the oldest point. This
       prevents all 5 horizons from collapsing to identical values.
    
    Made-with: Cursor
  581. 69c7e28
    fix: resolve all compile errors across test targets
    Show commit body
    - exit_path.rs / market_regime.rs: add missing drift_metrics field to
      MarketFeatures test constructors
    - protection_flow.rs: remove tests referencing removed
      submit_stop_loss_and_wait_ack function, removed hard_block and
      add_stop_loss_order from trait mocks that no longer match the trait
      definitions, add missing OpenOrderAnalysis import
    - order_state_lifecycle.rs: rewrite tests to match current OrderState
      API (OrderEvent/apply_state were removed from the module)
    - entry_route.rs: remove unused DriftMetrics/DriftSign/RejectedStrategy
      imports
    - strategy_readiness_report.rs: correct RegimeChaos → RegimeChaosNoise
      blocker label for noise-blocked pairs, add to primary_blockers list
    
    Made-with: Cursor
  582. d608dae
    fix(lifecycle): eager transition logging to prevent double-hop loss
    Show commit body
    transition_to previously deferred recording via a single-slot
    last_transition_from Option, which was silently overwritten when
    evaluate_inner triggered two transitions in one call (e.g.
    TimeStopImminent → TakeProfitReached). Now records each transition
    immediately at the callsite with full context (action, trigger,
    pnl, conditions snapshot). Adds regression test for the exact
    BreakEvenArmed → TimeStopImminent → TakeProfitReached scenario.
    
    Made-with: Cursor
  583. 4c70749
    feat(chaos): ChaosSubtype directional/noise split with DB-first explainability
    Show commit body
    ChaosDirectional (direction_consistency >= 0.5 OR |drift_3m| >= 3 OR
    |drift_5m| >= 2 with alignment) unlocks strategy eligibility in CHAOS
    regime — it does NOT bypass hard safety invariants (mandate spread-vs-stop,
    fill probability, spot-short checks all still apply).
    
    ChaosNoise blocks ALL strategies except NoOpportunity with machine-readable
    "chaos_noise_regime" rejection reason for every strategy.
    
    Changes:
    - strategy_activation.rs: ChaosSubtype enum, classify_chaos_subtype(),
      evaluate_all() early-rejects for Noise, chaos_subtype stored in
      StrategyActivation and persisted to CDV
    - strategy_selector.rs: candidate_strategies_for_regime_chaos() allows
      Momentum for ChaosDirectional in legacy path
    - readiness_gate.rs: RegimeChaosNoise blocker variant, chaos_directional
      param splits the CHAOS gate, chaos_subtype in PairReadinessResult
    - strategy_readiness_report.rs: computes drift to classify chaos subtype
      and passes chaos_directional to readiness evaluation
    - CDV: chaos_subtype column populated from activation result
    
    Made-with: Cursor
  584. bcd89cc
    feat(mandate): CDV migration + mandate persistence with full policy chain JSONB
    Show commit body
    Migration 20260401144000 adds 5 columns to candidate_decision_vectors:
    mandate_decision, mandate_reason_code, mandate_reason_detail,
    mandate_entry_route_reason, mandate_policy_chain_json (JSONB).
    
    The mandate_policy_chain_json carries the complete decision path
    (strategy activation, entry route, exit policy with concrete parameters,
    and verdict) without lossy degradation — downstream analysis can
    reconstruct exactly why a candidate was Execute/Observe/Block.
    
    Pipeline builds mandate_by_symbol map from the full policy chain
    (activation + entry_route + exit_policy + edge + spread) and threads
    it through all 5 persist_pipeline_candidate_vector call sites.
    
    Made-with: Cursor
  585. fb038e4
    feat(mandate): execution mandate + DB-explainable lifecycle transitions + noise dampening
    Show commit body
    New module src/pipeline/execution_mandate.rs:
    - MandateDecision: Execute / Observe / Block
    - DominantReasonCode: 10 machine-readable codes (approved,
      no_opportunity_strategy, entry_route_blocked, spot_short_blocked,
      exit_policy_emergency_only, direction_conflict, insufficient_edge,
      spread_exceeds_stop, fill_probability_too_low, time_stop_too_short)
    - MandateResult: carries the full policy chain (activation + entry
      route + exit policy) without lossy degradation.  exit_policy is the
      original ExitPolicyResult, not an ExitConfig reconstruction.
    - evaluate_mandate(): coherence checks across the full chain.
    
    position_state_machine.rs improvements:
    
    DB-explainable lifecycle transitions:
    - LifecycleTransitionEvent struct: from_state, to_state, action,
      pnl_bps_at_transition, elapsed_secs, peak/trough, trigger,
      conditions_snapshot.
    - transition_log: Vec recorded on every state change.
    - transition_log_json(): serializable for DB persistence.
    
    Noise dampening (non-PnL hooks):
    - MIN_ELAPSED_FOR_CONDITION_HOOKS_SECS = 30: no condition-based
      tightening in the first 30s after open.
    - SPREAD_BLOWOUT_DEBOUNCE = 3: spread must exceed threshold for 3
      consecutive ticks before triggering tighten.
    
    Symbol-level helper documented as compatibility-only:
    - get_lifecycle_for_symbol() carries explicit WARNING that it is NOT
      the source of truth for multi-position-per-symbol scenarios.
      Callers must prefer get_lifecycle(symbol, order_id) when available.
    
    Made-with: Cursor
  586. d350832
    fix(ingest): shorten run health export query; safer observability timer; autovacuum SQL
    Show commit body
    - Replace LATERAL MAX probes with GROUP BY run_id IN (runs) in run_health_timeline_last_n
    - Use OnUnitInactiveSec for observability export timer to avoid overlapping exports
    - Add ingest run_symbol_state autovacuum reloptions script + operator doc (trader_rewrite timeout skipped when shared with writer)
    
    Made-with: Cursor
  587. bfc0979
    feat(lifecycle): wire PositionLifecycle into runtime + market condition hooks
    Show commit body
    Runtime integration — position_monitor now uses policy-derived thresholds:
    - TP: lifecycle.take_profit_bps() instead of hardcoded 200 bps
    - BE: lifecycle.break_even_trigger_bps() instead of hardcoded 30 bps
    - Trail: lifecycle.trailing_distance_bps() instead of hardcoded 0.7%
    All three fall back gracefully to existing constants when no lifecycle
    exists (legacy positions pre-dating the state machine).
    
    exit_lifecycle: registers a PositionLifecycle on fill via
    register_lifecycle(symbol, order_id, policy).  Logs lifecycle state
    for traceability.
    
    Global lifecycle registry: keyed by (symbol, order_id) for per-
    position identity.  Provides get_lifecycle, get_lifecycle_for_symbol,
    update_lifecycle, remove_lifecycle, gc_closed_lifecycles.
    GC runs on each position_monitor refresh cycle.
    
    Market condition hooks: MarketConditionUpdate struct carries drift,
    accel, spread, direction_consistency, l3_quality.
    evaluate_with_conditions() checks non-PnL signals (drift weakening
    while trailing → tighten; spread blowout → tighten) without
    overriding panic/time-stop.
    
    ExitPolicyResult::from_exit_config() reconstructs policy from
    ExitConfig for lifecycles created at runtime (after potential
    degraded-policy mutation).
    
    Invariants:
    1. PositionLifecycle remains pure decision layer — never touches orders
    2. Runtime actions traceable: lifecycle registered with order_id
    3. Per-position identity preserved via (symbol, order_id) key
    4. Non-PnL hooks available even though initial integration uses PnL
    
    Made-with: Cursor
  588. 8b5ad6f
    feat(lifecycle): position state machine — strategy decision state layer
    Show commit body
    New module src/pipeline/position_state_machine.rs:
    
    LifecycleState enum (8 states): OpenAtRisk, BreakEvenArmed,
    TrailingActive, TakeProfitReached, FadeTargetReached,
    TimeStopImminent, ExitRequested, Closed.
    
    LifecycleAction enum: Hold, ArmBreakEven, ActivateTrailing,
    TightenTrailing, TakeProfit, FadeExit, TimeStopExit, PanicExit.
    
    PositionLifecycle: per-position (per-candidate) tracker with frozen
    ExitPolicyResult parameters.  evaluate(pnl_bps) returns the
    recommended action and advances state.  Peak/trough PnL tracked.
    
    Separation of concerns:
    - This module = strategy policy state (which phase, which thresholds)
    - ExitManager/exit_lifecycle/position_monitor = exchange/order state
    - No parallel exit engine: existing machinery consults this layer
    
    Policy parameters exposed as accessors (break_even_trigger_bps,
    trailing_activation_bps, trailing_distance_bps, etc.) so
    position_monitor can use them instead of hardcoded constants.
    
    Three distinct evaluation paths based on policy type:
    - Trailing: OpenAtRisk -> BE -> TrailingActive -> tighten
    - Static: OpenAtRisk -> BE -> TakeProfit
    - Fade/Snapback: OpenAtRisk -> (BE) -> FadeTargetReached
    
    Panic and time-stop override all paths unconditionally.
    
    Made-with: Cursor
  589. 26a4920
    feat(cdv): exit policy migration $79-$86 + pipeline wiring
    Show commit body
    Migration 20260401143000 adds 8 nullable columns to
    candidate_decision_vectors: exit_policy_selected, exit_policy_reason,
    initial_stop_bps, break_even_trigger_bps, trailing_activation_bps,
    trailing_distance_bps, fixed_take_profit_bps, time_stop_seconds.
    
    CDV struct extended with matching fields ($79-$86).
    apply_exit_policy_to_cdv() stores the authoritative ExitPolicyResult
    including all concrete parameters — SQL analysis shows exactly why
    a candidate got TightScalp vs TrailingBreakout vs FadeRevert.
    
    Pipeline wiring: exit_policy_by_symbol HashMap built once from
    activation + entry_route + drift + spread, threaded through all 5
    persist_pipeline_candidate_vector call sites.
    
    EmergencyMarket is only written for explicit exceptional reasons
    (no_opportunity_strategy, no_family) — never as a silent fallback.
    
    Made-with: Cursor
  590. 77e8670
    ops: include Postgres pool snapshots in incident captures
    Show commit body
    - Timebox DB commands and include db_target_precheck + pg_stat_activity + pg_stat_statements + bgwriter
    - Make systemd ExecStart point to /usr/local/lib/krakenbot/incident_snapshot.sh
    - Installer now copies script outside repo and ensures /var/log/incident-snapshots exists
    
    Made-with: Cursor
  591. 4ef31af
    feat(position_policy): 10-variant exit policy selection, strategy-driven + route-aware
    Show commit body
    New module src/pipeline/position_policy.rs:
    
    - ExitPolicy enum (10 variants): TightScalp, MakerLadderPassive,
      TrailingBreakout, TrailingMomentum, FadeRevert, SnapbackQuick,
      VolatilityAdaptive, LiquidityDrain, TimeDecayOnly, EmergencyMarket.
    
    - ExitPolicyResult carries concrete parameters for every selection:
      initial_stop_bps, break_even_trigger_bps, trailing_activation_bps,
      trailing_distance_bps, fixed_take_profit_bps, time_stop_seconds.
    
    - to_exit_config() produces an authoritative ExitConfig that is the
      binding source for ExitManager / exit_lifecycle / position_monitor.
      No parallel exit engine created — this module selects policy only.
    
    - select_exit_policy() dispatches on ActivatedStrategy family +
      EntryRouteFamily.  Same strategy with different entry route can
      yield different exit policy (e.g. BreakoutImmediate + taker gets
      TrailingBreakout; + maker gets TightScalp).
    
    Invariants:
    1. Strategy-driven and route-aware dispatch.
    2. to_exit_config() is authoritative, not advisory.
    3. No parallel exit engine — policy selection only.
    4. All results carry concrete numeric parameters.
    
    Made-with: Cursor
  592. 514e331
    ops: add incident snapshot systemd timer
    Show commit body
    - Add scripts/incident_snapshot.sh to write periodic snapshots to /var/log/incident-snapshots
    - Add systemd unit+timer and installer script for git-only server install
    - Captures CPU/mem/disk/iostat/journal/networkd/resolver + krakenbot service tails
    
    Made-with: Cursor
  593. a3c0639
    fix(activation): derive MomentumFade direction from 10m drift, not 1m
    Show commit body
    eval_momentum_fade trades counter to an established 10m trend. It was
    using drift_sign (derived from 1m drift) to determine direction. When
    1m fades or reverses while 10m remains strong, this produced the wrong
    direction — e.g. 10m DOWN + 1m reversed to UP gave Short instead of
    the correct Long (counter to 10m DOWN), causing spot short blocking.
    
    Now derives fade_direction directly from drift_bps_per_min_10m sign.
    
    Made-with: Cursor
  594. cf0e7d6
    feat(cdv): entry route migration + struct $73-$78 + pipeline wiring
    Show commit body
    Migration 20260401142000 adds 6 nullable columns to
    candidate_decision_vectors: entry_route_selected, entry_route_reason,
    entry_fill_probability, entry_expected_fee_bps,
    entry_expected_slippage_bps, entry_expected_latency_bps.
    
    CDV struct extended with matching fields ($73-$78).
    apply_entry_route_to_cdv() stores the exact EntryRouteResult that
    feeds downstream execution — no recomputation or lossy summary.
    
    Pipeline wiring: entry_route_by_symbol HashMap built once from
    activation + v2_state + fee_tier, threaded through all 5
    persist_pipeline_candidate_vector call sites.
    
    Invariants enforced:
    1. Every routed candidate persists all 6 fields.
    2. entry_route_selected=None always has non-empty machine-readable
       entry_route_reason.
    3. Persisted values are the actual selection result, not a recomputed
       summary.
    
    Made-with: Cursor
  595. 2a57aba
    feat(entry_route): strategy-driven entry route selection with 3 hard invariants
    Show commit body
    EntryRouteFamily taxonomy: MakerPassive, MakerStepAhead,
    TakerAggressive, TakerMarket, HybridPassiveThenCross, HybridLimitIoc.
    
    select_entry_route() enforces:
    1. Strategy-driven: breakout/momentum → taker/hybrid;
       maker strategies → MakerPassive/MakerStepAhead.
    2. Direction-aware: Short on spot → None + "spot_short_not_supported".
    3. No silent None: every blocked route carries machine-readable reason
       (fill_probability_too_low, spread_too_wide, insufficient_orderbook_depth,
       spot_short_not_supported, no_opportunity_selected, strategy_family_none).
    
    Legacy EntryMode mapping via to_legacy_entry_mode().
    
    Made-with: Cursor
  596. 32c4a7a
    feat(cdv): migration + struct for 8 activation columns ($65-$72) with pipeline wiring
    Show commit body
    Migration 20260401141000 adds selected_strategy, eligible/rejected
    strategies JSON, activation_reason, activation_score, selected_direction,
    strategy_family, activation_context_json.
    
    apply_activation_to_cdv() populates all 8 fields from StrategyActivation.
    activation_context_json includes spot_execution_blocked_for_short flag
    to prevent Short direction from leaking into spot execution.
    
    Pipeline builds activation_by_symbol map (regime + features + drift)
    and passes to all 5 persist_pipeline_candidate_vector call sites.
    Both live_runner readiness blocks compute activation inline.
    
    Made-with: Cursor
  597. 4725398
    feat(activation): 11-variant strategy activation with explicit rejection reasons
    Show commit body
    ActivatedStrategy taxonomy: BreakoutImmediate, BreakoutConfirmed,
    MomentumRide, MomentumFade, MeanReversionSnapback, MeanReversionGrind,
    MakerStepAhead, MakerPassiveQueue, VolatilitySurge, LiquidityVacuum,
    NoOpportunity.
    
    activate_strategies() scores all 11 against regime + features + drift,
    returns exactly one selected with full audit trail: eligible set,
    rejected list with machine-readable reasons, activation_reason string.
    No black-box fallback to NoTrading — every rejection is explicit.
    
    Legacy SelectedStrategy mapping preserved via to_legacy().
    
    Made-with: Cursor
  598. 2bd433d
    feat(drift): wire DriftMetrics into MarketFeatures, pipeline CDV, and live_runner
    Show commit body
    - Add drift_metrics field to MarketFeatures; enrich_with_drift() calls
      compute_drift_metrics from price history ring buffer.
    - apply_drift_to_cdv() helper populates 13 drift fields on CDV rows.
    - persist_pipeline_candidate_vector accepts drift; all 5 call sites pass
      drift_by_symbol lookup (pre-computed per symbol before the main loop).
    - Both live_runner readiness CDV blocks compute drift inline and apply
      before insert.
    - Route selectors (v1 + v2) call enrich_with_drift after ignition.
    
    Made-with: Cursor
  599. ab589c3
    feat(cdv): migration + struct for 14 drift/movement columns ($51-$64)
    Show commit body
    Migration 20260401140000 adds drift_bps_per_min_{1,3,5,10,15}m,
    accel_bps_per_min2, expected_persistence_min, direction_consistency,
    drift_sign, expected_gross_capture_{bps,long_bps,short_bps},
    persistence_confidence, chaos_subtype.
    
    All existing CDV construction sites use ..Default::default() for the
    new fields (None until pipeline wiring in next commit).
    
    Made-with: Cursor
  600. acccd34
    feat(drift): signed drift/accel/persistence engine with median-anchored windows
    Show commit body
    DriftMetrics with 1m/3m/5m/10m/15m signed drift (bps/min), direction-
    aware gross capture (long/short), acceleration, persistence model,
    alignment signals, and persistence_confidence.
    
    Anchor price uses median of ±15% age bracket to dampen single-tick
    noise without masking genuine level changes.
    
    Made-with: Cursor
  601. 9c66772
    feat(price_cache): add 900s mid-price ring buffer for drift metrics
    Show commit body
    Stores (Instant, f64) mid-price ticks in a per-symbol VecDeque,
    retained for 15 minutes. Exposes price_history() API for the
    drift/acceleration engine.
    
    Made-with: Cursor
  602. cb32443
    fees: allow env override of bootstrap fee bps
    Show commit body
    Adds BOOTSTRAP_MAKER_FEE_BPS and BOOTSTRAP_TAKER_FEE_BPS (clamped) for analysis/testing when no live fee provider is available; defaults remain 25/40 bps.
    
    Made-with: Cursor
  603. 11390e6
    Align readiness fill prob with L3 quality; add env tunables
    Show commit body
    - Move compute_l3_quality_score to analysis::l3_quality (shared with market_features).
    - Readiness: use L3-based quality for hybrid fill_probability instead of edge_score.
    - fill_probability: env FILL_PROB_REF_TIME_SECS, MISSING_L3_PENALTY, FLOOR, HYBRID_BASE (clamped).
    - readiness_gate: READINESS_MIN_FILL_PROB_LIQUIDITY override (clamped 0.05–0.45).
    - Drop redundant route_engine::l3_quality module.
    
    Made-with: Cursor
  604. 15e0c48
    fix(writer): chunk multi-row INSERTs under Postgres bind parameter limit
    Show commit body
    - Cap each INSERT at 32000/columns (ticker+trade 4000 rows, L3 2000 rows)
    - On failure after partial flush, only restore unwritten tail; avoids
      irrecoverable errors and documents behavior in WRITER_PARALLEL_DESIGN
    
    Made-with: Cursor
  605. bcd7367
    ingest: multi-row batch INSERT for ticker, trade, L3 queue metrics
    Show commit body
    - Use sqlx QueryBuilder push_values for one INSERT per flush batch
    - On DB error, restore batch buffer so rows can retry
    - Document DualWriter limits, ops runbook for DNS/network vs WS logs,
      and update L3 scaling + LOGGING WRITER_METRICS notes
    
    Server baseline (ingest): bulk writer_channel_pending saturated at 4096;
    pg_stat_statements showed ~809k single-row L3 inserts before this change.
    
    Made-with: Cursor
  606. af0b969
    feat(edge): strict executable edge markets (top per symbol)
    Show commit body
    Made-with: Cursor
  607. d896321
    fix(cdv): backfill required detail_json keys for edge detection
    Show commit body
    Made-with: Cursor
  608. 5744c1e
    fix(cli): route debug-edge mode via cli_mode
    Show commit body
    Made-with: Cursor
  609. 66e2ccc
    feat(edge): DB-first per-candidate positive-edge detection and hard no-order assertion
    Show commit body
    - Add decision DB query for raw positive-edge candidates (no aggregates)
    - Add debug-edge CLI to print per-candidate truth from Postgres
    - Enforce required detail_json fields (entry_type, exit_type, expected_fill_probability) with ERROR+counter
    - In live execution loop: ERROR if positives exist, and panic if positives>0 but orders==0
    
    Made-with: Cursor
  610. 23bb33c
    fix(cdv): backfill explainability v2 fields for historical rows
    Show commit body
    Made-with: Cursor
  611. 56986f2
    feat(cdv): v2 DB-first explainability fields for no-trade root cause
    Show commit body
    Add non-null edge decomposition + strategy/market context columns to candidate_decision_vectors, and populate them at all write points (route eval, pipeline, readiness, execution precheck).
    CDV insert failures now emit ERROR with run_id+symbol and increment a write-failure counter.
    
    Made-with: Cursor
  612. 89e5e13
    feat(parquet): validate manifest and skip exported partitions
    Show commit body
    Made-with: Cursor
  613. 4144fa6
    feat(parquet): production-grade cold export with manifests, boundaries, ZSTD, and CDV dataset
    Show commit body
    - Manifest completeness: row_count, min/max ts, checksum_sha256, exported_at, schema_version
    - Atomic writes for parquet+manifest; refuse overwrite (idempotency safety)
    - trade_samples: hard boundary via --max-run-id or --ended-before (closed runs only)
    - candidate_decision_vectors: decision DB export with --older-than-days cutoff
    - Chunked streaming export (no full table loads)
    - Parquet compression: ZSTD
    
    Made-with: Cursor
  614. ce4617c
    Ensure candidate_decision_vectors always has evaluation_index
    Show commit body
    Generate negative non-cycle evaluation_index values for route_eval and execution hooks so every vector row has a unique (run_id, evaluation_index) without relying on DB constraints.
    
    Made-with: Cursor
  615. d6187b8
    Harden candidate_decision_vectors writes and indexing
    Show commit body
    Make evaluation_index unique per run, add fail-visible logging + cdv_write_failed counter, and add a concurrent index plus a health query script for DB-first safety checks.
    
    Made-with: Cursor
  616. cc7501f
    Fix trailing comma in candidate_decision_vectors insert
    Show commit body
    Removes a stray trailing comma in the VALUES list that caused a syntax error and prevented readiness vectors from persisting.
    
    Made-with: Cursor
  617. 0cf15bf
    Fix extra placeholder in candidate_decision_vectors insert
    Show commit body
    Removes a stray $38 placeholder so VALUES count matches the target column list, unblocking DB-first persistence for readiness vectors.
    
    Made-with: Cursor
  618. a18864a
    Fix candidate_decision_vectors insert column/value mismatch
    Show commit body
    Restores the insert statement to the canonical column list so persistence does not fail with "more expressions than target columns"; new explainability fields remain nullable and are carried in detail_json for now.
    
    Made-with: Cursor
  619. c9c6f67
    fix(parquet): write manifest parquet_file as string
    Show commit body
    Made-with: Cursor
  620. a7b4c81
    Warn once when readiness vector persistence fails
    Show commit body
    Adds a single warning on the first insert failure per evaluation cycle so DB-first candidate vector persistence errors can't silently yield zero rows.
    
    Made-with: Cursor
  621. fb02e48
    fix(parquet): avoid broken pipe panic when output is piped
    Show commit body
    Made-with: Cursor
  622. b974f1d
    Persist readiness vectors in exec-only loop
    Show commit body
    Ensure candidate_decision_vectors are written in the execution-only evaluation loop so DB-first edge/surplus/blocker evidence is available even when no orders are submitted.
    
    Made-with: Cursor
  623. d8b1efe
    fix(parquet): align timestamp schema with Arrow builders
    Show commit body
    Made-with: Cursor
  624. b814580
    Persist readiness candidate economics to candidate_decision_vectors
    Show commit body
    Writes one row per readiness-evaluated symbol per evaluation cycle so edge/surplus and blocker reasons are queryable DB-first even when no orders are submitted.
    
    Made-with: Cursor
  625. e8654a2
    feat(cold): add Parquet archival CLI for trade_samples
    Show commit body
    Add a read-only cold export path to local-disk Parquet for ingest raw data.
    The new CLI mode `export-parquet-cold` exports ended runs older than a cutoff and
    writes one Parquet file per (dt, symbol, run_id) with a manifest for reproducibility.
    
    Runtime and execution paths remain Postgres-first; no live reads from Parquet.
    
    Made-with: Cursor
  626. 0cce304
    docs(parquet): classify hot/warm/cold and select archival candidates
    Show commit body
    Define where Parquet adds structural value without affecting runtime SSOT.
    Includes a per-dataset classification and an explicit ML lens (raw/features/decision/outcome)
    with strict exclusions for live/execution/safety/state tables.
    
    Made-with: Cursor
  627. 162e5f7
    docs(redis): live validation of pipelining optimizations
    Show commit body
    Made-with: Cursor
  628. 6e402ea
    docs(redis): add latency optimization implementation report
    Show commit body
    Made-with: Cursor
  629. f83707c
    perf(redis): pipeline batch reads and writes for MSP state
    Show commit body
    Latency audit revealed sequential Redis commands as the largest bottleneck:
    - redis_get_batch: 643 sequential HGETs → 1 pipeline = ~240× faster
    - redis_set_state: 2 round-trips (HSET + SADD) → 1 pipeline = 2× faster
    - redis_rebuild_from_db: 1286 sequential commands → 1 pipeline = ~200× faster
    
    Changes:
    - redis_get_batch now uses redis::pipe() to batch all HGET commands
    - redis_set_state pipelines HSET + SADD in a single round-trip
    - redis_rebuild_from_db pipelines all writes for startup/reconnect
    
    Impact:
    - MSP runtime phase check: 2.4ms → <10us
    - Position reconciliation: 2.4ms → <10us
    - ~4.8ms saved per evaluation cycle (hot-path)
    
    Audit: docs/redis_latency_audit_2026-03-31.md
    Made-with: Cursor
  630. 2bfd4d8
    perf(postgres): pre-live tuning measurement-first (random_page_cost, effective_io_concurrency, work_mem)
    Show commit body
    Applied safe-now tuning based on comprehensive measurement:
    - random_page_cost: 4 → 2 (both instances, SSD/NVMe-appropriate)
    - effective_io_concurrency: 1 → 100 (both instances, modern SSD)
    - work_mem: 4MB → 16MB (INGEST only, 44GB temp files proved necessity)
    
    Proven improvements:
    - symbol_safety_state multi-get: Seq Scan → Bitmap Index Scan
    - L3 incremental: Parallel Seq Scan (cost 622K) → Merge Append Index Scan
    - Runtime validated: no regressions, services healthy
    
    Deferred: autovacuum per-table overrides (no bloat proof, high risk pre-live)
    
    Settings persistent via ALTER SYSTEM (postgresql.auto.conf).
    Rollback-ready via ALTER SYSTEM RESET + pg_reload_conf().
    
    Made-with: Cursor
  631. 1ce3efe
    fix(tooling): make retention/proof_runner ingest-canonical for observation_runs
    Show commit body
    - Retention: guard deletes per-pool with to_regclass; never require observation_runs on decision.
    - Proof runner artifacts: read observation_runs run_id from ingest pool only.
    
    Made-with: Cursor
  632. 8257997
    fix(ingest): make run_id/id index rollout safe for partitions
    Show commit body
    Skip parent index creation in migration when raw tables are partitioned, and create per-partition indexes concurrently via the DBA script.
    
    Made-with: Cursor
  633. e7d7f3f
    perf(ingest): add (run_id, id) indexes for incremental refresh
    Show commit body
    Adds indexes on raw ingest tables to support WHERE run_id = $1 AND id > watermark patterns used by run_symbol_state incremental refresh. Includes a DBA script to create them concurrently in production.
    
    Made-with: Cursor
  634. a6a61d3
    perf(decision): add time-window indexes for execution_orders and fills
    Show commit body
    Adds execution_orders(created_at) index for 24h/1h observability windows and an expression index on COALESCE(ts_exchange, ts_local) for fills time-window queries. Includes DBA scripts to create indexes concurrently in production.
    
    Made-with: Cursor
  635. e646c4d
    perf(decision): add indexes for positions and open execution_orders symbols
    Show commit body
    Adds partial indexes to speed up positions pinned/exposure queries and a partial index (with DBA concurrent helper) to avoid seq scan + sort for pinned_symbols_from_open_orders.
    
    Made-with: Cursor
  636. 801fedf
    perf(ingest): serve run health counts from run_symbol_state
    Show commit body
    run_health_timeline_last_n no longer counts raw ticker/trade/L2/L3 rows. It sums counts from run_symbol_state per run and uses per-run LATERAL MAX(ts_local) to compute feed freshness.
    
    Made-with: Cursor
  637. 70479e1
    perf(decision): add index for raw_private_executions unmatched parsed fetch
    Show commit body
    Adds composite index on (matched_status, parse_status, first_seen_at) and a DBA script to create it concurrently in production.
    
    Made-with: Cursor
  638. 3ee9edd
    fix(safety): reset l3_resync_count when hard block expires
    Show commit body
    Keep clear_expired_hard_blocks consistent with clear_l3_resync_blocks by
    also zeroing l3_resync_count; prevents immediate re-block after expiry.
    
    Made-with: Cursor
  639. 290dad6
    Revert "fix(safety): reset l3_resync_count when hard block expires"
    Show commit body
    This reverts commit 04f02595e66400732fef02a4a467edf23b33b36d.
  640. 04f0259
    fix(safety): reset l3_resync_count when hard block expires
    Show commit body
    clear_expired_hard_blocks now also zeroes l3_resync_count to prevent
    immediate re-blocks after expiry cleanup.
    
    Made-with: Cursor
  641. 02805bd
    Revert "docs: PostgreSQL ingest/decision query audit report (2026-03-30)"
    Show commit body
    This reverts commit 1418a3dc1c228d48d68b9f7c197ef9959bde6583.
  642. 8153b51
    Revert "fix(safety): reset l3_resync_count when hard block expires"
    Show commit body
    This reverts commit 4fcc1b58ddb245f87b314dc4f3aa6686a60765e9.
  643. a666000
    fix(live): retry instrument preload with bounded backoff
    Show commit body
    Instead of failing run-execution-live immediately when instrument preload fails, retry in-process with exponential backoff capped at 60s to avoid restart storms on transient network/DNS issues.
    
    Made-with: Cursor
  644. 4fcc1b5
    fix(safety): reset l3_resync_count when hard block expires
    Show commit body
    When clearing expired hard blocks, also clear l3_resync_count so symbols return to a clean safety state.
    
    Made-with: Cursor
  645. 1418a3d
    docs: PostgreSQL ingest/decision query audit report (2026-03-30)
    Show commit body
    Made-with: Cursor
  646. cf18695
    fix(safety): auto-clear expired hard_blocked symbols at startup
    Show commit body
    Symbols with mode='hard_blocked' and expired hard_block_until were
    never reset to 'normal', permanently inflating dashboard counts.
    Added clear_expired_hard_blocks() called at startup of ingest,
    execution, and exec-only paths.
    
    Made-with: Cursor
  647. 9189d80
    fix(observability): scope run_health_timeline query to relevant run_ids
    Show commit body
    The CTEs for ticker/trade/L2/L3 aggregation were scanning ALL rows
    across all runs instead of filtering by the last N run_ids from the
    runs CTE. With millions of L2/L3 rows this took 5+ minutes, blocking
    DB I/O and stalling the ingest epoch cycle.
    
    Made-with: Cursor
  648. 646acf1
    fix(freshness): eliminate remaining slow L3 MAX(ts_local) queries
    Show commit body
    All 3 remaining callers of last_timestamps_for_run (ingest_runner stream
    health, consistency_watchdog, observability export) only used ticker/trade
    timestamps but still triggered ~10-min L3 table scans. Switched to
    freshness_timestamps_for_run and removed the now-dead function.
    
    Made-with: Cursor
  649. 052d04d
    fix(pinned): include error/reconcile/done as terminal order statuses
    Show commit body
    pinned_symbols_from_open_orders only excluded filled/rejected/canceled,
    causing 711 stale orders (error/reconcile/done) to pin 133 symbols.
    This reduced epoch active symbols from ~170 to ~34.
    
    Made-with: Cursor
  650. 582b026
    fix(freshness): use only ticker/trade for freshness, skip L2/L3 hot-path queries
    Show commit body
    The DATA_FRESHNESS_STATE check was querying MAX(ts_local) on l2_snap_metrics
    (2M+ rows) and l3_queue_metrics (3M+ rows), taking 631 seconds under write
    contention. By the time all four queries completed, ticker/trade timestamps
    were stale, causing false data_stale=true.
    
    Freshness now uses only ticker/trade timestamps (millisecond queries).
    L2/L3 coverage is already checked separately via FEATURE_READY_SIGNAL.
    
    Made-with: Cursor
  651. 7c294a6
    fix(writer): dual-channel writer separates ticker/trade from L2/L3
    Show commit body
    The single bounded mpsc(2048) channel was shared by all data types.
    High-volume L2/L3 writes saturated the channel, blocking ticker/trade
    writes and freezing DB timestamps that readiness depends on — causing
    data_stale=true even when feeds were active.
    
    Split into two independent writer tasks:
    - Priority channel (512): ticker + trade only (freshness-critical)
    - Bulk channel (4096): L2 + L3 only (high-volume)
    
    Each channel has its own writer task, batching, and metrics logging.
    L2/L3 backpressure can no longer starve ticker/trade freshness updates.
    
    Made-with: Cursor
  652. 7ca95b3
    fix(live_runner): use data_run_id for l2_feature_coverage state sync check
    Show commit body
    When ingest_provides_data=true, the coverage check queried run_id (execution's
    own) which has no market data in the DB. This caused STATE_SYNC_REQUIRED to
    fire on every evaluation, blocking the entire eval loop. Now queries
    data_run_id (the ingest epoch's run_id) which has the actual state.
    
    Made-with: Cursor
  653. ae164ca
    fix(arch): unify freshness on ingest SSOT, eliminate duplicate execution writes
    Show commit body
    When a separate ingest process is active, the execution process now:
    - Feeds price_cache from ticker/trade WS in cache-only mode (no DB writes)
    - Skips spawning L2/L3 feeds (ingest provides canonical data)
    - Uses the ingest epoch's run_id (bound_run_id) for all data queries:
      refresh_run_symbol_state, readiness, universe, feature_ready
    - Exec-only mode: ticker WS is always cache-only
    
    Removes microstructure_stale gate from execution_realism. Global data
    freshness is handled by the readiness report (data_stale) which gates
    the evaluation loop. execution_realism now focuses on per-symbol market
    activity: spread cap, tape count, trade recency, edge expiry.
    
    Root cause: execution ran duplicate WS connections and wrote duplicate
    ticker/trade/L2/L3 rows, causing writer channel backpressure (2048
    bounded mpsc saturated by L2/L3 volume) that blocked ticker/trade
    persistence and froze readiness timestamps. Additionally,
    execution_realism used price_cache (in-memory, change-driven) as a
    second freshness truth that diverged from the canonical ingest DB.
    
    Made-with: Cursor
  654. fbab34d
    fix(exit): align static SL breakeven activation threshold
    Show commit body
    Apply the same breakeven activation floor as position_monitor for static stop-loss protection so low-fee accounts don’t diverge between exit lifecycle and monitor takeover.
    
    Made-with: Cursor
  655. 8feb704
    chore(cleanup): delete unused skeleton and stage modules
    Show commit body
    Remove proven-unused skeleton directory and legacy pipeline stage modules that had no call sites.
    
    Made-with: Cursor
  656. cd31084
    chore(cleanup): remove unused skeleton and legacy modules
    Show commit body
    Drop proven-unused skeleton modules and legacy pipeline/validation helpers, and silence a DB-row dead_code warning without altering runtime behavior.
    
    Made-with: Cursor
  657. a8a97d3
    fix(pipeline): make missing state-row liquidity fallback symmetric
    Show commit body
    Use the same conservative spread-only liquidity classification when the per-symbol state row is missing, for both maker and taker entries, to avoid asymmetric forced Ineligible outcomes.
    
    Made-with: Cursor
  658. 36f888a
    fix(pipeline): do not hard-block pure taker via conflict lane
    Show commit body
    Keep maker-preferred behavior by applying taker penalties, but remove the maker-only hard block so profitable taker fallbacks remain possible.
    
    Made-with: Cursor
  659. 64fc1ea
    refactor(edge): use canonical surplus edge in readiness
    Show commit body
    Align readiness edge with route/pipeline by using expected_surplus_bps (cost breakdown) as the canonical expected edge instead of a separate fill_prob-weighted formula.
    
    Made-with: Cursor
  660. efe1285
    refactor(readiness): make spread anchor a soft signal
    Show commit body
    Remove readiness’ hard spread cap so spread admission is single-sourced downstream (conflict lane / realism), while keeping spread health only for high-conviction labeling.
    
    Made-with: Cursor
  661. 0d1a390
    fix(route): align feature-complete with rolling L2 window
    Show commit body
    Use a rolling 15-minute L2 snapshot count for is_feature_complete so restart/run_id rollovers don’t wrongly pre-exclude symbols before edge/route evaluation.
    
    Made-with: Cursor
  662. e68fbc9
    fix(health): base watchdog stats on process run_id
    Show commit body
    Use live runner’s process run_id for funnel/order activity and ingest freshness checks to avoid false stalls when the bound epoch run_id differs from writer scope.
    
    Made-with: Cursor
  663. a0ab818
    fix(exit): enforce BE floor for native trailing stops
    Show commit body
    Cap trailing-stop distances after a fee-aware break-even level so protection cannot trail below break-even once price has moved sufficiently.
    
    Made-with: Cursor
  664. a4a5596
    fix(exit): make break-even fee-aware
    Show commit body
    Compute break-even floors using roundtrip fees instead of fixed bps offsets, for both static SL amend logic and the position monitor’s static SL trailing.
    
    Made-with: Cursor
  665. d8e06fa
    fix(universe): use rolling 15m activity counts for gating
    Show commit body
    Replace per-run cumulative activity counters with a rolling ts_local window so universe selection and epoch validity don’t collapse after run_id rollovers.
    
    Made-with: Cursor
  666. e581d71
    Revert "fix(universe): warm-gate hard trade liquidity filter"
    Show commit body
    This reverts commit 533027677ec7253e929e34f150d8d31dc2a6f471.
  667. 5330276
    fix(universe): warm-gate hard trade liquidity filter
    Show commit body
    Delay hard execution liquidity filtering until run data is warm (sufficient rows and age>=10m), and clamp run-age to avoid divide-by-zero.
    
    Made-with: Cursor
  668. 7be0e7e
    fix(execution): apply spread realism cap only for taker entries
    Show commit body
    Maker/post-only routes already price half-spread capture; enforcing
    EXECUTION_REALISM_SPREAD_CAP_BPS against full spread blocked valid
    Execute plans (e.g. ENTRY_BLOCKED_EXECUTION_REALISM spread_cap_exceeded).
    
    Made-with: Cursor
  669. d8c1a36
    fix(pipeline): maker limit falls back to last ticker top-of-book
    Show commit body
    Thin pairs can skip the 5s fresh ticker window while still passing L2-backed
    V2 admission, which produced only no_price_snapshot_for_maker Skips and
    ranked=0. After snapshot_fresh fails, use snapshot; try normalized pair key for
    BTC/XBT-style cache entries.
    
    Made-with: Cursor
  670. c5712fe
    fix(pipeline): V2 edge floor uses same EV basis as route admission
    Show commit body
    validate_candidate gates on pre-L3 net edge; L3 only scales ranking. The pipeline
    used post-L3 expected_net_edge_bps for edge_floor_block, so valid selections could
    still be skipped and EDGE_FLOW_RANKED stayed empty.
    
    Use max(post-L3, pre-L3) for hard EV checks, conflict taker shortfall, sizing,
    and outcome edge_score so admission matches selection (incl. adaptive edge).
    
    Made-with: Cursor
  671. 57af163
    fix(route): maker pre-L3 net edge matches readiness surplus (no fill_prob wrap)
    Show commit body
    Readiness expected_surplus_bps is linear in capturable/fees/slips/spread/drag.
    The maker branch scaled only the core block by fill_probability while subtracting
    full wait/drag, making route validation far stricter than readiness on low-fill
    names and causing pervasive pre-L3 EdgeNegative.
    
    Made-with: Cursor
  672. ec04465
    fix(route): align surplus stack with readiness (TSL drag, L3 validate)
    Show commit body
    - Replace dynamic wait_risk with fixed READINESS_TSL_DRAG_BPS (2) matching
      strategy_readiness_report cost breakdown.
    - Run validate_candidate / EdgeNegative on net edge before soft L3 multiplier;
      L3 only scales ranking edge and time_adjusted_score per existing intent.
    
    Made-with: Cursor
  673. 1cd91a8
    fix(route): unify expectancy move basis with readiness strategy move
    Show commit body
    Path expected_move alone could be below the move/fee gate while readiness
    already used compute_expected_move_for_strategy (vol_proxy=micro like
    strategy_readiness_report). Use max(path, strategy move) for capturable
    inputs, net-edge move scaling, relative_move_score, and validate_candidate.
    
    Made-with: Cursor
  674. 7a8e431
    fix(edge): drop adaptive matrix rows with disallowed exit regimes
    Show commit body
    Route families included exit modes that select_allowed_exit_regimes never
    admits for the current path/features, yielding only ExitRegimeNotAllowed
    candidates. Pre-filter the regime matrix so evaluation matches the same
    exit-regime contract as the path model.
    
    Made-with: Cursor
  675. ff692b7
    fix(edge): align adaptive V2 regime with readiness detect_regime
    Show commit body
    Readiness uses RegimeMetrics (scaled trade_density) + detect_regime for
    strategy fan-out; adaptive used classify_regime on raw features, so the
    route matrix could disagree with the same symbol’s readiness tradable
    rows. Drive V2 route matrix bucket from the same metrics/mapping and
    refresh payoff asymmetry for the selected bucket.
    
    Made-with: Cursor
  676. cdd7de0
    fix(live): ingest readiness/route/pipeline use writer run_id, not epoch run_id
    Show commit body
    Combined observe+execution path used bound_run_id from epoch while ticker/trade/L2/L3
    WS writers tag rows with the process observation run_id. When LIVE_USE_OWN_RUN_ONLY
    is false, those can differ — last_timestamps_for_run and run_symbol_state then read
    stale MAX(ts_local) for the wrong run. Align with execution-only path (run_id).
    
    Made-with: Cursor
  677. b9cb33b
    fix(readiness): structural freshness uses L2/L3 ts_local, not only ticker/trade
    Show commit body
    data_stale used min age over ticker and trade MAX(ts_local) only; Kraken ticker
    is change-driven so MAX can lag while L2/L3 keep advancing. Align staleness with
    all ingest feeds already returned by last_timestamps_for_run.
    
    Made-with: Cursor
  678. fbf025c
    fix(readiness): align live data_stale with ticker/trade timestamps
    Show commit body
    run_readiness_analysis_for_run_from_state used only MAX(updated_at) on
    run_symbol_state, which could mark DataStale while raw ticker/trade rows
    were still within LIVE_DATA_MAX_AGE_SECS. Structural freshness now matches
    run_readiness_analysis_for_run; last_* fields in the report reflect real
    L2/L3/ticker/trade max(ts_local).
    
    Made-with: Cursor
  679. 0c7de5e
    fix(edge): scenario tail net edge uses regime-scaled tail_upside
    Show commit body
    build_move_distribution already scales tail_upside by regime (e.g. 1.5× for
    VolatilitySpike), but compute_scenario_edges used p90 (= raw upside) for
    net_edge_tail. is_admissible TailPositive therefore ignored that scaling,
    keeping admitted=false and tradable_count=0 despite candidate volume.
    
    Regression test: spike tail net edge exceeds calm for the same path.
    
    Made-with: Cursor
  680. 4fd10fd
    fix(edge): apply adaptive admission for any V1 reject when is_admissible
    Show commit body
    Production logs showed candidates>0 but pairs_no_valid_candidate=10 and
    pairs_valid_but_nonpositive_score=0 — V1 dominant codes included
    path_confidence_too_low, which was never eligible for the narrow override.
    
    When is_admissible passes, promote using scenario ranking edge and recompute
    time_adjusted_score with regime-aware confidence so select_winner_adaptive can
    rank positive scores.
    
    Made-with: Cursor
  681. e3e8584
    fix(route): analyze_run_from_state uses ingest pool when split
    Show commit body
    run_adaptive_route_analysis / run_v2_route_analysis_with_ingest passed the
    decision pool into analyze_run_from_state, which reads observation_runs and
    run_symbol_state on ingest. Use ingest_pool.unwrap_or(pool) for that read.
    
    Made-with: Cursor
  682. c4873a2
    fix(execution): route V2 pipeline ingest vs decision pools
    Show commit body
    analyze_run_from_state reads observation_runs and run_symbol_state on ingest;
    funnel/candidate vectors/shadow trades stay on decision. Live was passing only
    the decision pool and crashed with missing observation_runs. Shutdown UPDATE
    for observation_runs now uses ingest_pool.
    
    Made-with: Cursor
  683. 226181a
    fix(execution): pass edge_score as L3 quality for hybrid fill probability
    Show commit body
    Hardcoded l3_quality_score=0 forced hybrid path to fp*0.5, crushing
    expected_edge_bps and blocking all pairs at readiness EdgeNegative.
    
    Made-with: Cursor
  684. 21dfa73
    feat(db): harden dual-pool ops — no DATABASE_URL, identity suffix, overlap guard
    Show commit body
    - Reject legacy DATABASE_URL; require distinct INGEST_DATABASE_URL and DECISION_DATABASE_URL.
    - Validate current_database() suffix _ingest vs _decision per role.
    - Fail startup if krakenbot table names overlap across pools (DB_SCHEMA_OVERLAP_FORBIDDEN).
    - Route epoch/lineage/run_symbol_state and related reads through ingest pool in live_runner;
      remove ingest→decision sync helpers from run_symbol_state and symbol_safety_state.
    - ingest_runner: decision pool for safety/pinned; drop dual-write of epochs to decision.
    - Add operator maintenance DROP scripts and db_inventory_compare.sh for overlap proof.
    - Update .env.example, trading_env, DBA scripts, and execution systemd comment.
    
    Made-with: Cursor
  685. 4882871
    feat(db): dual-pool env INGEST_DATABASE_URL + split sqlx migrations
    Show commit body
    - Replace DATABASE_URL with INGEST_DATABASE_URL in config, probes, scripts, docs
    - trading_env: INGEST_DB_* URL construction; no legacy DATABASE_URL shim
    - Split migrations into migrations/ingest (13) and migrations/decision (32);
      edge persistence ALTER runs after candidate_decision_vectors (20260331110000)
    - Optional INGEST_/DECISION_EXPECTED_DATABASE_NAME -> DB_TARGET_MISMATCH
    - Runbook, audit, decontaminate inventory SQL, negative-test notes
    
    Made-with: Cursor
  686. cb8393e
    feat(scripts): enforce DB pool via psql_pool.sh + CI assert
    Show commit body
    - Add scripts/psql_pool.sh: ingest|decision|replay|exec with live identity on stderr
    - Add assert_no_raw_psql.sh and run it from scripts/check.sh (no raw psql in scripts/**/*.sh)
    - Migrate all shell scripts that called psql on DATABASE_URL/DECISION URLs to psql_pool
    - measure_reconcile + flow_capture: DECISION_DATABASE_URL only (remove ingest fallback)
    - pg_dba_vacuum_analyze_hot_tables: use decision pool for execution/safety tables (was DATABASE_URL)
    - Docs: AGENTS.md + db-diagnosis-routing note
    
    Made-with: Cursor
  687. 44915c5
    fix(execution): create execution_live observation run on ingest first (dual-DB contract)
    Show commit body
    Aligns with ingest_runner: ingest owns raw-sample FK chain; decision receives
    the same run_id via ensure_observation_run_on_pool. Removes shutdown re-ensure
    to ingest (run already originated there).
    
    Made-with: Cursor
  688. c684a53
    feat(scripts): mechanical DB pool precheck + AGENTS gate
    Show commit body
    - Add db_target_precheck.sh: live SQL identity for ingest/decision with mandatory block output
    - Run precheck before readiness in validate_execution_on_server and validate_live_engine_server
    - ws_safety_report: decision precheck before execution/safety SQL
    - AGENTS.md: operator/agent instructions beyond Cursor rules; link from trading_env + db rule
    
    Made-with: Cursor
  689. eef104c
    fix(execution): mirror observation_runs to ingest at execution_live start
    Show commit body
    execution_live inserts the run on decision; ticker/L2/L3 writers use ingest.
    Without the same run_id on ingest, partitioned l3_queue_metrics FK fails and L3
    state can diverge. Sync row at startup and realign observation_runs id sequence.
    
    Made-with: Cursor
  690. 03c3725
    fix(execution): seed price_cache from ingest before run-execution-once submit
    Show commit body
    CLI one-shot had instrument cache after preload but no ticker WS, so market
    prevalidation failed on empty price_cache. Pull latest bid/ask from
    ticker_samples for the run (fallback l2_snap_metrics) and update price_cache.
    
    Made-with: Cursor
  691. b452080
    fix(execution): preload Kraken instrument WS cache in run-execution-once
    Show commit body
    run-execution-once is a separate process from the long-lived runner; without
    preload_all the constraints cache stayed empty and exchange prevalidation
    skipped submit (e.g. after route-proof zero-tradable fallback).
    
    Made-with: Cursor
  692. 4477ca4
    fix(execution): route proof fallback when V2 tradable_count=0; configurable realism spread cap
    Show commit body
    - When ROUTE_PROOF_MODE is on and no tradable routes, pick tightest-spread
      feature-complete symbol in scope and plan a small taker entry via plan_execution.
    - Add EXECUTION_REALISM_SPREAD_CAP_BPS (5..500, default 60) for flow_execution
      pre-submit spread check.
    
    Made-with: Cursor
  693. 63a0f7f
    feat: MIN_L2_SNAPSHOTS_FEATURE_COMPLETE env for feature-complete gate
    Show commit body
    Default remains 50; operators may lower (1..=200) when a fresh observation
    run must participate in routing before L2 sampler reaches 50 ticks per symbol.
    
    Made-with: Cursor
  694. 891c1de
    fix(v2): sync time_adjusted_score after adaptive admission override
    Show commit body
    Adaptive is_admissible could mark a route valid while the candidate still
    carried V1 negative edge and a non-positive time_adjusted_score, so
    select_winner_adaptive never selected a route (tradable_count stayed 0).
    
    After override, set expected_net_edge_bps from median/tail/best edge and
    recompute time_adjusted_score with the same elasticity factors as
    evaluate_route_candidate.
    
    Made-with: Cursor
  695. fd5e6e2
    fix(execution): run phantom open-order reconcile every epoch; remove is_fresh gate
    Show commit body
    - Periodic phantom cleanup now matches startup: ownOrders coherent only
    - Invoke reconcile_phantom_open_orders_periodic once per bound epoch (live + exec-only)
    - Removes silent no-op when cache was coherent but slightly stale
    
    Made-with: Cursor
  696. e0086ea
    fix(execution): periodic DB open-order reconcile vs exchange ownOrders
    Show commit body
    - Treat coherent+fresh own_orders_cache as SSOT for open Kraken order IDs
    - Mark DB rows in active statuses missing from snapshot as reconcile (incl cancel_requested)
    - Run every 10 evaluations on execution-live and execution-only loops
    - Clarify module docs: DB drift corrected toward exchange, not vice versa
    
    Made-with: Cursor
  697. 057985b
    chore(cursor): enforce ingest vs decision DB routing for diagnosis
    Show commit body
    Made-with: Cursor
  698. 5c14d8f
    Add minimal execution summary counters (no log spam)
    Show commit body
    Single EXECUTION_SUMMARY line per v1/v2 pipeline cycle plus optional
    EXECUTION_FULL_BLOCK when ranked candidates exist but none execute.
    Classify skips into aggregate buckets without per-symbol logging.
    
    Made-with: Cursor
  699. dde0dd6
    collect_open_ssot_evidence: single time-bounded aggregate (avoid full-table max)
    Show commit body
    Made-with: Cursor
  700. 2864c4a
    collect_open_ssot_evidence: set PG statement_timeout for psql
    Show commit body
    Made-with: Cursor
  701. cfd83dc
    Persist SSOT audit rows: fanout, route scope, MSP blocks; run-once regime
    Show commit body
    - trading_funnel_events: FANOUT_GATE_APPLIED (live_runner + run-execution-once),
      FANOUT_ROUTE_SCOPE from route analysis when funnel_pool is decision DB,
      EXECUTION_BLOCKED_NO_MSP*, MSP_ADMISSION_BLOCK from flow path.
    - run_execution_once: stamp intent.regime from CapitalAllocator bucket;
      optional RUN_ONCE_EQUITY_QUOTE for sizing context.
    - scripts/collect_open_ssot_evidence.sh: SQL counts for new markers + regime column.
    - Unit tests for queue_decision and ImprovePrice propagation in execution_planner.
    
    Made-with: Cursor
  702. aa9866c
    docs(ENGINE_SSOT): sluit GEDEELTELIJK af — AFGELOPEN, OPEN, L2/L3-afronding
    Show commit body
    - Statusregels: AFGELOPEN (ontwerp), AFGELOPEN (observatie), OPEN (bewijs pending)
    - Matrix: Multiregime/Maker/MSP/eligibility/reconcile/protection/Fan-out → OPEN; Multistrategy + flow + warming → AFGELOPEN
    - Tabel afronding L2/L3-plan ↔ SSOT; forced validation 2026-03-27 bijgewerkt
    
    Made-with: Cursor
  703. ae18d81
    docs(ENGINE_SSOT): matrix + V3 gate — flow-poller, D5 fan-out, maker-queue
    Show commit body
    - §6: execution bullet uses flow-poller/heap wording (not legacy Top-1)
    - §7: V3 BLOCKING checklist (L2/L3 plan): SQL + journal + L2 regressie AND
    - Multiregime row: primary bewijsniveau runtime-seen; regime column code-wired
    - Maker/queue row: reflects D5 intent wiring; GEDEELTELIJK until ImprovePrice proven
    - Flow poller row renamed; fan-out D5 matrix row + testmethoden; probe/ExitManager VERWIJDEREN row
    
    Made-with: Cursor
  704. d537b00
    feat(execution): run-execution-once uses D5 chain (V2 + fanout + pipeline_v2)
    Show commit body
    - Refresh run_symbol_state, readiness from state + live fee tier (same as live path)
    - Hard-block filter on decision DB, fanout gate + FANOUT_* logs
    - V2 route analysis with EDGE_ENGINE_V2 gate; run_strategy_pipeline_v2 with exec_allowed
    - EXECUTION_DECISION log; pass LiveFeeProvider into submit path
    
    Made-with: Cursor
  705. c1d0298
    chore(execution): D5 cleanup — legacy queue_ahead from L2, lean logs
    Show commit body
    - Legacy readiness pipeline: queue_decision uses depth_near_mid from L2 map when present (was always 0)
    - order submit log: include execution_mode alongside queue_decision/post_only
    - EXECUTION_DECISION: structured fields only (no duplicate format args)
    
    Made-with: Cursor
  706. d3a0cb8
    feat(execution): D5 contract — fanout gate, maker/taker intent, adapter + logs
    Show commit body
    - OrderPlan/ExecutionIntent: execution_mode, post_only, fallback_policy, queue_decision
    - plan_execution validates maker vs taker; ExecutionIntent::from_order_plan enforces contract
    - V2 route analysis optional fanout symbol filter + FANOUT_ROUTE_SCOPE
    - live_runner: intersect exec_allowed with fanout, FANOUT_DECISION / FANOUT_GATE_APPLIED; pass set into V2 ingest
    - kraken_adapter: post_only and queue_decision from intent; EXECUTION_DECISION in flow_execution
    - diagnostics/shadow_compare: adaptive route analysis fanout arg None
    
    Made-with: Cursor
  707. 4eca86e
    docs(SSOT): correct L3/is_feature_complete claim, add L2 persistent chain row
    Show commit body
    L3 data (l3_count) is NOT part of is_feature_complete — that gate checks
    only spread + microprice + l2_count >= 50. L3 absence is handled via
    MISSING_L3_PENALTY and L3_QUALITY_FLOOR_NO_L3 (graceful degradation).
    
    Add statusmatrix row for L2 persistent chain (server-proven, VOLLEDIG):
    run 934, 638/638 symbols l2_count >= 50, ~18 rows/sym/min, reader-sampler
    split (f6c99b6).
    
    Made-with: Cursor
  708. f6c99b6
    l3_ts_partition_retention_drop: support --dry-run flag
    Show commit body
    Made-with: Cursor
  709. 3fa1001
    observe(l2): pass 2 — checksum/resync WARN only via heartbeat thresholds
    Show commit body
    - L2_CHECKSUM_MISMATCH and L2_READER_CHECKSUM_RESYNC_REQUESTED always DEBUG
    - Aggregate WARN: L2_CHECKSUM_MISMATCH_ELEVATED (>=80/30s), L2_CHECKSUM_RATE_EXTREME (>=200)
    - L2_CHECKSUM_RESYNC_STORM when resync batches >=25 per heartbeat window
    - Drop per-symbol cooldown maps; enrich extreme WARN with top_symbols context
    
    Made-with: Cursor
  710. 2cca6ce
    Add L3 time-partition cutover, daily ensure, and partition-DROP retention scripts
    Show commit body
    Made-with: Cursor
  711. 96bd3ab
    Add L3 time-partition target table (daily UTC), BRIN, ensure_l3_daily_partitions()
    Show commit body
    Made-with: Cursor
  712. d9ff636
    observe(l2): move L2_SAMPLE_TICK and L2_SAMPLE_ROW_WRITTEN to DEBUG
    Show commit body
    - Per-interval sampler detail stays at DEBUG; L2_SAMPLER_HEARTBEAT remains INFO
    
    Made-with: Cursor
  713. deb8893
    observe(l2): rate-limit checksum/resync WARN + L2_CHECKSUM_HEARTBEAT
    Show commit body
    - L2_CHECKSUM_MISMATCH: WARN at most once per symbol per 60s, else DEBUG
    - Resync requested/completed: WARN only for symbols newly outside cooldown; completed at DEBUG
    - L2_CHECKSUM_HEARTBEAT on reader 30s tick with aggregate counters and top symbols
    - L2_CHECKSUM_RATE_EXTREME WARN when window mismatch count exceeds threshold
    
    Made-with: Cursor
  714. b31d275
    observe(l3): replace per-symbol l3_metrics_emitted INFO with DEBUG + L3_METRICS_HEARTBEAT
    Show commit body
    - Log per-emit l3_metrics_emitted at DEBUG only
    - Emit aggregate L3_METRICS_HEARTBEAT at most once per 30s with window counts
    
    Made-with: Cursor
  715. 7ba30e4
    L3 maintenance: empty-cut and zero-row skip, optional BRIN SQL, retention preflight skip flag
    Show commit body
    Made-with: Cursor
  716. 8ae4247
    L3 maintenance: scalable preflight (observation_runs + run_id-scoped mx), lighter inventory SQL
    Show commit body
    Made-with: Cursor
  717. 96cb9c2
    Add L3 ingest maintenance: inventory, watermark preflight, batched time-cut delete, retention
    Show commit body
    Made-with: Cursor
  718. c19388f
    fix(l2): split runtime loop into reader + sampler tasks (D1 structural fix)
    Show commit body
    The single-task select! loop could not guarantee 5s sample timer
    execution under continuous WS book traffic. Split into two independent
    tokio tasks sharing books via Arc<RwLock<HashMap>>:
    
    - Reader task: drains WS messages, short write-lock for book mutation,
      handles checksum resync with explicit markers
      (L2_READER_CHECKSUM_RESYNC_REQUESTED/COMPLETED, L2_READER_STREAM_ENDED).
    - Sampler task: 5s interval, short read-lock for minimal snapshot
      extraction, builds rows + sends outside lock.
    - Failure propagation: either task exit aborts sibling, returns Err
      to trigger outer reconnect. Exit logs include explicit cause field.
    - Dual heartbeats: L2_READER_HEARTBEAT + L2_SAMPLER_HEARTBEAT (30s).
    - Subscribe phase unchanged (single-threaded before split).
    
    Made-with: Cursor
  719. 1feba08
    fix(l2): subscribe drain wait for acks or budget, not idle gap
    Show commit body
    10ms read timeout no longer ends the batch drain; only full ack set,
    drain wall budget, stream end, or read error stops the loop.
    
    Made-with: Cursor
  720. c579c57
    fix(l2): bounded subscribe drain + batch timing markers (D1)
    Show commit body
    Evidence: batch drain waited for 10ms idle while book updates never go idle,
    stalling wall-clock minutes per subscribe batch before the sample loop ran.
    
    - Cap per-batch drain at L2_SUBSCRIBE_BATCH_DRAIN_MAX (350ms) and exit early
      when batch_acked >= chunk.len().
    - Add monotonic elapsed_ms markers: L2_BATCH_* lifecycle, first sample/row,
      L2_SUBSCRIBE_BATCH_DRAIN_INCOMPLETE when capped without full acks.
    - Log L2_FEED_LOOP_STARTED / L2_SNAPSHOT_WINDOW_COMPLETE with elapsed_ms.
    
    Made-with: Cursor
  721. a50c4ea
    chore(scripts): add collect_l2_l3_plan_evidence for D1-D2-V3 SQL gates
    Show commit body
    Made-with: Cursor
  722. 67f38ec
    fix(l2): biased select + heartbeats + markers for persistent sampling
    Show commit body
    - Use tokio::select! { biased; } so sample_interval wins over read.next()
      (avoids WS update starvation of L2Snap writes).
    - Explicit read stream end (None) and L2_FEED_LOOP_EXITED; channel close
      logs L2_WRITER_CHANNEL_CLOSED.
    - L2_FEED_HEARTBEAT every ~30s: subscribed_symbols, books_tracked,
      sample_ticks, rows_written_total; L2_SAMPLE_TICK / L2_SAMPLE_ROW_WRITTEN
      aggregates per tick.
    - L2_WRITER_ERROR on l2_snap insert failure (writer).
    - L2_FEED_TASK_ABORTED / L2_FEED_TASK_RESTARTED around L2 handle in
      ingest_runner and live_runner.
    
    Made-with: Cursor
  723. 3af9fb4
    fix(ingest): avoid L2/L3 feed restart when universe sets unchanged
    Show commit body
    Universe refresh ran every 60–120s and always aborted and respawned L2/L3
    feeds even when pool/L2/L3/execution symbol sets were identical. That
    repeatedly reset multi-chunk L3 bootstrap (staggered connections + subscribe
    ACKs), starving l3_queue_metrics and keeping l3_count low.
    
    Restart feeds only when L2 set, L3 set, or execution set (for L2 validation)
    actually changes; log UNIVERSE_SELECTION_SKIP_FEED_RESTART when skipping.
    Initialize ingest current_l2/current_exec from the first snapshot.
    
    Made-with: Cursor
  724. 82f61f6
    docs(ENGINE_SSOT): regime persist open until DB proof; L3 data note
    Show commit body
    - Multiregime: code-wired for regime column; no server-proven upgrade before SELECT
    - New section: regime persist open proof + SQL success criteria
    - Section 6: L3/l3_count and feed coverage; regime fix pending post-deploy order
    - Forced validation test 8 marked not closed
    
    Made-with: Cursor
  725. 3156897
    docs(ENGINE_SSOT): forced validation run 2026-03-27 + regime persist notes
    Show commit body
    Document restart/MSP rebuild evidence, multistrategy log samples, regime DB
    counts; update section 6 and Multiregime row; replace regime gap with historical
    NULL note.
    
    Made-with: Cursor
  726. c6eeabf
    feat(execution): persist capital RegimeBucket to execution_orders.regime
    Show commit body
    Wire regime from sizing_decision through ExecutionIntent to insert_order so
    observability no longer shows NULL for all orders. Protection and external
    backfill paths pass None.
    
    Made-with: Cursor
  727. b35206f
    docs(ENGINE_SSOT): matrix hardening — bewijsniveau-kolom, statusregels, 8 degradaties
    Show commit body
    Sectie 7 herschreven als bewijs-gedreven beslisinstrument:
    - Bewijsniveau-definities (server-proven/runtime-seen/code-wired/docs-only)
    - Afdwingbare statusregels (VOLLEDIG alleen bij server-proven)
    - 8 items gedegradeerd van VOLLEDIG naar GEDEELTELIJK met concrete testmethoden en upgrade-voorwaarden
    - Alle VOLLEDIG items voorzien van concreet bewijs (DB counts, log markers, Redis state)
    
    Made-with: Cursor
  728. 9d46601
    docs(ENGINE_SSOT): NULL inet_server_addr edge case for live triple guard
    Show commit body
    Made-with: Cursor
  729. a686aba
    audit(docs): ENGINE_SSOT matrix verification and closure
    Show commit body
    Full matrix audit (27+ items) against code, runtime, and server state.
    Corrections: execution attach (run-execution-only); DbPools mandatory;
    competitive scoring replaced by V2; ExitManager dead code; dual regime
    system documented; DB model A discontinued. Known gaps documented:
    execution_orders.regime never populated; dual regime not consolidated.
    
    Made-with: Cursor
  730. 31fa607
    feat(db): dual-DB live identity validation and distinct triple guard
    Show commit body
    - Add DbRole, LiveDbTriple, validate_pool_live_identity, assert_dual_db_live_identities_distinct
    - create_pools: connect both pools, validate URL vs current_database, refuse identical triples, then migrate
    - create_pools_from_env_urls for probes; main passes pre-parsed SanitizedDbTarget
    - check-execution-readiness: print and log live triple per pool
    - trading_env_psql_ingest/decision helpers; script role headers; docs and changelogs
    
    Made-with: Cursor
  731. 0ecf8c7
    fix(msp): add flush_dirty diagnostics, material drift gate for entries
    Show commit body
    - flush_dirty: log MSP_FLUSH_CYCLE with dirty_total, eligible count,
      interval to diagnose why market_state_projection DB stays at 0 rows
    - flow_execution: drift_detected only blocks entries when exchange or
      reconciled position qty is non-zero (material exposure), preventing
      phantom drift from stale expected_position_qty from blocking trades
    
    Made-with: Cursor
  732. accc5eb
    fix(deploy): git-only deploy.sh; document in git-only-codeflow rule
    Show commit body
    Replace rsync-to-releases flow (forbidden by git-only policy) with
    ssh git pull --ff-only + cargo build --release on /srv/krakenbot.
    CHANGELOG: note under Systemd/operations.
    
    Made-with: Cursor
  733. 5865903
    docs: MSP bootstrap invariant for live + exec-only (ENGINE_SSOT, CHANGELOG)
    Show commit body
    Document mandatory bootstrap, proof order (logs → decision DB → sample),
    and verify_msp_projection_db.sh; changelog rows for 2bd36ef and c60f165.
    
    Made-with: Cursor
  734. c60f165
    Add verify_msp_projection_db.sh for decision vs ingest MSP proof
    Show commit body
    Documents that market_state_projection must be queried on DECISION_DATABASE_URL;
    ingest DATABASE_URL may show zero rows without indicating MSP failure.
    
    Made-with: Cursor
  735. 2bd36ef
    Fix MSP DB persistence on run-execution-only
    Show commit body
    Execution-only never called init_global, init_engine, redis_rebuild_from_db,
    or seed_symbols (only run-execution-live did), so MSP workers and periodic
    DB flush never ran while Redis could still hold keys from prior runs.
    
    Add bootstrap_msp_engine_and_redis + seed_pool_symbols helpers and invoke
    them at execution-only startup; refactor execution-live to use the same helpers.
    
    Made-with: Cursor
  736. c136add
    Document DB-first validation; trim execution/state dead code
    Show commit body
    - VALIDATION_MODEL + ENGINE_SSOT: PostgreSQL funnel + MSP as primary proof; journal secondary
    - Remove duplicate market precheck and price_cache stale-diagnosis helpers (prepare_order covers path)
    - Wire epoch_completed_at in DATA_FRESHNESS_EVALUATED; flow poller observes skipped; fill avg in logs
    - Delete unused market_state module, load_unprotected_from_msp, ProofState, heap prune helper
    - Consolidate MspEvent allows; explicit reservations for Halt and RepeatedFailure variants
    
    Made-with: Cursor
  737. 6d56e92
    docs: add H/I dead_code scope — large-file inventory, no big refactor in lint rounds
    Show commit body
    Made-with: Cursor
  738. 6e95ecc
    docs(lint): finalize dead_code policy; parent mod allows for skeleton domains
    Show commit body
    - ENGINE_SSOT: definitive policy (no Cargo [lints] dead_code, item-level allow+reason,
      parent mod attributes for domain/skeleton; execution stays explicit)
    - CHANGELOG + CHANGELOG_ENGINE: A/B/C summary for dead_code cleanup
    - main.rs: #[allow(dead_code, reason)] on core/domain/edge/fees/market/observe/pipeline/
      risk/selection/skeleton/trading module declarations
    - analysis/mod: allow on adverse_selection, friction_attribution, latency_breakdown,
      realized_surplus
    - db/mod: allow on read; exchange/mod: allow on kraken_private
    
    Made-with: Cursor
  739. 8cd1b59
    chore: replace blanket dead_code lint with targeted per-item annotations
    Show commit body
    Remove [lints.rust] dead_code = "allow" from Cargo.toml and add
    #[allow(dead_code)] with reason strings on specific items across 78
    files. Remove genuinely unused MSP code (MarketStatePatch,
    upsert_partial, redis_invalidate, count). Adds rust-version = "1.83".
    
    Made-with: Cursor
  740. c2eed1c
    docs: dead_code policy roadmap (execution/state/MSP first)
    Show commit body
    - ENGINE_SSOT: baseline accepted + phased narrowing plan
    - CHANGELOG + CHANGELOG_ENGINE: reference
    
    Made-with: Cursor
  741. 7267ea5
    chore: sqlx 0.8, dead_code via Cargo.toml lints
    Show commit body
    - Bump sqlx 0.7 -> 0.8.6 (drops never-type future-incompat in sqlx-postgres)
    - Remove #![allow(dead_code)] from main.rs; set [lints.rust] dead_code in Cargo.toml
    - Document policy in ENGINE_SSOT + changelogs
    
    Made-with: Cursor
  742. a054350
    fix(msp): align upsert values with target columns
    Show commit body
    Correct the market_state_projection INSERT expression count to match
    its 59 target columns (58 bind placeholders + now()). This resolves
    runtime MSP_FLUSH_FAILED/MSP_EVENT_FAILED SQL errors.
    
    Made-with: Cursor
  743. 9f548e6
    chore: warning debt cleanup, redis 0.32, probe price_cache
    Show commit body
    - Crate-level allow(dead_code) with documented rationale (schema/API reserves)
    - Mechanical fixes: imports, mut, unused assignments, ignition invariant log field
    - Probes: bid/ask from price_cache::snapshot_fresh (no deprecated REST)
    - position_reconcile: reduce classification as bool directly
    - raw_execution_backfill: keep tuple cl_ord_id for mark_event_matched
    - redis 0.25 -> 0.32.7 (fixes never-type future-incompat in redis crate)
    - sqlx 0.7 future-incompat noted in CHANGELOG (upgrade deferred)
    - CHANGELOG.md + docs/CHANGELOG_ENGINE.md
    
    Made-with: Cursor
  744. 1b5ebec
    fix(msp): correct market_state_projection upsert placeholder count
    Show commit body
    The INSERT statement listed more target columns than provided values,
    causing MSP_EVENT_FAILED/MSP_FLUSH_FAILED on server runtime. Add missing
    $57-$59 placeholders for event/runtime fields so upserts succeed.
    
    Made-with: Cursor
  745. 6dd17f4
    execution/docs: align asymmetric MSP gating semantics
    Show commit body
    Make MSP mandatory for entry admission while keeping exit/protection paths
    risk-reducing and degradable. Add hybrid MSP+DB/WS lock/notional checks,
    startup phase visibility, and align docs/changelogs to hard-vs-soft
    eligibility semantics and operational dual-DB wording.
    
    Made-with: Cursor
  746. a360a4e
    msp: implement phase transition and confidence inference in derive_flags
    Show commit body
    The runtime_phase was permanently stuck at "bootstrap" because no
    transition logic existed. This blocked ALL entries via entry_eligible=false.
    
    Three fixes in derive_flags():
    1. Phase transition: bootstrap→warming→live based on market_data_confidence
    2. Confidence inference: exposure/order/protection confidence inferred
       from actual data state (no position + events processed = medium/high)
    3. Readiness flags derived from phase (live = entries allowed)
    
    Made-with: Cursor
  747. 31f9a68
    execution: debounce private ws reconnect requests
    Show commit body
    Rate-limit reconnect requests at the hub boundary so repeated watchdog/live-runner requests cannot trigger rapid reconnect churn.
    
    Made-with: Cursor
  748. bd103a8
    execution: harden private ws reconnect and external TP guard
    Show commit body
    Prevent stale reconnect signals from immediately tearing down fresh private WS sessions, and ignore external TP-like sell orders when no live base position exists to avoid false protection/exit adoption.
    
    Made-with: Cursor
  749. 9ecccc0
    docs: document MSP runtime projection integration
    Show commit body
    Capture the unified market-state projection and Redis runtime read-layer across architecture, runbook, and changelog docs so deploy/readiness behavior stays traceable and consistent with live execution paths.
    
    Made-with: Cursor
  750. cae6ab5
    execution: move MSP migration to unique version
    Show commit body
    Replace the conflicting migration version with a new timestamped file to avoid sqlx duplicate-version and checksum conflicts during dual-DB startup migrations.
    
    Made-with: Cursor
  751. ba904f8
    execution: add unified market-state projection with Redis runtime cache
    Show commit body
    Introduce an event-driven market_state_projection on the decision DB and wire live runtime flows to publish and consume MSP state through Redis for faster, consistent execution/protection/reconcile decisions.
    
    Made-with: Cursor
  752. d768c71
    execution: enforce spot long-only order policy
    Show commit body
    Block sell-side entries in runner and reject negative-qty protection paths so execution remains crypto spot long-only (buy entries, sell exits only).
    
    Made-with: Cursor
  753. 3b22b72
    execution: align spot exit classification with Kraken WS docs
    Show commit body
    Treat reduce/exit coverage by close-side direction relative to live spot position and stop relying on reduce_only as a required signal, matching Kraken WS v2 spot behavior.
    
    Made-with: Cursor
  754. 2e6b975
    execution: treat manual reduce orders as protection truth
    Show commit body
    Merge private WS own-orders reduce quantities into exposure reconcile so manual protection orders are recognized and not repeatedly flagged as unprotected due to DB-only blind spots.
    
    Made-with: Cursor
  755. 5126e04
    execution: prevent unintended protection cancellation on TP/remediation
    Show commit body
    Keep protection active unless TP market submit succeeds, and stop auto-canceling extra reduce orders during reserved-funds remediation so manual protection orders are preserved.
    
    Made-with: Cursor
  756. 73466ca
    execution: unblock entries with per-symbol halt and single submit gate
    Show commit body
    Replace global entry_halt with per-symbol TTL halts, remove duplicate submit-time realism/precheck blockers, and keep submit admission consistent with the flow-stage realism decision.
    
    Made-with: Cursor
  757. e73f51d
    execution: add stale-freshness protection-only escape and configurable reconcile age gates
    Show commit body
    - Add ALLOW_PROTECTION_ON_STALE_FRESHNESS when freshness is stale but private WS is recent and explicit exchange spot sum >= qty_min.
    - Keep entries blocked by using AllowProtectionOnly decision only in reduce/protection flows.
    - Make reconcile freshness caps env-configurable via RECONCILE_BALANCE_MAX_AGE_SECS and RECONCILE_OWN_ORDERS_MAX_AGE_SECS (defaults remain 30s).
    
    Made-with: Cursor
  758. f0482b2
    scripts: check_execution_runtime_truth uses restart log in nohup mode
    Show commit body
    Nohup redirects stdout to run_execution_live_restart_*.log; journalctl _PID
    often lacks FLOW_* markers. Use --log for truth check when not using systemd restart.
    
    Made-with: Cursor
  759. b80f3ae
    execution: log EDGE_BIAS_BACKFILL_WORKER_STARTED marker when backfill disabled
    Show commit body
    check_execution_runtime_truth.sh greps for this substring; emit disabled_noop line
    so waived path passes worker section without changing shell contract.
    
    Made-with: Cursor
  760. b07969f
    execution: waive EDGE_BIAS_BACKFILL self-verify when backfill disabled
    Show commit body
    When EDGE_BIAS_BACKFILL_ENABLE is false no worker runs; mark gate satisfied so
    EXECUTION_RUNTIME_SELF_VERIFY_OK can pass and restart_execution_live_validated.sh succeeds.
    
    Made-with: Cursor
  761. d0b4dc2
    execution: per-symbol reconcile single-flight + phantom-flat cooldown
    Show commit body
    - Gate reconcile_symbol_durable with tokio Semaphore (try_acquire) so concurrent
      calls return SkippedConcurrentInvocation without running phantom logic again.
    - After successful close_dust_position on phantom, record 5s exit-path cooldown
      (SkippedCooldownAfterPhantomResolve); CancelOnlyCleanup bypasses cooldown only.
    - Add atomic + RECONCILE_DECISION metrics for skip totals; reconcile_invoke_guard logs.
    - reconcile_symbol_durable_ignition retries skips for ignition critical path.
    - protection_flow Pending on skip; live_runner/position_monitor info logs (not halt).
    
    Made-with: Cursor
  762. 053cf7d
    obs: reconcile stabilisatie KPIs — SQL script + export JSON + shell runner
    Show commit body
    - reconcile_stabilization_metrics.json from export-observability-snapshots
      (halt funnel emitted/suppressed, freshness-still-halt-after-retry, suppress ratio)
    - sql/analysis/measure_reconcile_stabilization_kpis.sql: HALT windows, phantom vs uncertain,
      BalanceInconclusive text, orders/fills 15m/60m, incident sessions + median duration
    - scripts/measure_reconcile_stabilization_kpis.sh
    
    Made-with: Cursor
  763. 54011b0
    fix(reconcile): exchange spot sum None≠0, HALT dedupe, freshness retry
    Show commit body
    - Use exchange_base_spot_sum_for_base (dual-source sum) for phantom/reserved paths;
      BalanceInconclusive → uncertain, never phantom or DB repair from unknown keys.
    - HALT funnel inserts deduped per (symbol,event)+fingerprint TTL 30s; emitted/suppressed counters.
    - Single 400ms retry only for FreshnessStale when retryable; metric for still-halt-after-retry.
    - protection_flow: no resolved unwrap_or(0); inconclusive → reserved/uncertain paths.
    - Optional FILL_POSITION_DRIFT_LOG=1 before phantom DB correction.
    
    Made-with: Cursor
  764. 7bc8bd7
    populate funnel latency_ms on execution critical stages
    Show commit body
    Record submit-path elapsed milliseconds on realism and order-decision events so 15-minute audits can quantify decision-to-submit latency from persisted funnel data.
    
    Made-with: Cursor
  765. d1e40ed
    add event-driven wake path for flow poller scheduling
    Show commit body
    Wake the flow poller on recycle-driven heap updates so ranking/submit no longer depends only on timer cadence and emits explicit heap refresh markers for timing audits.
    
    Made-with: Cursor
  766. 1cbb53d
    disable edge-bias backfill in execution hot path by default
    Show commit body
    Gate the realized-move backfill worker behind EDGE_BIAS_BACKFILL_ENABLE so execution no longer runs nearest-mid diagnostic scans unless explicitly enabled.
    
    Made-with: Cursor
  767. e17a1f7
    enforce explicit trade-recency guard in shared realism contract
    Show commit body
    Require last-trade freshness to be present and within the same realism horizon used by admission and submit so stale candidates are blocked earlier and reason aliases remain audit-compatible.
    
    Made-with: Cursor
  768. 63ca853
    stop repeated phantom halts after reconcile auto-correction
    Show commit body
    When reconcile returns skip-no-real-position or corrected-phantom-to-flat during amend checks, remove the symbol from the monitor set so stale in-memory position state does not keep re-triggering HALT_PHANTOM_POSITION events.
    
    Made-with: Cursor
  769. 1fc1b34
    enforce per-call order price/constraint invariants across entry and exits
    Show commit body
    Require price presence only for limit orders, hard-drop market limit_price on wire, and remove 5dp/fallback price paths in exit flows so all priced submits must normalize against live instrument constraints.
    
    Made-with: Cursor
  770. 2a0dc47
    harden ws price quantization fallback to prevent invalid price rejects
    Show commit body
    When instrument constraints cache misses, quantize outbound limit/static trigger prices conservatively on wire (USD pairs to 4dp) instead of sending raw floats, preventing exchange invalid-price rejections across entry/exit paths.
    
    Made-with: Cursor
  771. 4fc9134
    add cooldown for repeated microstructure-stale execution realism blocks
    Show commit body
    When execution realism blocks due to microstructure stale, set a temporary symbol quiet window so stale symbols stop re-entering every cycle while market data catches up.
    
    Made-with: Cursor
  772. 763e462
    add tape-gate quiet cooldown to reduce repeated dead-symbol retries
    Show commit body
    Apply a short symbol quiet period after tape_gate_entry_blocked and check quiet/hard-block state before tape evaluation so repeatedly underactive symbols stop consuming admission cycles.
    
    Made-with: Cursor
  773. d026edf
    tune conflict lane defaults to reduce false-negative admissions
    Show commit body
    Lower the quality-override tape floor and default spread minimum to avoid blocking otherwise valid low-spread candidates while preserving the 60 bps hard safety cap.
    
    Made-with: Cursor
  774. 195cb39
    align selection and submit with a shared execution realism contract
    Show commit body
    Use one microstructure/freshness decision path across tape admission and submit precheck, emit unified realism observability with legacy aliases, and add O(1) trade recency cache signals to reduce stale-mid mismatches.
    
    Made-with: Cursor
  775. 323682f
    fix(execution): align market precheck freshness with live data contract
    Show commit body
    Apply a global freshness floor from LIVE_DATA_MAX_AGE_SECS to mid-snapshot and price-cache prechecks so tier defaults (5/15/30s) no longer cause avoidable missing-mid failures on active symbols.
    
    Made-with: Cursor
  776. 37def28
    fix(execution): avoid sending market price on Kraken add_order
    Show commit body
    Prevent market submissions from carrying intended_price_quote as wire limit_price, so pair decimal precision checks do not reject otherwise valid market entries.
    
    Made-with: Cursor
  777. 56b71d9
    docs(env): document MAX_OPEN_NOTIONAL_PCT and conservative PnL exposure mult
    Show commit body
    Made-with: Cursor
  778. f42cd00
    fix(scripts): harden execution restart self-verify journal wait
    Show commit body
    Use systemd ActiveEnterTimestamp as journal --since anchor, extend wait
    to 120s to tolerate journal indexing lag.
    
    Made-with: Cursor
  779. ae5dd36
    feat(audit): persist EDGE_FLOW_RANKED to funnel DB, pad SQL 10-13, funnel windows always 4 rows
    Show commit body
    - live_runner: insert trading_funnel_events heap rows after each EDGE_FLOW_RANKED (v1 + exec_only) with active_heap_plausible in detail JSON
    - migration: partial index on heap EDGE_FLOW_RANKED events
    - SQL 01: LEFT JOIN so 3m/10m/2h/24h always emitted
    - SQL 10-13: path_tape mix, skip vs admit by segment, heap stats from DB, trace spotcheck
    - export_audit_csv.sh for cohort CSV; README + report note for redeploy
    
    Made-with: Cursor
  780. 3515240
    docs: use full dominant_skip_reason strings in audit report
    Show commit body
    Made-with: Cursor
  781. 8c35c99
    docs: drop volatile commit hash from audit report header
    Show commit body
    Made-with: Cursor
  782. aac140e
    docs: align audit report HEAD hash
    Show commit body
    Made-with: Cursor
  783. ebfc835
    docs: simplify audit report run header
    Show commit body
    Made-with: Cursor
  784. 0367ad8
    docs: note report commit hash for server audit run
    Show commit body
    Made-with: Cursor
  785. 20bce56
    docs: fill EDGE_SURVIVAL_AUDIT_REPORT with server SQL + journal results
    Show commit body
    Made-with: Cursor
  786. 6655927
    fix(audit): restore defaultdict import; split 04 admitted query CTE scope
    Show commit body
    Made-with: Cursor
  787. 7376975
    Add edge survival audit SQL, journal log parser, and report template
    Show commit body
    - SQL 01-09: plausible cohort funnel, clusters, killers, admission economics,
      move_below_fees ticker proxy, capital/slot pressure, symbol killers,
      latency percentiles, signal-to-exec trace latency
    - scripts: parse_execution_logs.py (EDGE_FLOW_RANKED, SYMBOL_EXECUTION_LOCK),
      fetch_execution_journal_audit.sh
    - docs/reports/EDGE_SURVIVAL_AUDIT_REPORT.md: sections 1-8 methodology + STAP 4b matrix
    
    Made-with: Cursor
  788. ea9a59c
    fix(execution): ghost exposure lock — reconcile DB vs exchange before SYMBOL_EXECUTION_LOCK
    Show commit body
    Root cause: unprotected_exposure used krakenbot.positions.base_position alone; a stale
    non-zero row blocks submits while exchange balance + fills are flat.
    
    - balance_cache: sum_balance_all_keys_for_base (incl. zero legs) for exchange-flat detection
    - positions: get_position_meta, force_flat_position_after_desync
    - position_truth: pub fill_net_base_qty + has_recent_fills for pre-lock checks
    - exposure_ghost_lock: try_resolve_ghost_exposure_before_lock (fresh balances, dust,
      fill ledger agreement); clear_exit_only_when_position_flat; SYMBOL_* logs
    - check_symbol_execution_lock: open orders first, then ghost reconcile, then gates
    
    Short positions skip auto-flatten; recent fills skip reconcile.
    
    Made-with: Cursor
  789. f83dbe3
    fix(scripts): journalctl _PID must use --since, not -n tail (L3 noise)
    Show commit body
    Made-with: Cursor
  790. 8613fa5
    fix(execution): align live argv, systemd, and runtime role truth
    Show commit body
    - Remove EXECUTION_ONLY from live unit; load .env first then force EXECUTION_ONLY=0
    - Add run-execution-only CLI entry + run_execution_only() for split mode
    - run-execution-live always logs argv role run-execution-live; truth script enforces ROLE_MATCH
    - Add krakenbot-execution-only.service template for exec-only split
    - Extend check_execution_runtime_truth.sh: role_match, argv role, cmdline, repo unit guard
    
    Made-with: Cursor
  791. e62bfcd
    fix(ops): validated restart uses systemd when krakenbot-execution is enabled
    Show commit body
    - Auto-detect krakenbot-execution.service (enabled/active); systemctl stop, kill stragglers, systemctl restart
    - Wait for EXECUTION_RUNTIME_SELF_VERIFY_OK in journal for MainPID
    - KRAKENBOT_EXEC_RESTART_VIA and KRAKENBOT_EXEC_SYSTEMD_UNIT overrides
    - nohup mode still stops the unit first to avoid Restart=always duplicates
    
    Made-with: Cursor
  792. 2cf8c93
    fix(ops): absolute krakenbot path + /proc PID scan for execution-live singleton
    Show commit body
    - Add scripts/_execution_runtime_pids.sh (pids_run_execution_live, count)
    - start_live_validation_engine_server: exec REPO_ROOT/target/release/krakenbot
    - restart: kill/wait via pids_run_execution_live; fail if count != 1 after self-verify
    - truth check: same PID source; pgrep sanity uses target/release/krakenbot
    - Cursor rule: warn against dual supervisor (systemd + nohup)
    
    Made-with: Cursor
  793. 547d56f
    fix(ops): execution truth — strict cmdline match + journalctl fallback
    Show commit body
    - Avoid pgrep false positives; match run-execution-live via /proc cmdline regex
    - check: --journal-pid for systemd/socket stdout; list all release krakenbot PIDs
    - restart: wait loop recounts live PIDs; truth check prefers journalctl when available
    
    Made-with: Cursor
  794. c187a00
    feat(ops): self-verifying execution runtime + truth scripts + embed CLI
    Show commit body
    - build.rs: KRKBOT_EMBED_GIT_COMMIT, KRKBOT_BUILD_UNIX_SECS
    - execution_runtime_verify: EXECUTION_RUNTIME_* logs, worker atomics, 8s self-verify exit 1 on missing workers
    - Worker markers: FLOW_POLLER_STARTED, FLOW_RECYCLE_*, EDGE_BIAS_*, PRIVATE_WS_RECYCLE_HOOK_*, EXEC_ONLY_FLOW_LOOP_STARTED
    - CLI execution-runtime-embed-info; live_runner parity for exec-only
    - scripts/check_execution_runtime_truth.sh, restart_execution_live_validated.sh
    - Cursor rule execution-runtime-truth.mdc; CHANGELOG
    
    Made-with: Cursor
  795. 1b04ee3
    fix(execution): preload instruments before universe fetch in run-execution-live
    Show commit body
    Cold restart failed with empty instrument cache; align startup order with ingest_runner.
    Remove duplicate preload later in the same function to avoid a second instrument WS.
    
    Made-with: Cursor
  796. 7fc1707
    chore(repo): gitignore /reports/ for clean server deploy tree
    Show commit body
    Made-with: Cursor
  797. ea8502e
    chore(sql): order A–I report windows 15m, 60m, 4h
    Show commit body
    Made-with: Cursor
  798. ec39529
    feat(observability): bias realized-move backfill + A–I report SQL
    Show commit body
    - edge_bias_entry_capture: ingest ticker_samples lookup for +3m/+10m mids (bps vs entry_mid), 30s worker spawned from live and exec-only runners
    - sql/analysis/55_flow_capture_report_a_i.sql: 15m/60m/4h metrics A–I including sequential JSON, route/gate mix, bias coverage, realized backfill rates
    - flow_capture_metrics_report.sh runs 55 then legacy window blocks
    - private_ws_hub: recycle on trade/filled/canceled/expired/rejected exec_type
    - CHANGELOG: unreleased bullet updated
    
    Made-with: Cursor
  799. e8ec3ac
    feat(execution): WS flow recycle, edge bias capture, route near-tie, metrics SQL
    Show commit body
    - private_ws_hub: optional FlowSymbolRecycleHook after first executions snapshot; terminal exec reports schedule symbol recycle.
    - flow_poller: FlowRecycleTrigger + spawn_flow_recycle_worker; recycle_symbol_into_heap public; live + exec-only wire hub hook.
    - edge_bias_entry_capture table + insert on live_validation submit from runner; ExecutionIntent bias fields; flow_execution fills them.
    - route_selector_v2: near-tie score band with non-breakout family preference (ROUTE_FAMILY_NEAR_TIE_SCORE_DELTA).
    - sql/analysis/54_flow_capture_metrics_windows.sql + scripts/flow_capture_metrics_report.sh; CHANGELOG.
    
    Made-with: Cursor
  800. 95c3c38
    feat(execution): single flow-poller execution path and exec-only parity
    Show commit body
    - Remove duplicate inline submit loops from live and exec-only eval cycles; orders only via spawn_flow_poller + flow_execute_single_candidate.
    - spawn_flow_poller takes Arc<AtomicI64> correlation run_id (exec-only sets per epoch bind).
    - Exec-only: shared FlowHeapState, latest_v2, exec_allowed, readiness/engine atomics, EDGE_FLOW_RANKED, pipeline_config Arc<RwLock>.
    - CHANGELOG: document flow-poller-only and exec-only parity.
    
    Made-with: Cursor
  801. 4c8a666
    feat(execution): flow-ranked edges, dynamic slots, gate softening, route matrix
    Show commit body
    - Add edge_heap scoring (realism x edge / decay) and rank all Execute outcomes
    - CapitalAllocator: flow-driven total slots, MAX_CAPITAL_SLOTS_PER_SYMBOL (default 1)
    - conflict_lane: liquidity soft-pass in quality segment; tape min lowered by bucket
    - Route matrix: Pullback + PassiveSpreadCapture taker variants
    - candidate_decision_vectors: expiry, edge age, reentry_count + migration
    - ExecutionIntent + runner persist new analytics fields
    
    Made-with: Cursor
  802. 9c808ba
    feat(throughput): activate non-breakout routes and targeted quality unlocks
    Show commit body
    Unlock multi-route flow by adding non-breakout candidate variants and family-aware winner selection, while reducing liquidity/spread/tape overblocking only in high-quality segments.
    
    Made-with: Cursor
  803. fe1e905
    fix(harvest): unblock low-liquidity option and honor size policy env
    Show commit body
    Add a bounded conflict-lane low-liquidity option for controlled harvest widening and make risk policy read POSITION_SIZE_MIN_QUOTE/POSITION_SIZE_MAX_QUOTE so runtime sizing floors can be changed via env as intended.
    
    Made-with: Cursor
  804. e38900f
    feat(data-harvest): add bounded gate widening and fixed monitor query
    Show commit body
    Make conflict-lane spread constraints hard-capped at 60 bps and add env-tunable, bounded economics/confidence harvest knobs so borderline candidates can pass without disabling safety protections. Add a single DB-first monitor SQL used identically for pre/post harvest baseline comparison.
    
    Made-with: Cursor
  805. 0dbb303
    persist full candidate decision vectors for auditability
    Show commit body
    - Add candidate_decision_vectors SSOT table with staged decision snapshots and hard FK from trading_funnel_events.
    - Persist route_eval/path_validation vectors for all route candidates and conflict/admission/capital/execution_precheck vectors in pipeline/execution flows.
    - Extend route candidate payload with confidence/fallback/move-fee diagnostics and add reconstruction SQL for recent blocker examples.
    
    Made-with: Cursor
  806. b6016a0
    conflict lane: soft-admit taker with tunable penalties and funnel events
    Show commit body
    - Default maker-preferred: require_maker_entry env now defaults false; true restores hard taker block.
    - Pure taker passes lane with size_mult * taker penalty, extra net-edge floor in pipeline, soft-gate discount for logging/detail.
    - New env: CONFLICT_LANE_TAKER_SIZE_MULTIPLIER, TAKER_NET_EDGE_PENALTY_BPS, TAKER_SOFT_GATE_MULTIPLIER.
    - Funnel: conflict_lane_maker_allowed, conflict_lane_taker_soft_allowed, conflict_lane_rejected_other_reason; richer CONFLICT_LANE_EVAL log.
    - Add sql/analysis/62_conflict_lane_tuning_snapshot.sql for post-deploy counts.
    
    Made-with: Cursor
  807. cee5e0e
    feat(execution): optional conflict-lane admission gate for V2 pipeline
    Show commit body
    - Add CONFLICT_LANE_* env-driven filters: liquidity tier (mid+), spread band,
      min 24h trade_count, 15m forecast confidence, maker-priority entries
    - Relax thresholds when account taker fee is at/below configured threshold
    - Apply conservative size multiplier on pass; log CONFLICT_LANE_EVAL
    - Wire from Config into live + exec-only pipeline config (default off)
    
    Made-with: Cursor
  808. c3d19f5
    fix live-runner truth halts and funnel reject telemetry
    Show commit body
    Harden periodic entry halts to require healthy truth snapshots and add standardized funnel counters plus structured reject-map payloads for direct no-trade diagnostics. Update changelogs to document the runtime and observability behavior changes.
    
    Made-with: Cursor
  809. 029f43c
    docs(changelog): backfill website updates since March 20
    Show commit body
    Add missing changelog entries in existing hash-based style so website changelog reflects all recent repository updates without gaps.
    
    Made-with: Cursor
  810. ab9693f
    feat(observability): add derived per-market 15m forecast and soft-gate scoring
    Show commit body
    Introduce a conservative 15m forecast derived from route/path outputs, apply it as bounded ranking weight (never as hard block), persist per-symbol snapshots per evaluation, and expose the data in tier2_data_bundle.
    
    Made-with: Cursor
  811. 70d3a87
    fix(route-engine): reduce false-negative no-trade gating
    Show commit body
    Calibrate confidence and economics gates to admit clearly positive-edge candidates while keeping strict spread/confidence guardrails for safety.
    
    Made-with: Cursor
  812. cb52825
    fix(observability): make 15m pnl bucketing Postgres-safe
    Show commit body
    Replace non-portable date_trunc('15 minutes') with an hour+minute-floor expression so snapshot export works on the server Postgres version.
    
    Made-with: Cursor
  813. 6bb39e4
    fix(observability): align dashboard counts with factual windows
    Show commit body
    Use created_at-based order windows for 24h dashboard metrics and keep status feed counters/freshness on a single status run to avoid mixed-run inconsistencies.
    
    Made-with: Cursor
  814. 70bda28
    feat(execution): add hybrid_v1 ignition exit path with phase telemetry
    Show commit body
    Introduce a feature-flagged hybrid TP-first to overshoot-trailing exit path behind allowlist routing while keeping legacy trailing intact, and persist per-trade hybrid summary events for cohort analysis.
    
    Made-with: Cursor
  815. 218fa90
    fix(observability): include pending shadow trades in outcome counts
    Show commit body
    Keep unresolved shadow trades visible in run-level outcome summaries by removing the outcome-not-null filter and labeling null outcomes as pending.
    
    Made-with: Cursor
  816. d7c8616
    fix(execution): enforce message freshness eligibility and remove dead runtime paths
    Show commit body
    Make max_message_age_secs a hard execution gate with explicit eligibility_message_stale evidence in funnel events, and remove unused legacy code paths in execution/protection/reconcile modules to reduce critical-path ambiguity.
    
    Made-with: Cursor
  817. 827b9bd
    fix(execution): unblock raw backfill fill normalization path
    Show commit body
    Fix fills_ledger insert placeholder mismatch that broke process_fill, and keep raw backfill diagnostics so unmatched raw executions can now normalize into fills/positions idempotently.
    
    Made-with: Cursor
  818. 4740f81
    chore(execution): add context to raw backfill failures
    Show commit body
    Annotate raw execution backfill errors with per-event processing stage and IDs so runtime diagnostics can pinpoint the failing normalization step.
    
    Made-with: Cursor
  819. cf6653e
    fix(execution): make raw backfill synthetic order upsert robust
    Show commit body
    Replace dynamic SQL upsert for synthetic external orders with a deterministic insert-or-fetch path based on insert_order, preserving idempotency while avoiding runtime insert shape mismatch.
    
    Made-with: Cursor
  820. 601502a
    feat(execution): backfill unmatched raw executions into fills and positions
    Show commit body
    Add a background worker that scans unmatched parsed raw private executions, resolves orders or creates deterministic synthetic external orders, writes fills idempotently through the ledger, and marks raw events matched.
    
    Made-with: Cursor
  821. 6eb92cd
    feat(execution): persist all private executions as durable raw events
    Show commit body
    Add append-only raw_private_executions storage with idempotent event dedupe and parse-error capture, persist every executions-channel event before correlation, and mark matched events asynchronously when order correlation succeeds.
    
    Made-with: Cursor
  822. 4ba12d0
    fix(execution): recover timeout orders via ownOrders reconcile
    Show commit body
    Parse both status/order_status from ownOrders payloads and allow snapshot reconcile transitions from reconcile state so timeout-marked orders can recover to exchange-truth terminal states.
    
    Made-with: Cursor
  823. 53d32ce
    fix(ws): subscribe executions with snap_trades for fill recovery
    Show commit body
    Enable snap_trades on private executions subscriptions so reconnects can recover missed trade events and reconcile order status/fills after transient WS gaps.
    
    Made-with: Cursor
  824. f4599dc
    fix(execution): add single precheck retry for transient cache gaps
    Show commit body
    Retry missing-mid and price-cache precheck failures once after 400ms before skipping, while keeping fail-closed behavior and persisting retry diagnostics for direct funnel measurement.
    
    Made-with: Cursor
  825. 586a750
    fix(execution): gate periodic entry halt on healthy exchange truth
    Show commit body
    Only trigger periodic_unprotected_pending after healthy private snapshots and a reconcile-confirmed real exposure. Stage pending as unknown while truth is stale and short-circuit dust/phantom records out of pending.
    
    Made-with: Cursor
  826. be04a11
    fix(execution): align early mid-snapshot gate with tiered cache freshness
    Show commit body
    Use the same liquidity-tier cache age policy for the early BLOCK_ENTRY_WITHOUT_MID_SNAPSHOT check as the market precheck, removing the overly strict fixed 2s choke before order execution decisions.
    
    Made-with: Cursor
  827. 78e379f
    perf(ingest): lower epoch refresh to safe 120s cadence
    Show commit body
    Reduce default ingest universe refresh cadence to 120s and clamp effective interval to a safe 60-120s range so execution consistently gets fresh epochs while avoiding overly aggressive churn.
    
    Made-with: Cursor
  828. fc68ea1
    fix(ingest): clamp epoch refresh cadence below execution freshness window
    Show commit body
    Keep ingest epoch publication within execution's 300s bind window by capping effective ingest refresh to 240s and lowering the default universe refresh interval.
    
    Made-with: Cursor
  829. bd8b6ea
    fix(protection): fail-closed liveness and degrade reconcile constraints
    Show commit body
    Treat missing snapshots as stalled for reconnect recovery and avoid PayloadIncoherent deadlock on missing/invalid instrument constraints by using degraded cached/fallback constraints in reconcile.
    
    Made-with: Cursor
  830. 7910242
    fix(protection): add deadlock recovery loop for stale private WS snapshots
    Show commit body
    Prevent permanent entry_halt deadlocks by adding snapshot liveness watchdog transitions, reconnect/truth-refresh recovery hooks, and starvation detection in emergency protection retry logic.
    
    Made-with: Cursor
  831. eba9b50
    docs: protection deadlock-recovery Mermaid in EXIT_PATHS + DOC_INDEX
    Show commit body
    Made-with: Cursor
  832. efb53b2
    fix(exec-only): write ticker_samples with bound ingest run_id
    Show commit body
    Execution-only public ticker WS used run_id=0 for DB rows, while trade/L2
    live on the epoch ingest run — observability and freshest_active_run_id
    then mixed partitions. Resolve the same epoch as the eval loop (valid
    epoch, else exit-only fallback) and pass that run_id to run_ticker_ws;
    fall back to 0 only if no epoch exists within max_age.
    
    Made-with: Cursor
  833. cc46163
    feat(observability): diagnose PRICE_CACHE_STALE_SKIP (MISSING/STALE/SYMBOL_MISMATCH)
    Show commit body
    - Extend RAM price_cache snapshots with wall_updated_at for post-hoc timestamps.
    - Classify precheck failures via diagnose_stale_skip_for_precheck (canonical pair incl. XBT/BTC).
    - Persist structured JSON in trading_funnel_events.detail plus enriched reason and logs.
    - precheck_market_price_cache returns MarketOrderPrecheckSkip; no threshold or tape changes.
    
    Made-with: Cursor
  834. f2af13f
    fix(live_runner): run economic snapshots on execution-only path
    Show commit body
    Periodic exposure_nav + data_quality writes lived only in the combined
    run-execution-live loop; EXECUTION_ONLY=1 uses run_execution_only_loop
    and never called them. Factor write_periodic_economic_snapshots helper
    and invoke from both loops after periodic reconcile.
    
    Made-with: Cursor
  835. d47ed18
    fix(exposure): align exposure_nav_snapshots INSERT binds and harden observability
    Show commit body
    - Fix VALUES placeholder drift: use $1..$9 for nine bound columns (was $10 with nine binds).
    - Centralize SQL in insert_exposure_nav_snapshot_sql; add max_dollar_placeholder guard + anyhow context.
    - Log breakdown/insert failures at ERROR with operation/table; info on success with snapshot_id.
    - Alert after two failed periodic cycles (CRITICAL); warn when eval_count>=10 and snapshot missing.
    - Unit test for placeholder arity; optional ignored DB round-trip test.
    
    Made-with: Cursor
  836. e0ec180
    feat(observability): trace IDs, fill/PnL lifecycle, exposure & DQ snapshots
    Show commit body
    - Schema: strategy_run_id, position_id, correlation_id on orders; position_opened_at;
      realized_pnl entry/exit/position timestamps; fill fee_base + data_quality_violations;
      exposure_nav_snapshots + execution_data_quality_snapshots tables
    - Enforce decision_trace_id on inserts (emergency_stop exempt); stamp submit_ts/ack_ts;
      persist orderbook mid at entry when available
    - fills_ledger: slippage/mid on fill rows, fee/mid DATA_QUALITY warnings, realized_pnl
      links exit_order_id + position_closed_at vs entry first-fill open time
    - Runner: block missing trace; optional strict mid (default on); pre-DB 98% deployment cap
      and conservative global exposure when 24h PnL negative
    - Live: periodic exposure + data-quality snapshot rows and log thresholds
    - SQL analysis script for new tables
    
    Made-with: Cursor
  837. 87cb2de
    feat(observability): align L3 snapshot with epoch run + verification scripts
    Show commit body
    - Export l3_count/l3_symbol_count using epoch_run_id when present (consistent L3 % vs epoch_symbol_count)
    - Add optional epoch_run_id to public_status_snapshot
    - OBSERVABILITY_SNAPSHOT_CONTRACT + scripts for DB/runtime L3 checks
    - CHANGELOG
    
    Made-with: Cursor
  838. cf442b3
    chore(cursor): rule for website changelog + git deploy + live validation closeout
    Show commit body
    Made-with: Cursor
  839. 99df44f
    feat(orders): persist decision_trace_id for WS fill funnel correlation
    Show commit body
    Made-with: Cursor
  840. 714b207
    feat(funnel): decision_code + correlation on live_runner, protection, reconcile, truth
    Show commit body
    Made-with: Cursor
  841. dcd0d96
    feat(observability): Tier-2 data bundle + funnel correlation columns
    Show commit body
    - Migration: trading_funnel_events correlation_id, cl_ord_id, order_id, decision_code, detail JSONB + indexes
    - TradingFunnelEvent/insert_event extended; runner/ws_handler set trace + order linkage on key paths
    - Outcome.decision_trace_id + ExecutionIntent.decision_trace_id; pipeline generates UUID per candidate
    - Export tier2_data_bundle.json (ingest+decision aggregates, disclosure_policy NL)
    - shadow_blocker_reasons_for_run query; contract_version 1.1 + OBSERVABILITY_SNAPSHOT_CONTRACT §3.8
    
    Made-with: Cursor
  842. 5595af1
    docs+script: clarify decision DB for execution; truncate ingest mirror safely
    Show commit body
    Made-with: Cursor
  843. 8e949c3
    docs: path_tape_class DB rollups (SQL + operational notes)
    Show commit body
    Made-with: Cursor
  844. 119176f
    feat(obs): path_tape_class on funnel + orders (schema, writers, reads)
    Show commit body
    Made-with: Cursor
  845. 6bfdc8f
    docs: path-tape log queries + continuation tuning reference
    Show commit body
    Made-with: Cursor
  846. 12097f4
    feat(routes): continuation move/fee relax + score blend (guarded)
    Show commit body
    - move/fee ratio 0.76 vs 0.80 for Breakout/Pullback only when trade_density,
      l3_quality, spread band OK; net_edge>0 gate unchanged
    - time_adjusted_score: blend path confidence with continuation_prob for
      same routes + trailing exits (ranking only)
    - validate_candidate takes explicit threshold; tests for relaxed band
    
    Made-with: Cursor
  847. 025ac09
    feat(edge): tie-break spread-farming to trend on continuation tape
    Show commit body
    - When SF wins by <=0.10 over trend_score but trend_strength/direction/
      density/impulse show continuation, classify TrendContinuation so breakout/
      pullback matrix runs
    - Unit test for narrow-SF fixture
    - Confidence uses winner score after tie-break; payoff asymmetry follows final regime
    
    Made-with: Cursor
  848. 5e2a5ed
    fix(execution): persist resolved base qty for market orders (not notional-as-qty)
    Show commit body
    - plan_execution used notional USD as quantity_base placeholder for market orders
      → dashboard showed 20 for min $20 notional; exchange path was already correct
    - prepare_order_for_submit once before DB-first; store prep.order_qty on execution_orders,
      OrderTracker, ack path; submit_order_with_prep avoids double prepare
    - Document placeholder in execution_planner
    
    Made-with: Cursor
  849. 2145ec1
    fix(kraken): quantize OTO/trailing pct with fixed decimals
    Show commit body
    - trigger_price_type=pct is a percentage, not a quote price; using
      effective_price_decimal_places (pair tick) could round to 0–1 dp and
      corrupt trail distance → exchange rejects on some alts (e.g. SYND/USD)
    - Use 6 fixed fractional digits via quantize_wire_f64; add regression test
    
    Made-with: Cursor
  850. ef28db2
    fix(routes): move/fee ratio gate 0.85 -> 0.80 for spread-style economics
    Show commit body
    - Still requires net_edge > 0 after full cost stack (no negative-edge admits)
    - Targets dominant move_below_fees churn in spread_farming without touching
      liquidity/L3/safety gates; unit tests pin threshold boundary
    
    Made-with: Cursor
  851. 5d2e11a
    obs(edge): ADAPTIVE_TRADABILITY_SNAPSHOT per cycle
    Show commit body
    - tradable_rate_pct, pairs with no valid candidates vs valid but score<=0
    - aggregated top invalid dominant_reason counts (per invalid candidate)
    - complements SYMBOL_ROUTE_DECISION + ADAPTIVE_ANALYSIS_COMPLETE for before/after tuning
    
    Made-with: Cursor
  852. 48e12f5
    fix(kraken): wire price/pct quantization from instruments (max decimals)
    Show commit body
    - effective_price_decimal_places: min(price_precision, increment-derived dp)
      so pairs like BTR/USD (4 dp cap) are not overridden by finer increments.
    - auth_ws: quantize limit_price on add_order/add_order_with_conditional;
      quantize OTO conditional trigger_price + optional conditional limit_price;
      quantize stop-loss static trigger and trailing pct triggers; amend_order
      limit_price; amend_stop_loss_trigger + amend_trailing_stop_trigger_pct
      (new symbol arg for instrument lookup).
    - Call sites: exit_lifecycle, ignition_exit, position_monitor, kraken_adapter.
    - Test: effective_price_decimal_places_is_min_of_precision_and_increment.
    
    Made-with: Cursor
  853. 9343ff7
    reconcile: resolve Kraken-prefixed base balances; periodic dust closes DB
    Show commit body
    - Use balance_cache::resolved_balance_for_base_asset in position_reconcile
      and protection_flow (phantom/uncertain/insufficient-funds paths) so XX*/X*
      keys do not read as zero base.
    - run_periodic_reconcile: on dust classification, call close_dust_position
      like reconcile_exposure_at_startup so phantom dust rows do not linger.
    - Add unit test reconcile_resolves_prefixed_kraken_balance_key.
    
    Made-with: Cursor
  854. 81dc8a2
    feat(remediation): cl_ord_id UUID wire, balance+universe freshness, NL denylist
    Show commit body
    - Exit/protection/probe: generate_cl_ord_id for Kraken WS (no kbtp/kbprot)
    - balance_cache: last_feed_at + max snapshot/feed for reconcile age
    - EXECUTION_SYMBOL_DENYLIST env + universe_source filter
    - SQL 53 invalid price/rejects window; REJECTS_REMEDIATION_AUDIT + deploy doc
    - CHANGELOG unreleased bullets
    
    Made-with: Cursor
  855. 2b443ad
    fix(reconcile): own_orders freshness uses snapshot or delta time
    Show commit body
    - freshness_age_secs = elapsed since max(last_snapshot_at, last_update_at)
    - snapshot_age_secs delegates (same call sites)
    - tests + mutex for global cache isolation
    - CHANGELOG unreleased note
    
    Made-with: Cursor
  856. df13bb2
    fix(execution): quantize order price/qty for Kraken WS wire (instrument decimals)
    Show commit body
    - Add quantize_wire_f64 + limit/qty helpers from InstrumentConstraints
    - Apply after normalize_order in prepare_order_for_submit (IEEE754 JSON noise)
    - exit_lifecycle maker TP, protection market close, position_monitor TP market, ignition market exit
    
    Made-with: Cursor
  857. a0f3549
    feat(live): path_tape notional scale + doctrine/observability logs
    Show commit body
    - PATH_TAPE_EXIT_ROUTING: exit_doctrine, SL/TP/panic/hold, base_exit_strategy
    - PATH_TAPE_NOTIONAL_SCALE: allocator_notional, notional_scale, final_notional
    - Apply scale to intent + register_open (both main and exec-only loops)
    - IGNITION_EXIT_SUPPRESSED includes exit_doctrine
    - CAPITAL_ALLOCATION funnel after scale with path_tape_scale in reason
    
    Made-with: Cursor
  858. c38e1a7
    feat(path_tape): tighten MRH/spiky/thin/fading/illiquid doctrine + notional_scale
    Show commit body
    - Shorter holds for MRH/thin/illiquid; stricter SL/TP/panic per class
    - primary_exit_doctrine_label + notional_scale_for_class (no config defaults change)
    - Tests: notional scale, MRH static doctrine
    
    Made-with: Cursor
  859. 492518f
    fix(path_tape): suppress ignition exit for fading momentum doctrine
    Show commit body
    Made-with: Cursor
  860. adbf7a7
    feat(live): rolling trade_samples gate + path/tape exit routing
    Show commit body
    - tape_queries::count_trades_since (ingest DB rolling window)
    - path_tape: classify + exit_config_for_path_tape + ignition suppress
    - TAPE_* env: gate enable, window secs, min trades, soft classify floor
    - live_runner: block low-activity entries; PATH_TAPE_EXIT_ROUTING logs
    - exec-only loop: same gate + routing
    
    Made-with: Cursor
  861. ae8c3f8
    feat(exit): PrimaryExitStyle + static SL post-fill phase + degraded static routing
    Show commit body
    - ExitConfig.primary_exit_style (TrailingNative | StaticSlLimitTp)
    - run_post_fill_static_sl_phase: native stop-loss, optional maker TP, panic/time, BE amend
    - pub(crate) CancelOrFill + wait_for_cancel_or_fill for ignition_exit
    - apply_degraded_exit_policy sets StaticSlLimitTp
    
    Made-with: Cursor
  862. 7e7bc56
    docs: exit doctrine research, EXIT_PATHS validation, tape policy, Kraken WS ref, rolling trade SQL
    Show commit body
    Made-with: Cursor
  863. 11d6f2f
    fix(oto): omit cl_ord_id on conditional add_order (Kraken WS); tracker symbol bridge
    Show commit body
    Made-with: Cursor
  864. 5c48e75
    fix: deserialize Kraken executions 'reason' on rejects; persist in ORDER_REJECT + events
    Show commit body
    Made-with: Cursor
  865. c63e99c
    chore(rag): script om __pycache__/*.pyc te verwijderen (niet alleen negeren)
    Show commit body
    - clean_rag_python_artifacts.sh: laat .venv intact (runtime)
    - README: sectie artefacten opruimen
    
    Made-with: Cursor
  866. 0a67b3b
    chore(gitignore): ignore Python venv, __pycache__, egg-info (rag-backend)
    Show commit body
    Untracked .venv/ on server is expected; deps via requirements.txt + pip install.
    
    Made-with: Cursor
  867. 88738ce
    docs: liquidity/flow entry research 48h + analysis SQL
    Show commit body
    Made-with: Cursor
  868. 423d9c4
    feat: consistency watchdog + recovery (DB flags, PrivateWsHub reconnect)
    Show commit body
    Made-with: Cursor
  869. 620ea24
    fix(l2_feed): do not drop order book on checksum mismatch
    Show commit body
    Removing books on mismatch left the L2 sample loop with nothing to persist,
    so l2_snap_metrics stayed empty for the active run → l2_raw_feature_ready
    never passed and execution could not refresh state.
    
    Resubscribe path unchanged; CRC warning retained. No change to FEATURE_READY thresholds.
    
    Tests: cargo check; cargo test (104+ ok)
    Made-with: Cursor
  870. 5bae3c8
    fix(execution-only): feed price_cache via same USD ticker pool as live
    Show commit body
    Exec-only previously subscribed ticker only for open positions; flat account
    → empty WS subscribe → no price_cache updates → spurious PRICE_CACHE_STALE_SKIP
    at precheck (policy unchanged; wiring was wrong).
    
    - preload_all + fetch_usd_ws_symbols(execution_universe_limit) then add positions
    - remove duplicate preload_all block
    - docs: EXEC_ONLY_EPOCH_BINDING + CHANGELOG_ENGINE
    
    Validation: cargo check; cargo test (104+ tests ok)
    Made-with: Cursor
  871. 415b16a
    revert: restore LOW_LIQUIDITY price_cache max age 30s; doc exec-only epoch binding
    Show commit body
    - Undo 120s tier window (no safety loosening without agreement)
    - Exec-only: richer skip log (max_epoch_age_secs, preferred_lineage)
    - Add docs/EXEC_ONLY_EPOCH_BINDING.md (queries, causes, ops)
    
    Made-with: Cursor
  872. 1d1f98e
    fix(execution): low-liq price_cache window + honest execute_count
    Show commit body
    - LowLiquidity tier: max_price_cache_age 30s -> 120s (quiet tape no longer spurious stale skip)
    - submit_and_wait_for_execution_reports returns SubmitAndWaitOutcome; live_runner only increments execute_count when DB order persisted
    
    Made-with: Cursor
  873. f345c9c
    fix(pipeline): V2 liquidity gate taker-only when state row missing — restore maker path
    Show commit body
    Made-with: Cursor
  874. 1c93639
    fix(pipeline): V2 fail-closed when no CurrentRunMarketRow — root cause blind market entries
    Show commit body
    Made-with: Cursor
  875. b8a3fb6
    fix(execution): exchange prevalidation before DB-first — no status=error for sizing/instrument failures
    Show commit body
    Made-with: Cursor
  876. 684949d
    fix(ingest): preload instrument cache before fetch_usd_ws_symbols (run-ingest)
    Show commit body
    Made-with: Cursor
  877. 3aff30f
    fix(exposure): classify dust on notional + cached qty_min — stop false unprotected when constraints Err
    Show commit body
    Made-with: Cursor
  878. cc30e4e
    fix(execution): do not exit emergency protection loop on SLA hard — clears entry_halt when safe
    Show commit body
    Made-with: Cursor
  879. 87100d3
    docs+sql: reconcile submit vs fill, blocker order, log timeline grep
    Show commit body
    Made-with: Cursor
  880. 08c9314
    feat(liquidity): depth_near_mid from l2_snap_metrics + p10 gating
    Show commit body
    - stats_queries::l2_avg_depth_near_mid_by_run: AVG bid/ask top5 depth per symbol
    - CurrentRunMarketRow.depth_near_mid; analyze_run + analyze_run_from_state merge
    - classify(..., depth_near_mid, depth_p10_run): thin vs run p10 + HIGH tier depth downgrade
    - Outcome.liquidity_depth_near_mid; pipeline logs depth + p10
    
    Made-with: Cursor
  881. f91798b
    feat(execution): tiered price_cache gating + liquidity INELIGIBLE pipeline skip
    Show commit body
    - Add liquidity_exec_policy (HIGH/MID/LOW/INELIGIBLE) from trades/s, spread, L3 fill time
    - Pipeline: classify symbols, skip Execute when INELIGIBLE, log LIQUIDITY_EXEC_CLASSIFICATION*
    - ExecutionIntent carries tier; kraken_adapter uses tier max-age + book spread cap
    - runner: precheck before DB insert; PRICE_CACHE_STALE_SKIP / BOOK_SPREAD_UNSAFE_SKIP (no order error)
    - run-execution-once + live_runner pass tier from Outcome; exits use default loose max-age
    
    Made-with: Cursor
  882. 3767c6c
    rag-backend: DOCS_EMBEDDING_API_KEY fallback + systemd unit template
    Show commit body
    Made-with: Cursor
  883. 35f5daf
    chore(cleanup): finalize archived doc moves and record classification
    Show commit body
    Finalize the report/research archive migration by committing original-path removals, add a cleanup classification record for auditability, and align one remaining validation-metrics doc pointer with current SSOT docs.
    
    Made-with: Cursor
  884. 01ebbf8
    chore(cleanup): archive report docs and tighten current docs index
    Show commit body
    Move time-bound RAPPORT and research outputs into docs/archive, update references that consume those reports, and rewrite README/DOC_INDEX pointers so root documentation only highlights current authoritative documents.
    
    Made-with: Cursor
  885. c7807fb
    chore(cleanup): add rollback snapshot and fix stale doc references
    Show commit body
    Capture a pre-cleanup rollback checkpoint and update stale runtime/script/docs references so cleanup can proceed with link-consistent, low-risk documentation pointers.
    
    Made-with: Cursor
  886. c81830d
    feat(observability): add risk-adjusted tier2 pnl metrics and lifecycle tests
    Show commit body
    Export Sharpe-like, Sortino-like, and drawdown-duration metrics from 24h realized PnL buckets in Tier2 snapshots, update the observability contract, and add integration tests for critical execution order-state transitions.
    
    Made-with: Cursor
  887. 33be68c
    docs(compliance): add incident/retention policies and env template hardening
    Show commit body
    Add explicit compliance baseline documentation (incident response and data retention/privacy), link it from README and runbook, and provide a complete .env.example for safer onboarding without exposing secrets.
    
    Made-with: Cursor
  888. 064bc22
    feat(economics): persist pre-trade metrics and align drawdown to 24h
    Show commit body
    Carry pipeline pre-trade metrics into execution submit persistence (orders plus submitted event payload), switch drawdown guards to 24h realized PnL, and add best-effort trailing-stop slippage measurement using fill-time orderbook mid reference.
    
    Made-with: Cursor
  889. 5a4efe5
    feat(integrity): add L2 checksum resync and hub lag hard reset
    Show commit body
    Validate Kraken L2 checksums against local top-10 book state with automatic resubscribe on mismatch, and harden private hub message handling with degraded/recovered markers plus forced reconnect on severe lag accumulation.
    
    Made-with: Cursor
  890. 826c1f6
    feat(safety): add private WS dead-man switch and fail-closed sender handling
    Show commit body
    Arm and refresh Kraken cancel_all_orders_after on the shared private WS hub, add reconnect backoff with jitter, and replace hot-path unwraps in execution submission loops to prevent panic during sender/token unavailability.
    
    Made-with: Cursor
  891. 8ce74a6
    fix(ignition): cancel-first exit + balance-based qty + timeout bail
    Show commit body
    Rewrites ignition_exit cancel_protection_and_market_exit to mirror the
    cancel-first semantics already in exit_lifecycle (B2):
    
    1. Cancel protection FIRST, wait for cancel-or-fill via shared
       CancelOrFill/wait_for_cancel_or_fill (now pub(crate))
    2. Use balance_cache::asset_balance as exit qty source (not tracked
       protected_qty) — prevents partial exits and dust
    3. Handle PROTECTION_FILLED_BEFORE_CANCEL (no market needed)
    4. Handle zero-balance after cancel (protection likely filled)
    5. Handle cancel timeout with balance check
    6. ImmediateExit timeout now bails MARKET_FILL_TIMEOUT_EXPOSURE_RISK
       (was silently returning Ok)
    
    Eliminates: double-exit risk, partial-position exits, silent timeout
    acceptance on all three ignition exit paths.
    
    Made-with: Cursor
  892. a558f58
    fix(ignition): bail on market fill timeout in panic exit path
    Show commit body
    ignition_exit panic path discarded wait_for_market_fill result with
    let _ =, treating a timeout as success and leaving the position open.
    Now bails with MARKET_FILL_TIMEOUT_EXPOSURE_RISK, matching the
    identical fix already applied in exit_lifecycle.rs.
    
    Made-with: Cursor
  893. 1b39fce
    docs: sync 12 docs met herstelplan-leakage wijzigingen (A1–F3, D4, D5)
    Show commit body
    ENGINE_SSOT, ARCHITECTURE, EXIT_PATHS, LOGGING, DOC_INDEX, LIVE_RUNBOOK,
    UNIVERSE_AUDIT, RAPPORT_PINNED, EXIT_SYSTEM_INVENTORY, EXIT_ORCHESTRATION,
    EXIT_PROTECTION_RESEARCH, PLAN_ADDENDUM bijgewerkt met:
    - cancel-first exit semantiek (B2)
    - staleness guards op price_cache (C1/C2)
    - RecvResult channel close vs timeout (F2)
    - REST eliminatie uit runtime (D1/D5)
    - OTO trailing-stop (D4), deadline (D2), qty normalisatie (F3)
    - nieuwe log markers (A3, B3, B4, F1, F2, D4)
    - HERSTELPLAN_LEAKAGE.md toegevoegd aan DOC_INDEX
    
    Made-with: Cursor
  894. 64757a0
    docs(changelog): herstelplan A1→F3+D4/D5 in CHANGELOG.md en CHANGELOG_ENGINE.md
    Show commit body
    22 commits (werkstromen A–F + D4/D5) gedocumenteerd met commit hashes,
    subsystem, trading impact en runtime impact per item.
    
    Made-with: Cursor
  895. c7f9650
    refactor(startup): universe_source leest uit instrument WS cache, geen REST meer (D5)
    Show commit body
    fetch_usd_ws_symbols gebruikt nu instruments::cached_symbols_with_status()
    in plaats van REST AssetPairs. Nul REST calls in volledige runtime
    inclusief startup. De instrument cache wordt al door preload_all()
    gevuld voordat symbol discovery draait.
    
    Made-with: Cursor
  896. 65c726b
    feat(oto): OTO conditional trailing-stop op entry market orders (D4)
    Show commit body
    ConditionalParams struct toegevoegd aan messages.rs voor Kraken OTO.
    add_order_with_conditional method in auth_ws.rs voor entry+protection
    in één atomic exchange call.
    
    submit_order accepteert nu oto_trail_bps: bij market orders met
    trailing-stop config wordt automatisch een conditional trailing-stop
    bijgevoegd. De exchange creëert de protection order bij fill —
    elimineert 50-200ms onbeschermde exposure.
    
    run_post_fill_exit_phase detecteert OTO trailing-stop via
    discover_oto_trailing_stop en skipt handmatige placement. Bij
    niet-gevonden OTO: fallback naar bestaande manuele flow.
    
    Made-with: Cursor
  897. c1e1c7f
    feat(exit): normalize_exit_qty ceil-to-step dust tracking (F3)
    Show commit body
    Voegt normalize_exit_qty toe die exit-qty naar boven afrondt op step
    increment als balance dit toelaat, anders floor. Voorkomt dust
    accumulatie over trades.
    
    Alle exit market orders in cancel_protection_and_market_exit en
    ignition cancel_and_exit gebruiken nu normalize_exit_qty.
    
    Made-with: Cursor
  898. fae0c36
    feat(ws): RecvResult enum onderscheidt channel-close van timeout (F2)
    Show commit body
    recv_timeout retourneert nu RecvResult { Message, Timeout, ChannelClosed }
    in plaats van Option<PrivateV2Response>.
    
    Kritieke paden (wait_for_market_fill, wait_for_cancel_or_fill,
    monitoring loops) escaleren ChannelClosed als expliciete error in plaats
    van het stilzwijgend als timeout te behandelen.
    
    Voorkomt dat een WS reconnect-event ten onrechte als "fill timeout"
    wordt geïnterpreteerd, wat kon leiden tot onbeschermde posities.
    
    Made-with: Cursor
  899. 16e51dc
    fix(exit): time_stop_secs=0 clamp naar 60s met warning (F1)
    Show commit body
    time_stop_secs=0 werd stilzwijgend geclamped naar 1s, wat een instant
    market exit veroorzaakte. Nu clamp naar 60s met expliciete
    EXIT_TIME_STOP_MISCONFIGURED warning.
    
    Made-with: Cursor
  900. 4ca8fab
    perf(fills): CTE combineert position read+upsert+read in 1 round-trip (E3)
    Show commit body
    get_position_tx (voor) + upsert_position_tx + get_position_tx (na)
    gecombineerd in upsert_position_cte_tx: één CTE query die pos_before,
    avg_entry_before en pos_after retourneert.
    
    Reduceert van 3 DB round-trips naar 1 binnen de fill-transactie.
    Alle 12 fills_ledger tests passeren ongewijzigd.
    
    Made-with: Cursor
  901. 15e435a
    perf(ws): elimineer dubbele serde deserialisatie execution reports (E2)
    Show commit body
    ExecutionReports worden nu eenmalig geparsed in private_ws_hub en
    opgeslagen in parsed_reports veld op PrivateV2Response. De runner
    gebruikt de pre-geparsede reports in plaats van opnieuw
    from_value + item.clone() per report.
    
    Elimineert: 2x from_value, 1x data.as_array().clone(), N x item.clone()
    per executions message. Eén deserialisatie per WS message.
    
    Made-with: Cursor
  902. 84c09a2
    feat(fills): populate orderbook_mid bij fill via price_cache (E1)
    Show commit body
    orderbook_mid was hardcoded None — compute_slippage_bps retourneerde
    daardoor altijd None. Nu wordt price_cache::snapshot_fresh(2s) gebruikt
    om bid/ask mid-price te lezen bij elke fill-verwerking.
    
    Invariant: orderbook_mid is populated voor elke fill waar price_cache
    data beschikbaar is. slippage_bps in fills tabel is niet-null wanneer
    mid beschikbaar is.
    
    Made-with: Cursor
  903. 81885e4
    perf(runner): observability DB writes fire-and-forget via tokio::spawn (D3)
    Show commit body
    order_latency::upsert_decision_and_submit en de success-path
    trading_funnel_events::insert_event worden nu via tokio::spawn
    uitgevoerd — niet meer blocking op het submit critical path.
    
    Safety-kritieke writes blijven sequentieel:
    - state_machine::on_submitted (DB-first)
    - exposure_reconcile::total_open_notional (safety gate)
    - risk_gate::check_global_exposure (safety gate)
    
    Verwachte latency-winst: 2-15ms minder per order submit.
    
    Made-with: Cursor
  904. b76d0b6
    feat(orders): deadline parameter op alle market orders (D2)
    Show commit body
    add_order berekent automatisch deadline = now + 5s voor order_type
    "market". Kraken matcht de order niet na deze deadline — bescherming
    tegen latency-geïnduceerde negatieve slippage.
    
    EService:Deadline elapsed rejection wordt herkend in runner.rs met
    korte quiet-mode (30s) en expliciete ORDER_DEADLINE_ELAPSED log.
    
    Invariant: elke market order heeft een expliciete deadline. Geen
    market order kan matchen nadat de markt significant bewogen is.
    
    Made-with: Cursor
  905. b35388e
    fix(adapter): REST best_bid/ask vervangen door price_cache in hot path (D1)
    Show commit body
    Alle drie REST call-sites in kraken_adapter::submit_order vervangen
    door price_cache::snapshot_fresh(5s):
    - Market order sizing: notional→base qty conversie
    - cost_min validatie
    - Final notional check na normalisatie
    
    get_best_bid en get_best_ask in instruments.rs gemarkeerd als
    #[deprecated] — niet meer gebruikt in het execution hot path.
    
    Invariant: submit_order doet nul await calls naar externe HTTP
    endpoints. Alle pricing komt uit price_cache (WS-fed, staleness guard).
    
    Made-with: Cursor
  906. a99ab2f
    fix(exit): fill price fallback gebruikt mid-price met staleness guard (C2)
    Show commit body
    Vervangt adverse-biased fallback (bid voor longs, ask voor shorts)
    door neutrale mid-price (bid+ask)/2 met snapshot_fresh(3s) staleness
    guard.
    
    Was: bij degraded fill price werd snap.bid gebruikt voor longs
    (systematisch te laag) en snap.ask voor shorts (systematisch te hoog),
    wat SL/TP levels in de verkeerde richting verschoof.
    
    Nu: mid-price als neutrale fallback, met bail als price_cache stale
    (>3s) of leeg is.
    
    Invariant: fill price fallback gebruikt altijd mid-price, nooit de
    adverse spread-kant. Fallback is altijd vergezeld van staleness guard.
    
    Made-with: Cursor
  907. 2b89839
    feat(price_cache): staleness guard met last_price_fresh en snapshot_fresh (C1)
    Show commit body
    Voegt freshness-aware functies toe aan price_cache:
    - last_price_fresh(symbol, max_age) -> Option<f64>
    - snapshot_fresh(symbol, max_age) -> Option<PriceSnapshot>
    Retourneert None als data ouder is dan max_age.
    
    Call-sites vervangen in execution/protection paden:
    - exit_lifecycle monitoring loop: max_age 5s
    - exit_lifecycle est_exit berekening: max_age 5s
    - balance_cache equity berekening: max_age 30s
    
    Bestaande last_price() en snapshot() behouden voor non-critical paden.
    
    Invariant: geen execution/protection beslissing op data ouder dan
    de geconfigureerde max_age.
    
    Made-with: Cursor
  908. 18fd1a2
    fix(exit): classify_cancel_or_fill verifieert order_id in cancel response (B2-fix)
    Show commit body
    Bug: classify_cancel_or_fill behandelde ELKE succesvolle cancel_order
    method response als cancel van de protection order, zonder te checken
    welke order daadwerkelijk is gecanceld. Bij panic exit en TP maker
    timeout wordt een TP cancel gestuurd vlak voor
    cancel_protection_and_market_exit — als die TP cancel response in
    wait_for_cancel_or_fill arriveert, wordt het foutief geclassificeerd
    als protection cancel, waarna een market exit wordt geplaatst terwijl
    de trailing-stop nog live is (dubbele-exit scenario).
    
    Fix: verifieer result.order_id tegen protection_order_id in de
    cancel_order method response. Bij mismatch: return None (fall-through
    naar executions channel canceled event dat al correct filtert).
    
    Tests: 9 (was 7) — nieuw: cancel response met matching order_id,
    cancel response met TP order_id (assert None), cancel response zonder
    result (assert None).
    
    Made-with: Cursor
  909. a565504
    fix(ws): broadcast lag recovery met lagged flag en reconciliatie (B4)
    Show commit body
    1. BROADCAST_CAPACITY 512 -> 2048 in private_ws_hub.rs
    2. PrivateMsgSource: lagged flag + total_lagged counter bij
       RecvError::Lagged; escalatie naar PRIVATE_WS_HUB_RELIABILITY_DEGRADED
       bij >= 100 verloren berichten
    3. exit_lifecycle monitor loop: bij lagged flag -> resubscribe
       executions (snap_orders=true) voor reconciliatie, clear flag
    4. PrivateMsgSource enum -> struct met MsgSourceInner; constructors
       dedicated() en hub() op alle call sites
    
    Invariant: broadcast lag is altijd zichtbaar en leidt tot
    reconciliatie. Geen stille message loss zonder recovery.
    
    Made-with: Cursor
  910. 4ede196
    fix(exit): panic exit fill-timeout escaleert naar EXPOSURE_RISK bail (B3)
    Show commit body
    Panic exit gooit fill-resultaat niet meer weg. Bij market fill timeout:
    - warn met exit_reason=panic_pnl_market_timeout + EXIT_COMPLETED_NO_FILL
    - bail met MARKET_FILL_TIMEOUT_EXPOSURE_RISK (identiek aan time-stop)
    - geen stille positie-verlating meer bij liquiditeitscrisis
    
    Invariant: elke exit-flow met EXIT_COMPLETED heeft bevestigde fill
    OF escaleert met MARKET_FILL_TIMEOUT_EXPOSURE_RISK.
    
    Made-with: Cursor
  911. 0357d8b
    fix(exit): cancel-first met balance-verificatie voor spot-safe exit (B2)
    Show commit body
    Herschrijft cancel_protection_and_market_exit naar cancel-first flow:
    - Cancel trailing-stop EERST (beschermt tot cancel-ACK)
    - CancelOrFill enum + classify_cancel_or_fill helper voor event-routing
    - wait_for_cancel_or_fill async helper met 5s timeout
    - Bij cancel-ACK: market exit op actuele balance_cache balance
    - Bij fill voor cancel (PROTECTION_FILLED_BEFORE_CANCEL): skip market exit
    - Bij timeout + zero balance: assume protection filled, skip market exit
    - Bij timeout + balance > 0: market exit op actuele balance
    
    Invariant: nooit meer dan één market sell live per positie.
    Exit sizing altijd op actuele balance, niet gecachete qty.
    
    7 unit tests voor classify_cancel_or_fill (fill/filled/canceled/method
    response/wrong order_id/non-fill exec_type/unrelated message).
    
    Made-with: Cursor
  912. d1e8317
    fix(execution): exec_type/order_status naar strikte Kraken WS v2 semantiek (B1)
    Show commit body
    Alle exec_type en order_status matching in de execution-laag is nu
    strikt conform de officiële Kraken Spot WS v2 specificatie:
    
    - trade = individueel fill-event (partial of final), verwerk in ledger
    - filled = order volledig afgerond, markeer als completed
    - triggers.status = "triggered" = tussenstatus, apart gedetecteerd
    
    Verwijderd uit match-patronen:
    - "fill" (niet-officieel)
    - "partial_fill" (niet-officieel exec_type)
    - "done" (niet-officieel order_status)
    - "triggered" als exec_type (was foutief; triggers.status is correct)
    
    Twee pub(crate) helpers: is_fill_exec_type, is_order_completed_status.
    Alle vier consumer-bestanden (ws_handler, exit_lifecycle, ignition_exit,
    position_monitor) zijn consistent via dezelfde helpers.
    
    2 tests: exhaustive exec_type en order_status classificatie.
    
    Made-with: Cursor
  913. 667c7e0
    fix(ws_handler): fill price zero-guard voor ledger (A3)
    Show commit body
    compute_fill_price kan Decimal::ZERO retourneren als alle fallbacks
    falen. Een fill met prijs 0 corrumpeert positions en realized_pnl.
    
    Guard toegevoegd direct na compute_fill_price: als prijs 0 is, wordt
    de fill gelogd als FILL_PRICE_ZERO_REJECTED en teruggestuurd als
    HandleResult::Unknown voor reconciliatie. De ledger wordt niet geraakt.
    
    Made-with: Cursor
  914. 9abc22c
    fix(ledger): fee-aftrek in realized PnL berekening (A2)
    Show commit body
    realized_pnl_quote was gross (fees niet meegenomen). Bij Kraken taker
    fee ~26 bps round-trip accumuleert dit ~$2.60 per $1000 positie dat
    niet werd meegerekend, waardoor drawdown-guards te optimistisch stuurden.
    
    Nieuwe compute_realized_pnl functie: gross_pnl - proportionele fee.
    Bij sign-flips wordt de fee geprorateerd op closed_qty/fill_qty_base.
    
    5 tests toegevoegd: long close met/zonder fee, short close met fee,
    flip met prorated fee, None bij ontbrekende avg_entry.
    
    Made-with: Cursor
  915. e2d08e5
    fix(ledger): VWAP-corruptie bij positiereductie elimineren (A1)
    Show commit body
    De SQL CASE in upsert_position_tx blendde exit-prijzen in
    avg_entry_price_quote bij partial closes, wat cost-basis kunstmatig
    verlaagde en drawdown-guards te optimistisch maakte.
    
    Herschreven met vier expliciete, auditbare branches:
      1. close to zero          → NULL
      2. same-side add          → VWAP blend
      3. reduce without flip    → behoud bestaande avg_entry
      4. sign flip / fresh pos  → fill_price als nieuwe cost basis
    
    Pure Rust mirror-functie compute_new_avg_entry toegevoegd met 7 tests:
    long add, long reduce, long close, long flip, short add, short reduce,
    short flip.
    
    Fix pre-bestaande test-compilatiefout in protection_flow.rs (missing
    success field op PrivateV2Response initialisaties).
    
    Made-with: Cursor
  916. 7381ba1
    docs: volledig herstelplan economische leakage eliminatie
    Show commit body
    Kraken-geverifieerd plan met 6 werkstromen (A-F), 20 concrete fixes,
    implementatievolgorde, REST-eliminatieplan, eind-invarianten en bewijsplan.
    
    Bron: leakage audit op fills_ledger, exit_lifecycle, ws_handler,
    kraken_adapter, price_cache, private_msg_source. Alle exec_type/
    order_status waarden geverifieerd tegen live Kraken Spot WS v2 docs.
    
    Made-with: Cursor
  917. c2d485a
    docs: flow/liquidity research results from 2026-03-16 cutoff
    Show commit body
    Made-with: Cursor
  918. b576844
    fix(research): remove stale BATCH_SIZE echo
    Show commit body
    Made-with: Cursor
  919. e6719c9
    perf(research): single ingest query with bounded trade_samples slice
    Show commit body
    Made-with: Cursor
  920. c2eee8b
    perf(research): drop heavy 5m L2 window; spread from snapshot only
    Show commit body
    Made-with: Cursor
  921. 011f1b2
    fix(research): argv parsing for flow/liquidity scripts
    Show commit body
    Made-with: Cursor
  922. b3c729e
    chore(research): dual-DB flow/liquidity analysis from 2026-03-16 (script+report)
    Show commit body
    Made-with: Cursor
  923. 4b76d52
    chore(env): remove REWRITE_ENV/rewrite.env; SSOT /srv/krakenbot/.env
    Show commit body
    - trading_env: only KRAKENBOT_ENV_FILE or /srv/krakenbot/.env; rename to trading_env_load_config
    - Docs: SERVER_RUNTIME_ENV_AND_READINESS.md; delete old rewrite-titled doc
    - Scripts/systemd: no legacy hints; lifecycle proof uses server .env
    
    Made-with: Cursor
  924. 7d4e152
    chore(env): default to /srv/krakenbot/.env; rewrite.env legacy only
    Show commit body
    - trading_env: KRAKENBOT_ENV_FILE > REWRITE_ENV > /srv/krakenbot/.env
    - Remove default REWRITE_ENV exports from validation/capacity scripts
    - systemd: drop EnvironmentFile for /etc/trading/rewrite.env
    - Docs/runbooks: SSOT for KapitaalBot .env; note migration path
    
    Made-with: Cursor
  925. d8e3ac5
    feat(runtime): trading_env, check-execution-readiness, no half-green execution validation
    Show commit body
    - Add scripts/trading_env.sh: DB_* / DECISION_DB_* → URLs + assert distinct dual-DB
    - Add krakenbot check-execution-readiness + scripts/validate_execution_readiness.sh
    - validate_execution_on_server / validate_live_engine_server: require dual-DB + readiness before run (SKIP_RUN stays build-only, labeled)
    - start_live_validation_engine_server: trading_env + readiness preflight
    - run_l3_capacity_test, validate_epoch_contract_fase1, run_400_symbol_scaling_test: dual-DB gate
    - health_check_after_start: assert dual-DB + readiness
    - systemd: optional EnvironmentFile rewrite.env; README + LIVE_RUNBOOK + SERVER_RUNTIME_REWRITE_AND_READINESS.md
    - Log markers: EXECUTION_RUNTIME_DB_BOUND, EXECUTION_LIVE_RUNNER_START
    
    Made-with: Cursor
  926. be6c0d3
    fix(scripts): allow validate_live_engine_server SKIP_RUN without DECISION_DATABASE_URL
    Show commit body
    Made-with: Cursor
  927. 7f0887b
    feat: mandatory dual-DB, trailing ledger truth, CLI/script DB routing
    Show commit body
    - Require distinct DATABASE_URL + DECISION_DATABASE_URL; log sanitized DB identity
    - create_pools only; retention purges both; replay/compare without ingest-as-replay
    - CLI analysis: decision pool for safety/latency/exit-realism/invariants-v2 + explicit labels
    - Trailing-stop: trail bps validation on persist; ws_handler treats triggered as fill; no slippage from trail bps
    - Scripts: hard-fail or decision-only where execution stats; SEV0 requires both URLs
    - Docs: DB_AND_TRAILING_TRUTH_HARDENING.md matrices; track .env.example via gitignore exception
    
    Made-with: Cursor
  928. 51a4429
    feat(execution): trail_bps v1 — fee floor, horizon α, regime, cap
    Show commit body
    - Add trail_bps_v1 (deterministic formula + constants)
    - trail_atr: raw ATR15m bps fetch only (no clamp)
    - exit_lifecycle + ignition: v1 + taker_fee from runner tier
    - submit_and_wait_for_execution_reports: fee_provider for taker bps
    - protect_exposure: optional fee_provider; v1 when prefer_trailing (300s horizon)
    - Docs: TRAIL_BPS_V1.md, EXIT_PATHS update
    
    Made-with: Cursor
  929. 206d76c
    feat(execution): 15m ATR trail, 50bps fallback, mid-trade adoption
    Show commit body
    - Add trail_atr module (900s bars, 6h lookback, top-14 TR)
    - exit_lifecycle + ignition_exit: replace 5m SQL; adopt ATR when DB fills later
    - Partial-fill replace: sync awaiting_atr_replacement with ATR flag
    - protection_flow: prefer_trailing uses estimate_atr15m_trail_bps + DEFAULT 50 bps
    - Docs: EXIT_PATHS_AND_PROTECTION_RUNTIME aligned with implementation
    
    Made-with: Cursor
  930. e915683
    web: add lightweight GA4 consent bundle (nl/en/de/fr)
    Show commit body
    Made-with: Cursor
  931. 9ebe2c2
    fix(exit): trailing pct amend, gapless replace, monitor shorts + runbook
    Show commit body
    Made-with: Cursor
  932. 77a4d7b
    docs: exit paths and protection runtime behavior (read-only)
    Show commit body
    Made-with: Cursor
  933. f3f8198
    fix(exit-protection): enforce trailing-first protection with ATR-based distance and deterministic recovery
    Show commit body
    Switch protection to trailing-first semantics with 5m ATR-based trail sizing, harden add_order ACK correlation, add bounded emergency retry with market-close escalation, and extend reconnect/exec-only reconcile gates to prevent unprotected exposure windows.
    
    Made-with: Cursor
  934. 28d7ec4
    fix(exit-capability): degrade fill-price dependent exits to market-anchor fallback
    Show commit body
    Prevent SL/TSL exit phases from failing when fill_price is missing by anchoring from live price cache, and pass avg_entry only when valid in emergency fallback so protection remains non-fragile.
    
    Made-with: Cursor
  935. af8d640
    fix(safety): halt new entries while unprotected exposure remains pending
    Show commit body
    Prevent new entry submissions when periodic reconcile still reports unprotected exposure after an immediate protection attempt, and keep the emergency retry loop active until protection is confirmed.
    
    Made-with: Cursor
  936. 8a0899f
    fix(protection): enforce protect-on-balance invariant with expectancy-based exit choice
    Show commit body
    When reconcile is uncertain but exchange balance proves real exposure, force immediate protection submit instead of staying pending. Prefer trailing stop when expectancy is better (favorable excursion) while retaining stop-loss/market fallback for guaranteed coverage.
    
    Made-with: Cursor
  937. 8246ebd
    fix(reconcile): treat close-side open orders as protective reduce evidence
    Show commit body
    Normalize order_type variants and infer reduce coverage for close-side open orders even when reduce_only/order_type metadata is imperfect. This enforces the invariant that existing exchange exit orders prevent unnecessary protection stalls.
    
    Made-with: Cursor
  938. 48552ed
    fix(balance-cache): reject empty balance snapshots as freshness signals
    Show commit body
    Do not mark balance snapshots fresh when balances payload contains no assets, and require non-empty assets for has_snapshot. This prevents false-fresh zero-balance states that can trigger phantom flat corrections and block protection.
    
    Made-with: Cursor
  939. f94ba26
    fix(reconcile): relax snapshot freshness gate for protection path
    Show commit body
    Increase balance and own-orders snapshot freshness windows so protection does not stall in pending loops when snapshots are a few seconds old. This keeps submit gating safety while allowing timely protection under normal WS cadence.
    
    Made-with: Cursor
  940. 8445ef3
    fix(exposure): include live balance cache in startup and periodic reconcile
    Show commit body
    Merge live balance-derived positions into reconciliation before DB-only holdings so exchange-held symbols cannot be missed when persistence lags or is empty. This closes the gap where real balances (e.g. MIM) were invisible to protection orchestration.
    
    Made-with: Cursor
  941. 1ff99fd
    fix(exit): throttle and dedupe trailing-stop placement attempts
    Show commit body
    Add a backoff between TSL submit attempts and detect existing open trailing stops for the same entry before submitting again. This prevents duplicate trailing-stop send storms when lifecycle re-enters during delayed acknowledgements.
    
    Made-with: Cursor
  942. e4f9ffc
    fix(execution): verify protection via exchange reconcile before normal state
    Show commit body
    Use durable symbol reconcile as the source of truth for ProtectionAcked transitions. Keep symbols pending when success outcomes cannot yet be verified by exchange-side coverage to prevent false-safe state flips.
    
    Made-with: Cursor
  943. 632e63e
    fix(execution): enforce spot-exit safety and verified protection state
    Show commit body
    Remove reduce_only from spot market/exit submissions and require post-outcome verification before marking symbols as ProtectionAcked. Keep symbols pending until DB protection coverage confirms protected-or-flat state so unprotected exposure cannot be acknowledged as safe.
    
    Made-with: Cursor
  944. 93c09c7
    fix(execution): enforce protect-first recovery for unprotected exposure
    Show commit body
    Make protection bootstrap entry-independent by using degraded trailing-stop placement first and fall back to stop-loss/market paths when needed. Add hard execution gates via symbol safety state and strengthen emergency retry orchestration with explicit protection-state transitions and unprotected exposure SLO telemetry.
    
    Made-with: Cursor
  945. 96d1473
    feat(execution): position truth model + balance recovery wiring
    Show commit body
    - Migration: positions truth columns (fills_confirmed / execution_confirmed / balance_recovery)
    - position_truth: reconcile on load, coherence after fill, execution ACK note, degraded exit policy
    - exposure: reconcile before load_open_positions; remove duplicate balance repair; lock on degraded truth
    - ws_handler/fills_ledger/exit_lifecycle/ignition_exit: integrate truth + protect-first behavior
    
    Made-with: Cursor
  946. bcdfe65
    docs(changelog): fix garbled commit hash for aae7e60 entry
    Show commit body
    Made-with: Cursor
  947. aae7e60
    fix(execution): order_qty from exchange + balance-led long position repair
    Show commit body
    - Sync execution_orders.quantity_base upward on ACK and fills from report.order_qty
    - OrderTracker::bump_quantity_base for OOO / understated DB qty
    - balance_cache::resolved_balance_for_base_asset; repair long base_position when
      balances snapshot is fresh (exposure load_open_positions)
    - Funnel POSITION_BASE_REPAIRED_FROM_BALANCE; LOGGING.md note
    
    Fills remain audit trail; exchange order_qty + balances reduce missed-fill gaps.
    
    Made-with: Cursor
  948. 46854f5
    docs(changelog): SEV0 protection fixes (0a79e25–d08e9ba)
    Show commit body
    Root CHANGELOG + CHANGELOG_ENGINE: spot reduce_only, precancel on
    insufficient funds, remediation script bounds, prior protection/desync hardening.
    
    Made-with: Cursor
  949. d08e9ba
    fix(sev0): precancel exit-side orders on SL insufficient funds
    Show commit body
    When Kraken ACK-rejects emergency stop-loss with Insufficient funds, skip
    hard_block, cancel open ownOrders on the exit side, wait for snapshot refresh,
    and retry SL once before market fallback. Adds open_order_ids_on_side helper.
    
    Made-with: Cursor
  950. 7a99fa5
    fix(sev0): avoid spot reduce_only rejects for protective orders
    Show commit body
    Stop-loss and trailing-stop WS helpers now omit reduce_only to prevent Kraken spot ACK errors that were blocking live protection for open exposure.
    
    Made-with: Cursor
  951. cb98e09
    fix(sev0): make remediation check pack bounded and fast
    Show commit body
    Add statement timeouts and switch ingest raw-feed checks to fast approximate row metrics so dual-db remediation audits complete reliably during incidents.
    
    Made-with: Cursor
  952. 0a79e25
    fix(sev0): harden protection flow and dual-db remediation checks
    Show commit body
    Close protection gaps by acking market exit before SL cancel, auto-repair non-zero closed positions during exposure reconcile, and add dual-database-aware remediation/report scripts for reliable runtime audits.
    
    Made-with: Cursor
  953. ae2fab4
    docs(changelog): keep 697ba05/9d9c2ee only; drop meta changelog commit lines
    Show commit body
    Made-with: Cursor
  954. 8bbb33e
    docs(changelog): note e33d917 changelog-format commit
    Show commit body
    Made-with: Cursor
  955. e33d917
    docs(changelog): hash-prefixed entries for observability bundle (9d9c2ee, 697ba05)
    Show commit body
    Made-with: Cursor
  956. 697ba05
    docs(changelog): reference 9d9c2ee for observability/export commit
    Show commit body
    Made-with: Cursor
  957. 9d9c2ee
    feat(observability): 24h export window + fill context; fix fills ledger; ignition lease await; rag-backend; docs
    Show commit body
    - observability_queries: strategy_counts + recent orders/fills use 24h window,
      COALESCE(strategy, strategy_context), fills join execution_orders for regime/strategy
    - export/snapshots: cap 250, truncation flags, RecentPublicFillRow extra fields
    - ws_handler: persist fee_quote to fills; prefer Kraken trade_id for idempotency key
    - ignition_exit: await symbol_exit_lease upsert/clear
    - docs: EXIT_ORCHESTRATION_DESIGN_SPEC, INVESTIGATION_ENJ_UNPROTECTED, DOC_INDEX
    - rag-backend: pgvector FAQ RAG service (README, app, indexer, SQL init)
    - gitignore: deep_trade_autopsy_12h_output.txt
    
    Made-with: Cursor
  958. 9967a44
    chore(docs): drop IDE vendor naming; rename dev rules to DEVELOPMENT_RULES.md
    Show commit body
    Made-with: Cursor
  959. 417da01
    fix(execution): await symbol_exit_lease upsert/clear; TP maker lease outside pool guard
    Show commit body
    Made-with: Cursor
  960. 5217e10
    fix(deterministic_proof): fill ExecutionReport fields for WS fee/trade shape
    Show commit body
    Made-with: Cursor
  961. 872379f
    feat(execution): exit lease table, reduce_only, TSL overlap, TP bps guard
    Show commit body
    Made-with: Cursor
  962. 2616b16
    fix(observability): 24h counts use exchange fill time + order activity
    Show commit body
    - Fills: window and ordering on COALESCE(ts_exchange, ts_local)
    - Orders/regime/strategy/status 24h: GREATEST(created_at, updated_at)
    - Regime switches 1h: filter/order by activity time
    - RecentFillDbRow: fill_ts for export bucketing
    - Document contract semantics
    
    Made-with: Cursor
  963. f027874
    fix(observability): export uses ingest + decision pools for dual-DB
    Show commit body
    - run_export(DbPools): raw/run/market/epoch from ingest; execution/fills/safety/latency from decision
    - CLI export-observability-snapshots passes full DbPools
    - Document in OBSERVABILITY_EXPORT_SETUP
    
    Made-with: Cursor
  964. 9ef7110
    observability: export last 10 execution orders and fills to public_trading_snapshot
    Show commit body
    - SQL: recent_execution_orders, recent_fills (newest first)
    - PublicTradingSnapshot: recent_orders, recent_fills (15m bucketed ts, short order_ref)
    - Document contract update for Tier 1 summary tables
    
    Made-with: Cursor
  965. c277248
    changelog: TSL exit overhaul + stale reconcile (6040fe9..328ed8d)
    Show commit body
    Made-with: Cursor
  966. 328ed8d
    exit_lifecycle TSL: SL first, at breakeven cancel SL and place Kraken trailing-stop; no TP
    Show commit body
    Made-with: Cursor
  967. 0cda8cd
    ExitConfig use_trailing_stop; TSL time_stop_secs=900 use_maker_tp=false no TP
    Show commit body
    Made-with: Cursor
  968. fb49917
    Add add_trailing_stop_order in auth_ws (trailing-stop, pct trigger)
    Show commit body
    Made-with: Cursor
  969. 90bf2a8
    ATR-based sl_bps and trail_distance_bps=sl_bps in strategy_selector; vol_at_hold from ignition
    Show commit body
    Made-with: Cursor
  970. 1045e40
    Add rolling_vol_15m_bps to IgnitionMetrics and vol_at_hold_bps(max_hold_secs)
    Show commit body
    Made-with: Cursor
  971. 6040fe9
    Startup stale-order reconcile in live_runner (exec + full run)
    Show commit body
    Made-with: Cursor
  972. c0e45dd
    fix: add Kraken application-level ping keepalive to public WS feeds
    Show commit body
    WS protocol Ping/Pong alone is insufficient with tokio-tungstenite
    split streams. Add periodic {"method":"ping"} every 30s per Kraken
    WS v2 docs to keep connections alive and flush queued protocol pongs.
    
    Made-with: Cursor
  973. 9c99d61
    fix: three blocking issues — regime, ping/pong, ownOrders
    Show commit body
    1. Remove global CHAOS regime from system_live_ready gate.
       Per-pair regime already blocks individual chaotic symbols; the global
       mean over 629 symbols (most illiquid) was a broken systemic blocker.
    
    2. Respond to WS Ping with Pong in ticker, trade, and L2 feeds.
       After ws.split() the write half was never used, so Pong was never
       sent. Kraken timed out connections after ~60s.
    
    3. Remove invalid ownOrders subscribe on ws-auth (not a WS v2 channel).
       Feed own_orders_cache from executions channel with snap_orders=true,
       which is the v2 replacement per Kraken docs.
    
    Made-with: Cursor
  974. dab68ea
    fix: clear pending list after successful L3 subscribe retry
    Show commit body
    Minor: pending wasn't cleared after final retry where all symbols
    were acked, causing false L3_SUBSCRIBE_INCOMPLETE warning.
    
    Made-with: Cursor
  975. d22d24d
    fix: L3 single subscribe per connection + retry rate-limited symbols
    Show commit body
    Per Kraken WS v2 docs: one subscribe request per connection (up to
    200 symbols), rate counter 200/s standard. Rate-limited symbols get
    automatic retry with 2s backoff (max 5 retries). Connections spaced
    6s apart to let account-wide rate counter recover.
    
    No dataset reduction. All 638 symbols across 4 connections.
    
    Made-with: Cursor
  976. aaf10ad
    fix: L3 sequential connection startup + slower pacing for rate limits
    Show commit body
    Kraken L3 snapshot rate limit is account-wide, not per-connection.
    Previous parallel startup exhausted the rate budget immediately.
    
    Changes:
    - Connections started sequentially (wait for subscribe to complete)
    - 5s delay between connection setups
    - L3 subscribe batch: 25→10 symbols (reduce per-batch credit usage)
    - L3 batch delay: 200ms→1000ms (respect snapshot rate limit)
    - 200 symbols × 10/batch × 1s = 20s per connection, ~80s total setup
    
    Made-with: Cursor
  977. 2199990
    fix: L3 multi-connection for full symbol coverage (revert universe cap)
    Show commit body
    The previous approach wrongly capped L3 to 200 symbols. The correct
    fix: keep all symbols, split across multiple ws-l3 connections
    (max 200 per connection per Kraken docs). Each connection gets its own
    fresh auth token + paced batched subscribe. All events fan into a
    single processing loop.
    
    638 symbols → 4 connections × ≤200 symbols each → full L3 coverage.
    
    Made-with: Cursor
  978. 188a91e
    fix: L3 subscribe rate limiting + universe cap at 200 symbols
    Show commit body
    Root cause: L3 client subscribed 638 symbols in a single message,
    exceeding Kraken's 200-symbol-per-connection limit. Only ~44 symbols
    received data, causing frac_l3=0.365.
    
    Changes:
    - L3 client: paced batched subscribe (25 symbols/batch, 200ms delay)
      with ACK/error accounting per batch
    - Universe: hard default l3_limit=200 (Kraken documented max)
    - ingest_runner + live_runner: apply 200 cap when no env override set
    
    Made-with: Cursor
  979. e0f38b8
    fix: process book messages during subscribe drain loop
    Show commit body
    The subscribe phase drain loop consumed book snapshot messages from
    the WS stream without processing them into books/stats, causing
    symbols_with_snapshot=0 in the health log despite books being active.
    Now book messages are also processed during ACK draining.
    
    Made-with: Cursor
  980. b66fcac
    fix: L2 subscribe rate limiting + full subscribe observability
    Show commit body
    Root cause: L2 book feed sent 64 subscribe batches (638 symbols/10 per batch)
    in a tight loop without delay, exceeding Kraken WS rate limit. ~134 symbols
    (biased U-Z) never received snapshots, causing frac_l2=0.884 < 0.90 threshold,
    all epochs degraded, engine locked in ExitOnly.
    
    Changes:
    - Paced subscribe: 25 symbols/batch with 100ms inter-batch delay (26 msgs)
    - Subscribe ACK/error accounting: parse WsSubscribeResponse, log per-batch
    - Snapshot wait window (15s) + missing-snapshot repair with retry
    - Execution-universe-first validation: log exec L2 coverage, error if incomplete
    - Reconnect: full paced resubscribe, coverage re-measurement
    - Per-symbol observability: snapshot/delta/write tracking, 60s health summary
    
    Made-with: Cursor
  981. 420dca4
    phantom position auto-correction: exchange balance is truth
    Show commit body
    When fresh WS snapshots confirm exchange_base_total=0 and no open
    reduce orders exist, the reconcile layer now automatically corrects
    the DB position to flat (RECONCILED_FLAT) instead of halting forever.
    
    Changes:
    - position_reconcile: add CorrectedPhantomToFlat decision variant
    - reconcile_symbol_durable: auto-correct phantom via close_dust_position
    - protection_flow: handle CorrectedPhantomToFlat as dust (no exit)
    - position_monitor: treat CorrectedPhantomToFlat same as SkipNoRealPosition
    
    Made-with: Cursor
  982. 11dd384
    harden exit lifecycle status-event persistence
    Show commit body
    Made-with: Cursor
  983. 5d02191
    fix schema approx_row_estimate correlation alias
    Show commit body
    Made-with: Cursor
  984. db75658
    fix evidence query column qualification
    Show commit body
    Made-with: Cursor
  985. 6a65fd0
    db inventory source sanity check probe
    Show commit body
    Made-with: Cursor
  986. 44a4dd3
    fix bot activity probe: remove CTE scope
    Show commit body
    Made-with: Cursor
  987. cf36992
    fix bot activity probe CTE name
    Show commit body
    Made-with: Cursor
  988. ba730a5
    bot activity last 24h (execution_orders/fills/realized_pnl)
    Show commit body
    Made-with: Cursor
  989. ef2ea35
    duty cycle last24h on krakenbot ticker/trade samples
    Show commit body
    Made-with: Cursor
  990. 7b7ecec
    introspect ticker_samples columns
    Show commit body
    Made-with: Cursor
  991. 3d0b73c
    check ticker_samples presence 2026-03-18
    Show commit body
    Made-with: Cursor
  992. 7963538
    check trade_samples presence 2026-03-18
    Show commit body
    Made-with: Cursor
  993. 1c9079c
    introspect trade_samples columns
    Show commit body
    Made-with: Cursor
  994. f7c6319
    check DB has 2026-03-18 market/execution rows
    Show commit body
    Made-with: Cursor
  995. 8d681c7
    check execution fills/order timestamps last 24h
    Show commit body
    Made-with: Cursor
  996. 5275f27
    timestamp unit check v2 (ms vs seconds)
    Show commit body
    Made-with: Cursor
  997. dcf7f46
    timestamp unit check for ingest
    Show commit body
    Made-with: Cursor
  998. 38c6264
    data plane integrity probe v4
    Show commit body
    Made-with: Cursor
  999. e16028a
    add data plane integrity probe v3
    Show commit body
    Made-with: Cursor
  1000. 427d4fd
    fix duty cycle in data plane integrity v2
    Show commit body
    Made-with: Cursor
  1001. b528ecb
    add data plane integrity probe last 24h
    Show commit body
    Made-with: Cursor
  1002. 1f2740a
    introspect public.trades columns
    Show commit body
    Made-with: Cursor
  1003. 5fbba0c
    list potential trade tables
    Show commit body
    Made-with: Cursor
  1004. 8292676
    fix v2 multi-run window anchored to latest data
    Show commit body
    Made-with: Cursor
  1005. 01252f4
    fix v2 multi-run median calc
    Show commit body
    Made-with: Cursor
  1006. 496305d
    fix v2 data_period FROM clause
    Show commit body
    Made-with: Cursor
  1007. 7653a56
    v2 multi-run blocker context validation
    Show commit body
    Made-with: Cursor
  1008. 936d0d2
    add multi-run context validation probe
    Show commit body
    Made-with: Cursor
  1009. 9c4de56
    add confidence/recommended to join coverage probe
    Show commit body
    Made-with: Cursor
  1010. d0a2d98
    fix order-join probe: count per candidate id
    Show commit body
    Made-with: Cursor
  1011. d6e197d
    feat(alerts): pushover on ingest degraded + snapshot insert failures
    Show commit body
    Made-with: Cursor
  1012. 87388e2
    probe v2: broaden exit-regime search
    Show commit body
    Made-with: Cursor
  1013. ea248f7
    fix probe 35: single union query
    Show commit body
    Made-with: Cursor
  1014. 0fc8370
    probe exit_regime_not_allowed string in candidates json
    Show commit body
    Made-with: Cursor
  1015. 078b65e
    feat(alerts): add pushover +/-3% realized PnL triggers
    Show commit body
    Made-with: Cursor
  1016. d7a2d7e
    fix exit-regime probe example query
    Show commit body
    Made-with: Cursor
  1017. f2fee69
    feat(alerts): send pushover on data_stale/readiness/hard-blocked
    Show commit body
    Made-with: Cursor
  1018. 85398ab
    add exit-regime and order-join probes
    Show commit body
    Made-with: Cursor
  1019. f88dbc4
    introspect trading_funnel_events columns
    Show commit body
    Made-with: Cursor
  1020. e107b4c
    fix 04: replace fill_probability with fill_flag proxy
    Show commit body
    Made-with: Cursor
  1021. 5d5f00b
    feat(alerts): add Pushover client and config
    Show commit body
    Made-with: Cursor
  1022. a8e5255
    fix spread/edge analyses: use public.fee_tiers
    Show commit body
    Made-with: Cursor
  1023. 4db0cb0
    fix fees join to public.fee_tiers
    Show commit body
    Made-with: Cursor
  1024. 3a4e909
    introspect public.fee_tiers columns
    Show commit body
    Made-with: Cursor
  1025. 9dad3e2
    list fee tables
    Show commit body
    Made-with: Cursor
  1026. c1aeea4
    rewrite candidate_decision_chain with real sources
    Show commit body
    Made-with: Cursor
  1027. ace49fd
    introspect market snapshot columns
    Show commit body
    Made-with: Cursor
  1028. cdc6a59
    list price/outcome tables
    Show commit body
    Made-with: Cursor
  1029. d904c07
    probe overlap orders with fills vs candidates
    Show commit body
    Made-with: Cursor
  1030. f4152ed
    fix probe semicolon
    Show commit body
    Made-with: Cursor
  1031. 995d606
    fix probe: add FROM joined
    Show commit body
    Made-with: Cursor
  1032. b8b378d
    probe candidate->order->fills join on eval+symbol
    Show commit body
    Made-with: Cursor
  1033. e892689
    find latest run with candidates and fills
    Show commit body
    Made-with: Cursor
  1034. 1b75ff5
    count rows for run_id=57
    Show commit body
    Made-with: Cursor
  1035. 18329dd
    join coverage probe run_id=57
    Show commit body
    Made-with: Cursor
  1036. 911ba12
    find latest run with fills
    Show commit body
    Made-with: Cursor
  1037. f1c685c
    check fills join keys
    Show commit body
    Made-with: Cursor
  1038. 121603c
    fix join probe: align run_id
    Show commit body
    Made-with: Cursor
  1039. 51b763e
    probe candidate->order join keys
    Show commit body
    Made-with: Cursor
  1040. c750e37
    fix join probe: remove taker_fee
    Show commit body
    Made-with: Cursor
  1041. ab9d16f
    probe join coverage candidate->order->fills->pnl
    Show commit body
    Made-with: Cursor
  1042. 8f4a1ee
    probe shadow_trades exit_reason
    Show commit body
    Made-with: Cursor
  1043. 62f47c4
    introspect shadow_trades columns
    Show commit body
    Made-with: Cursor
  1044. d769080
    introspect shadow_exit_analysis columns
    Show commit body
    Made-with: Cursor
  1045. 4cc8649
    probe runtime_notrade_state samples
    Show commit body
    Made-with: Cursor
  1046. 4dfe6da
    probe runtime_market_data_state non-numeric run_id
    Show commit body
    Made-with: Cursor
  1047. 95e3419
    fix runtime_market_data_state probe cast
    Show commit body
    Made-with: Cursor
  1048. 232090f
    fix runtime_market_data_state probe regex
    Show commit body
    Made-with: Cursor
  1049. 43b6e1d
    probe runtime_market_data_state dominant reasons
    Show commit body
    Made-with: Cursor
  1050. 4004c19
    probe execute candidate exit plan json
    Show commit body
    Made-with: Cursor
  1051. b142df5
    probe runtime_notrade_events details_json
    Show commit body
    Made-with: Cursor
  1052. 46bf1e8
    add extra introspection for readiness sources
    Show commit body
    Made-with: Cursor
  1053. 565eb9d
    probe candidate rejection reasons
    Show commit body
    Made-with: Cursor
  1054. 9d696c6
    add DB source introspection SQL
    Show commit body
    Made-with: Cursor
  1055. 4a6cd0c
    add DB tuning analysis pipeline (SQL + runner + docs)
    Show commit body
    Made-with: Cursor
  1056. 34669fc
    docs(changelog): record Phase A observability snapshots commit
    Show commit body
    Made-with: Cursor
  1057. 0143d40
    feat(observability): complete Tier2/Admin read-model snapshot exports
    Show commit body
    Made-with: Cursor
  1058. 6398d72
    refine invariant B classification and wire v2 safety report
    Show commit body
    Made-with: Cursor
  1059. 3203f79
    Add safety invariants v2 read-only analysis and CLI report
    Show commit body
    Introduce InvariantKind/InvariantStatus classification SSOT in safety_invariants_v2 and a CLI report to render per-symbol A/B invariant statuses without changing runtime behavior.
    
    Made-with: Cursor
  1060. a158ede
    Add low-cardinality observability for reconcile, remediation, and ACK
    Show commit body
    Log RECONCILE_DECISION, REMEDIATION_* events, and ACK_* lifecycle around the new authority/remediation layer without changing correctness.
    
    Made-with: Cursor
  1061. 7634745
    Cover reserved-funds replacement ACK timeout path in tests
    Show commit body
    Add a dedicated test that simulates replacement ACK timeout, asserting evidence write, symbol hard block, and error outcome.
    
    Made-with: Cursor
  1062. 3172823
    Add tests for reserved-funds replacement ACK success/failure
    Show commit body
    Introduce minimal ACK source and hard-block hooks to unit-test cancel→update→submit→ACK behavior. Prove replacement happens in the same remediation cycle and that ACK failure triggers evidence + hard block.
    
    Made-with: Cursor
  1063. 3d3df36
    Relax readiness log test for blocked path
    Show commit body
    Allow blocked readiness tests to pass when only READINESS_GATE_DECISION is logged, keeping assertions stable across environments.
    
    Made-with: Cursor
  1064. 9564b11
    Route all exit/amend paths through reserved-funds remediation
    Show commit body
    Unify reserved-funds cancel→ownOrders update→re-reconcile gating across protection, position monitor, and ignition exit. Add tests for remediation sequencing and uncertainty hard-block semantics, and document the reconcile/remediation authority.
    
    Made-with: Cursor
  1065. 9855097
    feat(execution): remediate reserved-funds conflicts via cancel+reconcile
    Show commit body
    Infer reduce classification when reduce_only is missing, add ownOrders update sequencing, and perform deterministic cancel cleanup for duplicate/conflicting orders before retrying protection submits.
    
    Made-with: Cursor
  1066. e23f09f
    chore(git): ignore observability_export artifacts
    Show commit body
    Prevent generated observability exports from dirtying the working tree on local and server checkouts.
    
    Made-with: Cursor
  1067. 8b8a829
    feat(execution): gate exits/protection on fresh balances+ownOrders
    Show commit body
    Introduce a central position/order reconcile choke point with safety-first freshness rules (balances/ownOrders <= 5s) and fail-closed HALT decisions. Wire all exit/protection submit paths to respect reconcile, allowing only cancel-only cleanup without fresh snapshots.
    
    Made-with: Cursor
  1068. 88cdeb4
    feat(exchange): add WS ownOrders inventory cache
    Show commit body
    Subscribe to private WS v2 ownOrders on the shared hub and maintain an in-memory open-orders snapshot for reconcile.
    
    Made-with: Cursor
  1069. 20eaaf1
    fix(execution): bound execution-only ticker bootstrap to open positions
    Show commit body
    Limit the execution-only ticker WS subscription set to symbols with open positions so price_cache warms quickly for emergency protection without subscribing to the full USD universe.
    
    Made-with: Cursor
  1070. 3a56086
    fix(execution): retry emergency protection with SLA and market escalation
    Show commit body
    Add a background emergency retry loop with warn/escalate/hard SLA thresholds, and introduce a market-close escalation path to avoid leaving exposure pending when price_cache prerequisites do not arrive in time.
    
    Made-with: Cursor
  1071. 95a252d
    fix(execution): add global entry halt when emergency protection pending
    Show commit body
    Introduce a fail-closed entry_halt gate in live runner loops and set it at startup when unprotected exposure cannot be immediately protected (Pending/HardFail/CriticalInvariantBreach).
    
    Made-with: Cursor
  1072. b89e6b9
    fix(execution): classify protection prereqs and insufficient-funds breaches
    Show commit body
    Introduce Pending/HardFail/CriticalInvariantBreach outcomes for emergency protection, fail-closed when the private WS hub is not ready, and classify Insufficient funds into explicit invariant breach types.
    
    Made-with: Cursor
  1073. 41b76fa
    fix(exchange): add disconnect-safe readiness for private WS hub
    Show commit body
    Expose is_ready()/wait_ready() and a generation counter so execution paths can fail-closed when the shared ws-auth sender is not yet usable or has disconnected.
    
    Made-with: Cursor
  1074. 89bb7b8
    fix(execution): correct closed_qty on position sign flips
    Show commit body
    Cap realized close quantity at the pre-fill position when a fill crosses through zero, and decode nullable parent_order_id as Option<i64> to prevent NULL decode crashes during fill processing.
    
    Made-with: Cursor
  1075. fd3733a
    fix(cli): run report-safety-invariants on decision DB
    Show commit body
    The invariants audit reads execution tables (positions/execution_orders), so dispatch it via the decision pool instead of ingest.
    
    Made-with: Cursor
  1076. 44e5328
    fix(cli): expose report-safety-invariants mode
    Show commit body
    Include report-safety-invariants in cli_mode detection so the audit report runs without starting other long-running modes.
    
    Made-with: Cursor
  1077. 911dcc8
    feat(analysis): add hard safety invariants audit report
    Show commit body
    Add `krakenbot report-safety-invariants [hours]` to correlate DB exposure/exit coverage with journalctl evidence and print Invariant A/B breach rows including min_order_size from the instrument cache.
    
    Made-with: Cursor
  1078. 2e59baa
    fix(execution): preserve partial fills and correct realized pnl
    Show commit body
    Use a deterministic per-fill idempotency key (not exchange order_id) so partial fills are not dropped, treat any order with parent_order_id as an exit order, and persist realized PnL only on position reductions (linked to the root entry order).
    
    Made-with: Cursor
  1079. 11b7152
    diag: log exchange_balances persist success
    Show commit body
    Logs EXCHANGE_BALANCES_PERSIST_OK with row count and ENJ presence so we can
    prove DB-first holdings are being written from the balances snapshot.
    
    Made-with: Cursor
  1080. f140b2d
    diag: log private WS subscribe acks and balances snapshots
    Show commit body
    - Log subscribe ACK/result/error for balances + executions
    - Log first balances snapshot (asset count + ENJ presence)
    - Log first executions batch size
    
    Purpose: prove why balances snapshot is missing and enable DB-first holdings.
    Made-with: Cursor
  1081. 942bb12
    fix: do not drop early balances/executions snapshots on subscribe
    Show commit body
    The private WS hub previously drained two messages after subscribe, which could
    silently discard the initial balances snapshot. We now drain for up to 3s while
    still processing/broadcasting balances and executions messages.
    
    Made-with: Cursor
  1082. 4501f03
    fix: persist exchange_balances from balance_cache
    Show commit body
    Use the balance_cache (already fed by the shared private WS hub) as the
    source for writing krakenbot.exchange_balances. This avoids relying on
    broadcast message delivery timing and keeps holdings DB-first.
    
    Made-with: Cursor
  1083. 06012e8
    feat: persist exchange balances for DB-first exposure discovery
    Show commit body
    - Add krakenbot.exchange_balances table (additive migration)
    - Persist private WS balances snapshots into DB via existing PrivateWsHub (no new WS)
    - Use exchange_balances as primary holdings source in exposure_reconcile; executions remains supplemental
    
    Made-with: Cursor
  1084. 39b19ab
    feat: add Kraken WS v2 implementation checklist rule
    Show commit body
    Made-with: Cursor
  1085. 07d9243
    feat: infer holdings from executions channel
    Show commit body
    - Add execution_holdings_cache (net per symbol) updated from private WS executions
    - Use executions-derived holdings for exposure reconciliation (no balances required)
    - Feed holdings cache in private_ws_hub
    
    Made-with: Cursor
  1086. 9772946
    chore: log ENJ balance sync skips
    Show commit body
    Adds targeted diagnostic log when an ENJ-like balance asset cannot be mapped
    into a tradable symbol during balance-driven reconcile.
    
    Made-with: Cursor
  1087. 0206e76
    fix: allow emergency protection without ticker price cache
    Show commit body
    - When avg_entry_price is known, compute stop from entry only (no price_cache needed)
    - For balance-discovered positions, recover avg_entry_price from positions table when available
    
    This enables protecting manual/external holdings (e.g. ENJ) even in execution-only mode.
    
    Made-with: Cursor
  1088. cd9b14a
    fix: protect manual positions without REST
    Show commit body
    - Revert REST usage in protection flow (strictly forbidden)
    - Normalize balance asset codes (X*/Z*/XX*/XZ*) to AAA/USD symbols for balance-driven reconcile
    - Delay startup protection briefly so price_cache has snapshots before computing stops
    
    Made-with: Cursor
  1089. 996ad39
    fix: autopsy script use DECISION_DATABASE_URL when set
    Show commit body
    Execution data (orders, fills, positions) lives in DECISION DB when
    physical separation is configured; ingest DB has no execution data.
    
    Made-with: Cursor
  1090. f128b05
    fix: do not close positions from stale balance check
    Show commit body
    The total_open_notional stale check (balance < 1% of DB position) was
    calling close_dust_position, which incorrectly zeroed open positions
    when balance_cache was stale (race with fill, or asset not in snapshot).
    
    Root cause of ENJ showing base_position=0 in DB while 349 ENJ on Kraken:
    fill processed -> position 244.72; balance_cache not yet updated ->
    stale check triggered -> close_dust_position -> position zeroed.
    
    Fix: only exclude from exposure total, do not mutate positions table.
    Made-with: Cursor
  1091. b17709c
    feat: balance-driven position discovery — protect manual/external positions
    Show commit body
    - Add load_positions_from_balance() to discover positions on Kraken not in DB
    - Merge balance positions into reconcile_exposure_at_startup and run_periodic_reconcile
    - Extend check_symbol_execution_lock to block entry when balance > 0 and no DB position
    - Fixes unprotected positions from manual trades (e.g. ENJ)
    
    Made-with: Cursor
  1092. 94b9883
    feat: add Deep Trade Autopsy script (12h live execution analysis)
    Show commit body
    Made-with: Cursor
  1093. 5b523fa
    fix: handle Kraken exec_type filled/partial_fill in ws_handler (was falling through to UNHANDLED)
    Show commit body
    Made-with: Cursor
  1094. df50ed8
    docs: EXIT_SYSTEM_INVENTORY + exit strategy debug/diagnose scripts
    Show commit body
    Made-with: Cursor
  1095. fa5b54c
    scripts: add exit_strategy_inference.sql for SL-based strategy counts
    Show commit body
    Made-with: Cursor
  1096. aa5f9f9
    changelog: add entry for 7e9ca71 (doc sync)
    Show commit body
    Made-with: Cursor
  1097. 7e9ca71
    docs: alle leidende docs up-to-date (exit, compounding, markers)
    Show commit body
    - CHANGELOG_ENGINE: exit runtime, sectie 2025-03-17
    - DOC_CONSISTENCY_REPORT: exit, portfolio, compounding sync met SSOT
    - LIVE_RUNBOOK: exit lifecycle markers, order→exit flow
    - LOGGING: EXIT_PLAN_CREATED, EXIT_ORDER_ACKED, POSITION_MONITOR_*
    
    Made-with: Cursor
  1098. 35071f9
    docs: ENGINE_SSOT + ARCHITECTURE — exit, compounding, capital up-to-date
    Show commit body
    - Exit: post-fill exit_lifecycle (SL+TP) + position_monitor (trail, TP) in live runner
    - Compounding: capital_allocator.update_equity(live_eq) per evaluation
    - Portfolio allocation: live equity; allocated_quote nog niet uit positions
    - Persistent ingest, execution attach: server bewezen
    
    Made-with: Cursor
  1099. 5b48b3f
    changelog: add entry for 9bd11b1 (L2/L3 epoch validity)
    Show commit body
    Made-with: Cursor
  1100. 9bd11b1
    fix: L2 cold-start + L3 partial (70%) for epoch validity
    Show commit body
    - L2 cold-start: criteria_l2_ok=true when >50% l2=0 (warmup)
    - L3 partial: criteria_l3_ok=true when frac_l3>=70% (Kraken rate limits)
    - EPOCH_VALIDITY_COMPUTED: add l2_cold_start, frac_l2 to log
    
    Made-with: Cursor
  1101. 6738335
    changelog: add entries for 967d1c6, 97cf3d3
    Show commit body
    Made-with: Cursor
  1102. 967d1c6
    fix: L3 cold-start relaxation + shadow persistence for engine_mode_blocked
    Show commit body
    - ingest_epoch: criteria_l3_ok=true when >50% l3_count=0 (systemic cold start)
    - ingest_epoch: EPOCH_VALIDITY_COMPUTED logging (ok_*, frac_*, l3_cold_start)
    - strategy_pipeline: insert_shadow_trades_engine_mode_blocked()
    - docs/BOTTLENECK_FIXES_2025-03-17.md
    
    Made-with: Cursor
  1103. 97cf3d3
    changelog: add entry for 764eebe (restart doctrine)
    Show commit body
    Made-with: Cursor
  1104. 764eebe
    feat(restart): unbounded execution + lineage_break NoNewEntries + RESTART_DOCTRINE
    Show commit body
    - LIVE_VALIDATION_RUNTIME_MINUTES=0: unbounded loop (100y deadline)
    - lineage_break: NoNewEntries i.p.v. ExitOnly (1 cycle milder)
    - LINEAGE_BREAK_GRACE + LINEAGE_POST_SWITCH_CYCLE logging voor meetmethode
    - docs/RESTART_DOCTRINE.md: doctrine, productiebeleid, meetmethode
    - L3_SCHAALBEPERKINGEN: incremental refresh + RESTART_DOCTRINE ref
    - systemd: comment LIVE_VALIDATION_RUNTIME_MINUTES=0 voor productie
    
    Made-with: Cursor
  1105. 9637371
    fix: partial fill SL — cancel+replace for full cum_qty, balance-based market exit
    Show commit body
    - Add get_open_sl_exchange_id_for_entry to cancel prior SL before placing new
    - Replace amend with cancel+replace on additional partial (amend unreliable, leaves dust)
    - Use balance_cache for TP internal market exit (was qty_f64)
    - Trailing amend and time stop use protected_qty
    
    Made-with: Cursor
  1106. 6e8e634
    fix: exposure reconciliation + L3 hard block bottleneck
    Show commit body
    Exposure (phantom positions):
    - total_open_notional now reconciles DB positions with exchange balance
    - If balance_cache says base asset < 1% of DB position, treat as stale
    - Exclude from exposure and close in DB (base_position=0)
    - Fixes GLOBAL_EXPOSURE_BLOCK when DB has positions not on exchange
    
    L3 hard blocks (589/642 symbols blocked after restart):
    - clear_l3_resync_blocks at startup: reset all l3_resync_limit_reached
      blocks so we start fresh (previous blocks were from rate-limited sessions)
    - Global cold-start: when >50% of L3 symbols have 0 rows, skip per-symbol
      blocks (Kraken rate-limiting, not symbol-specific failure)
    - Reset prev_count when hard_block_until expired: symbol gets fresh chance
      instead of immediate re-block
    - Applied in ingest_runner, live_runner (combined), run_execution_only
    
    Made-with: Cursor
  1107. 69da0df
    perf: persistent instrument WS — single connection, live updates
    Show commit body
    Replace the open/snapshot/close pattern with a single persistent WS
    connection to Kraken's instrument channel:
    
    - preload_all() connects once, receives the full snapshot (1490 pairs),
      populates the cache, then hands off the read stream to a background
      tokio task
    - The background task keeps the connection alive and processes `update`
      messages, so the cache stays fresh when Kraken changes instrument
      rules (qty_min, price_increment, status, etc.)
    - On disconnect: automatic reconnect after 10s with full re-snapshot
    - get_instrument_constraints() reads from cache only — zero WS calls
    - Removed the legacy per-symbol fetch_instruments_v2 (no callers left)
    
    Net result: exactly 1 WS connection for instruments (kept alive),
    down from N connections (opened and closed per symbol per call).
    
    Made-with: Cursor
  1108. b9f69a0
    perf: cache instrument constraints — single WS connection at startup
    Show commit body
    get_instrument_constraints opened a new WebSocket per call, fetching
    the full 600+ pair snapshot each time only to extract one symbol.
    During startup reconcile this meant 5+ concurrent WS connections for
    the same data, triggering Kraken rate limits.
    
    Now: preload_all() fetches all pairs once via a single WS connection
    and caches them in-memory. Runtime callers read from cache with zero
    network overhead. Cache-miss falls back to single-symbol WS fetch and
    backfills the cache.
    
    Called at both startup paths (run_execution_live + run_execution_only)
    before exposure reconciliation, so protection_flow and dust detection
    have constraints available immediately.
    
    Made-with: Cursor
  1109. d8c099d
    fix: use notional-based dust threshold instead of per-symbol WS call
    Show commit body
    get_instrument_constraints opens a WS per symbol — too expensive for
    the hot path (total_open_notional runs every evaluation cycle). Replace
    with a simple $5 notional threshold: positions below this are untradeable
    dust regardless of exchange-specific minimums.
    
    Made-with: Cursor
  1110. 47eacfd
    fix: unblock execution — dust exposure exclusion, protection retry, shadow persistence
    Show commit body
    Proven bottlenecks from live data (100% order block rate):
    1. Dust positions (CFG, RARI, XPL) below exchange min order qty consumed
       ~$9 of $51 exposure budget despite being untradeable. Now excluded
       from total_open_notional, consistent with EXPOSURE_DUST_SKIP.
    2. Protection flow failed with "Insufficient funds" for NPC + TRIA
       because DB qty exceeded actual exchange balance. Added balance-cache
       retry: on Insufficient funds, fetches real available qty and retries.
    3. Dust positions in DB kept status=open forever. Now auto-closed to
       base_position=0 during startup reconciliation when below min order.
    4. V2 pipeline shadow trades were only logged, not persisted. Added
       insert_v2_shadow_trade for edge_floor_block and risk_gate skips.
    
    Made-with: Cursor
  1111. 58a821c
    observability: no-trade reason tracing — per-symbol, per-strategy, shadow-equivalent logs + CLI
    Show commit body
    5 observability additions (no strategy logic changes):
    
    1. V2_PIPELINE_SCOPE log: tradable_in_report, exec_allowed_count, tradable_in_scope,
       filtered_by_universe at pipeline entry (strategy_pipeline.rs)
    
    2. NO_TRADE_DOMINANT_REASON log per evaluation when no Execute outcomes: single
       dominant reason (data_runtime_not_ready / no_positive_edge_any_symbol /
       tradable_filtered_by_universe_or_freshness / all_candidates_skip_economics)
       with tradable_in_report, exec_allowed count, skip count (live_runner.rs)
    
    3. SYMBOL_ROUTE_DECISION log per symbol after route analysis: outcome (TRADABLE
       with route details or NO_TRADE with reason), edge, score, valid/total candidates,
       top 3 reject reasons (route_selector.rs + route_selector_v2.rs)
    
    4. report-no-trade-reasons CLI: per-symbol decision table, per-strategy funnel
       (in_scope, tradable, no_trade, top 3 reasons), aggregate reject histogram,
       near-miss symbols (analysis_commands.rs + commands.rs)
    
    5. V2_SHADOW_BLOCKED log for every pipeline Skip (edge_floor or risk_gate):
       symbol, blocker_reason, route_family, edge, confidence, spread, expected_move
       (strategy_pipeline.rs)
    
    Made-with: Cursor
  1112. 742db2f
    cursor rules: expliciet dat alle wijzigingen ook op de server moeten staan
    Show commit body
    Made-with: Cursor
  1113. e18fba6
    changelog: volgorde nieuwste bovenaan; bf87455 toegevoegd aan lijst
    Show commit body
    Made-with: Cursor
  1114. bf87455
    changelog: add entry for d003854
    Show commit body
    Made-with: Cursor
  1115. d003854
    changelog: volledig chronologisch (oudste eerst), alle 264 commits herleidbaar vanaf e4182b3
    Show commit body
    Made-with: Cursor
  1116. e8a8486
    changelog: add entry for 20f2f4c
    Show commit body
    Made-with: Cursor
  1117. 20f2f4c
    changelog: add entry for 51abf53
    Show commit body
    Made-with: Cursor
  1118. 51abf53
    changelog: add entry for d9734b9
    Show commit body
    Made-with: Cursor
  1119. d9734b9
    changelog: add entry for bfadca3
    Show commit body
    Made-with: Cursor
  1120. bfadca3
    changelog: add entry for ea94e6e (changelog + RAG spec)
    Show commit body
    Made-with: Cursor
  1121. ea94e6e
    changelog: document commits 8dceb90..8a4261a (execution, exit, hub, recv_timeout); add RAG backend spec doc
    Show commit body
    Made-with: Cursor
  1122. 8a4261a
    fix: recv_timeout Hub use remaining time on Lagged retry
    Show commit body
    Track deadline once and use saturating_duration_since(Instant::now())
    for each retry so total wait never exceeds requested timeout. Prevents
    nearly-doubled wait for exit monitor (250ms) and SL trailing under load.
    
    Made-with: Cursor
  1123. 157c238
    feat: hub-aware protect_exposure and ignition_exit via PrivateMsgSource
    Show commit body
    protection_flow: accept hub: Option<&PrivateWsHub> — when provided reuses the
    shared private WS connection instead of opening a new one.
    ignition_exit: switch from UnboundedReceiver to PrivateMsgSource so the hub
    broadcast path is supported and WS disconnect no longer hard-fails (logged, not
    bailed).
    live_runner: pass Some(&private_hub) to protect_exposure and ignition_exit at
    all four call sites.
    
    Made-with: Cursor
  1124. 6d91c91
    execution: protection after exit fill + exit_lifecycle on PrivateMsgSource
    Show commit body
    - exposure_reconcile: load_open_exit_qty_per_symbol uses remaining qty
      (quantity_base - cum_qty_base) so partial fills on exit orders are
      reflected; add ensure_protection_after_exit_fill() and call
      protect_exposure for remainder when needed (skip dust).
    - ws_handler: HandleResult::Applied gains exit_fill_symbol for exit-order
      fills so runner can trigger protection for remainder.
    - runner: on exit_fill_symbol set, call ensure_protection_after_exit_fill
      (pool, run_id, symbol, api_key, api_secret, hub).
    - exit_lifecycle: take PrivateMsgSource instead of UnboundedReceiver,
      use recv_timeout() throughout so hub and dedicated paths both work.
    - Add private_msg_source module and wire in mod.rs.
    - deterministic_proof: match HandleResult::Applied with exit_fill_symbol.
    
    Made-with: Cursor
  1125. 5ce3547
    fix: use UnboundedReceiver directly in exit_lifecycle (no PrivateMsgSource)
    Show commit body
    Replace PrivateMsgSource abstraction with tokio::sync::mpsc::UnboundedReceiver<PrivateV2Response>
    directly — matches the server's runner.rs call sites and avoids the untracked
    private_msg_source.rs dependency. All recv calls use tokio::time::timeout wrapping.
    
    Made-with: Cursor
  1126. 20bd5a9
    feat: maker TP exit — dual-order monitoring with 30s fill-check
    Show commit body
    Place a post_only limit order at TP price alongside the SL on exchange.
    WS executions channel determines which order fills first:
    - TP fill: cancel SL, exit_reason=tp_maker
    - SL fill: cancel TP limit (cleanup)
    - 30s fill timeout or time_stop/panic: cancel TP + cancel SL, market exit
      on balance-size from WS balance_cache (no REST)
    
    Activated for TSL and MakerLadder strategies; graceful degradation to
    internal TP check on post_only rejection. All via WS only; no REST calls.
    
    Made-with: Cursor
  1127. 730e7a5
    feat: model maker TP exit in fee and route expectancy
    Show commit body
    Add use_maker_tp to ExitConfig; model exit fee as maker_bps when TP is
    placed as a post_only limit order. Update fee_realism.rs, route_selector_v2.rs,
    and route_expectancy.rs so TakeProfit routes reflect the lower exit cost
    in expected_net_edge_bps, reducing taker fee drain and improving tier economics.
    
    Made-with: Cursor
  1128. da16a5c
    observability: demo_trades uit echte fills, tier2_* + admin snapshots (100% oplevering)
    Show commit body
    Made-with: Cursor
  1129. 1fedf48
    feat: single persistent private WS hub — 1 connection for all consumers
    Show commit body
    Architecture:
      Auth WS (1 connection, stays open hours/days)
        ├── subscribe: executions + balances
        ├── order submit / amend / cancel (via shared Arc<sender>)
        ├── execution reports → broadcast to position_monitor + others
        ├── balance updates → balance_cache (live equity)
        └── auto-reconnect on disconnect
    
    Before: 3 separate private WS connections (balance_feed, position_monitor,
    main execution). Each consumed a Kraken connection slot, causing persistent
    429 rate limit errors.
    
    After: 1 shared connection. PrivateWsSender uses interior mutability
    (parking_lot::RwLock) for hot-swap on reconnect. Broadcast channel
    distributes incoming messages to all consumers.
    
    Also includes: breakeven floor + 0.7% trail for exit management.
    
    Made-with: Cursor
  1130. 8a5f5ee
    fix: breakeven floor, tighter trail, staggered WS startup
    Show commit body
    Exit management:
    - Trail distance capped at 0.7% from high watermark (was 1.5%)
    - Breakeven floor at 30bps: SL never goes below entry once in profit
    - TP lowered to 200bps (was 250) for faster capture
    - Applied to all three exit paths (monitor, lifecycle, ignition)
    
    WS reliability:
    - Staggered WS connections at startup (3s delays) to avoid 429
    - Initial backoff raised to 5s (was 1s), max to 120s (was 60s)
    
    Made-with: Cursor
  1131. c1b67e9
    fix: breakeven floor + 0.7% trail — stop giving back profit
    Show commit body
    All three exit paths (position_monitor, exit_lifecycle, ignition_exit)
    now enforce:
    - Trail distance capped at 0.7% from high watermark (was 1.5%)
    - Breakeven floor: once peak profit exceeds 30 bps, SL is floored
      at entry price — profit move is never fully surrendered
    - TP lowered to 200 bps (was 250) for faster profit capture
    
    Before: TRIA peaked +121 bps but SL trailed to 0.03122 (below entry
    0.03131). Profit was surrendered entirely.
    After: SL would be max(0.03169*0.993, 0.03131) = 0.03147 = +0.5%
    above entry, locking in profit.
    
    Made-with: Cursor
  1132. 9f79b88
    fix: feed position symbols through main ticker WS, remove separate WS
    Show commit body
    The separate position ticker WS was hitting Kraken's 429 rate limit
    (too many concurrent WS connections). Now position symbols are added
    to the main ticker WS subscription at startup, ensuring price_cache
    always has data for open positions without an extra connection.
    
    Made-with: Cursor
  1133. a6cb13c
    fix: tighten position monitor trail — 1.5% distance, immediate activation
    Show commit body
    Was: 3% trail distance, only activates after 1.2% profit (40% of 300bps).
    Now: 1.5% trail distance from high watermark, activates immediately
    when position is in any profit. Much tighter risk control — SL stays
    close to current price instead of sitting far below entry.
    
    For TRIA at +0.25%: SL moves from 0.03057 (-2.4%) to ~0.03092 (-1.5%).
    
    Made-with: Cursor
  1134. d2940d8
    fix: add periodic status logging to position monitor
    Show commit body
    Logs PnL, current price, SL price, and highest price for each
    monitored position every 60s for diagnostics.
    
    Made-with: Cursor
  1135. c42357a
    fix: position monitor gets own ticker WS for position symbols
    Show commit body
    The main ticker WS only subscribes to evaluation-universe symbols,
    leaving active position symbols (TRIA, NPC, etc.) without price data
    in the price_cache. The monitor now spawns a dedicated public ticker
    WS that subscribes specifically to symbols with open positions,
    feeding price_cache so trail/TP evaluation actually works.
    
    Made-with: Cursor
  1136. 247194a
    feat: active position monitor — trail SL + TP for all open positions
    Show commit body
    Background task that actively manages ALL positions (not just current-run trades):
    - Persistent private WS for order amendments
    - Reads prices from price_cache (no REST)
    - Trails SL when price moves >40% of SL distance in favorable direction
    - Takes profit at market when unrealized PnL exceeds 250 bps
    - Detects SL triggers/fills via executions channel
    - Refreshes position list from DB every 30s
    - Auto-reconnects on WS disconnect
    
    Fixes the gap where reconciled/legacy positions only got a static
    emergency SL with no trailing, TP, or active management.
    
    Made-with: Cursor
  1137. 6bbffb6
    fix: live equity, dust detection, SL trigger detection, exposure calc
    Show commit body
    - Live equity via WS balances channel (balance_cache + balance_feed):
      replaces static EXECUTION_EQUITY_QUOTE, enables compounding.
      CapitalAllocator and exposure gate use live equity each cycle.
    - Dust detection: positions below exchange min_order_size are marked
      as dust and skipped in protection_flow (no more failed SL attempts).
    - protection_flow uses price_cache instead of REST for bid/ask.
    - Exposure calculation uses positions × live prices instead of stale
      execution_orders records that can strand in non-terminal status.
    - Critical: SL trigger detection now recognizes "triggered" status
      and exec_type from Kraken WS — stop-loss orders transition to
      "triggered" (not "filled") when the trigger price is hit.
      This was causing the bot to miss SL executions and fall through
      to time_stop, wasting hold time and creating orphan exposure.
    
    Made-with: Cursor
  1138. e9e7a80
    feat: 24h blocklist for NL-restricted symbols
    Show commit body
    Symbols rejected with "restricted for NL" or "Invalid permissions" now
    enter quiet mode for 24 hours instead of the default ~2 minutes. Stops
    wasting evaluation cycles on permanently unavailable pairs.
    
    Made-with: Cursor
  1139. 504f430
    fix(critical): SL order ID mismatch — match on cl_ord_id, not first ACK
    Show commit body
    wait_for_next_add_order_result was picking up stale entry-order ACK
    instead of the SL order response, causing the exit lifecycle to monitor
    the wrong exchange order. Fixed by matching on result.cl_ord_id.
    
    Also: amend_stop_loss_trigger now passes order_qty (Kraken API requires
    it), and add_stop_loss_order now logs the sent JSON.
    
    Made-with: Cursor
  1140. 6baef85
    fix: start ticker WS in execution-only mode for price_cache
    Show commit body
    The SL-only exit model needs live prices from price_cache for trailing
    and internal TP checks. The execution-only loop was missing the ticker
    WS that feeds this cache.
    
    Made-with: Cursor
  1141. 8dceb90
    refactor: SL-only exit model — no TP on exchange, trail SL trigger_price
    Show commit body
    Eliminates double-execution risk by keeping only the stop-loss on Kraken.
    TP decisions are made internally using in-memory price cache (no REST).
    SL is trailed via amend_order trigger_price (max 1/sec).
    Both exit_lifecycle.rs and ignition_exit.rs converged to same model.
    
    Made-with: Cursor
  1142. ba47407
    docs: add exit lifecycle horizon-aware SL/TP fix to changelogs
    Show commit body
    Made-with: Cursor
  1143. 64a9dd9
    fix: use horizon max_hold_secs for TSL routes instead of 0
    Show commit body
    Trailing stop routes set max_hold_secs=0 in the route candidate (no fixed
    time limit). The exit config builder needs the horizon's actual hold time to
    set a proper time_stop, so use horizon.max_hold_secs() from the pipeline
    and guard against 0 as fallback.
    
    Made-with: Cursor
  1144. 1fe5f03
    fix: make exit SL/TP/time_stop horizon-aware instead of fixed values
    Show commit body
    The TSL exit config had SL=-60bps and time_stop=30s, which for breakout
    continuation/medium routes (expected hold: 600s, expected move: 100-300bps)
    meant the stop loss was inside the spread and positions were dumped after
    30 seconds before the breakout could develop.
    
    Changes:
    - Add max_hold_secs and expected_move_bps to Outcome, wire from V2 pipeline
    - Scale SL to 50% of expected_move (min 100, max 500 bps)
    - Scale TP to 150% of expected_move (min 150, max 1000 bps)
    - Set time_stop from route horizon instead of fixed 30s
    - Fix direction bug: SL/TP now correctly flip for short positions
    
    Made-with: Cursor
  1145. 3b9cf89
    fix: restore TSL capital protection gating for BreakoutContinuation
    Show commit body
    VolatilityTrailingStop was unconditionally allowed for breakout routes,
    contradicting the exit regime's capital protection doctrine which
    restricts TSL in volatile/reversal-prone markets.
    
    Root cause: the V2 regime matrix generates TSL candidates for
    VolatilitySpike breakouts, but the V2 admission override only handled
    EdgeNegative and MoveBelowFees — not ExitRegimeNotAllowed. This
    caused every VolatilitySpike breakout to be permanently blocked,
    which prompted the unconditional TSL workaround.
    
    Proper fix:
    1. Restore TSL gating for BreakoutContinuation (TSL only in non-
       volatile conditions, consistent with PullbackContinuation)
    2. Add ExitRegimeNotAllowed to V2 admission override conditions
    
    Now V1 capital protection is the default (TSL blocked in explosive
    markets), while V2 can override with quantitative justification
    when multi-scenario tail math is positive.
    
    Made-with: Cursor
  1146. 1708761
    fix: add 1% tolerance to final notional check for step-size rounding
    Show commit body
    After instrument step-size normalization, the final qty*price can
    be fractionally below the minimum notional (e.g., $9.9999 < $10.00)
    due to floating point truncation. This blocked orders that were
    effectively at minimum. Adding 1% tolerance prevents false rejections
    while still catching genuinely undersized orders.
    
    Made-with: Cursor
  1147. 0ab6a7e
    fix: use Kraken-compliant Short UUID for cl_ord_id
    Show commit body
    Kraken WS v2 accepts cl_ord_id in three formats: Long UUID (36
    chars with hyphens), Short UUID (32 hex chars), or Free text (max
    18 chars). The previous format "kb" + prefix + UUID simple = 35
    chars didn't match any valid format, causing
    EGeneral:Invalid arguments:cl_ord_id rejections on every order.
    
    Now generates pure UUIDv4 simple (32 lowercase hex chars) which is
    a valid Short UUID format. Also fixes the same issue in probe
    modules and intent.rs fallback path.
    
    Made-with: Cursor
  1148. 436a072
    fix: recalibrate exit regime thresholds for enriched vol_proxy
    Show commit body
    The exit regime selector used HIGH_VOL_THRESHOLD=2.5 and
    ORDERLY_VOL_MAX=2.0, calibrated for legacy microprice deviation
    (0-10 bps). With ignition-enriched vol_proxy (rolling realized vol,
    10-100 bps), every pair was classified as "volatile", blocking
    VolatilityTrailingStop for BreakoutContinuation and PullbackContinuation.
    
    This contradicted the regime route matrix which specifies TSL for
    vol_spike routes, causing all candidates to get ExitRegimeNotAllowed.
    
    Recalibrated to: HIGH_VOL=50, IMPULSE=3.0, ORDERLY_VOL_MAX=40.
    Also: TSL is now always allowed for BreakoutContinuation (natural fit).
    
    Made-with: Cursor
  1149. c4b5bef
    fix: add medium-horizon routes for vol_spike and trend regimes
    Show commit body
    VolatilitySpike previously only had Short routes where sqrt(3)=1.73x
    time factor couldn't produce moves that clear the 70 bps taker fee
    threshold. Medium horizon with sqrt(10)=3.16x enables breakout
    continuation to reach 95+ bps expected moves for vol_proxy >= 20 bps.
    
    Also adds medium BreakoutContinuation/MakerFirst to TrendContinuation
    and medium PumpFade/MakerFirst to VolatilitySpike for better fee
    efficiency (55 bps maker vs 70 bps taker roundtrip).
    
    Made-with: Cursor
  1150. a9ff78c
    fix: momentum routes use sqrt-time scaling + reactive vol_proxy
    Show commit body
    Two critical fixes for the edge calculation chain:
    
    1. enrich_with_ignition now uses max(rolling_vol_5m, rolling_vol_30m)
       instead of only 30m — reactive to recent spikes while keeping
       the longer-window floor.
    
    2. Momentum path builders (pullback, breakout, pump_fade, dump_reversal)
       now use sqrt(hold_minutes) as time factor instead of the fixed
       horizon_factor (0.4/1.0/2.2). vol_proxy is a per-minute metric;
       expected cumulative moves scale as sqrt(time) per volatility theory.
       This produces realistic 40-120 bps expected moves for active markets
       instead of the previous 10-15 bps that could never clear fees.
    
    PSC (spread capture) keeps its existing horizon_factor logic unchanged.
    
    Made-with: Cursor
  1151. c1a6f3d
    fix: V2 adaptive engine now uses ignition-enriched vol_proxy
    Show commit body
    The V2 engine was using raw microprice_deviation_bps (~2-5 bps) as
    vol_proxy instead of rolling_vol_30m_bps (50-500 bps in active markets).
    This made expected_move_bps perpetually too low to clear the 55 bps fee
    threshold, resulting in tradable=0 even during high-volatility markets.
    
    Now bootstraps ignition metrics and enriches features (vol_proxy,
    trend_strength, impulse_magnitude) before regime classification and
    expected path building, matching V1 behaviour.
    
    Made-with: Cursor
  1152. 96ab18b
    throughput: fix execution reliability + unlock trade frequency
    Show commit body
    - Fix critical bug: Kraken add_order WS method response errors were
      only logged (warn) but never handled — order stayed in PendingSubmit
      for 60s timeout instead of immediately transitioning to Rejected.
      Now: on_reject + tracker update + funnel event + quiet mode + break.
    
    - Widen route map: Quiet state adds PullbackContinuation, Compression
      adds BreakoutContinuation. All existing edge/confidence/economics
      gates still apply — only the candidate pool expands.
    
    - Wire EDGE_ENGINE_V2 config flag into live pipeline. When enabled,
      run_adaptive_route_analysis (V2 admissibility with tail-positive
      and best-scenario gates) replaces static V1 route analysis.
    
    - Allow up to 2 executes per evaluation cycle (was 1). Each candidate
      individually passes symbol lock, capital allocator, exposure gate,
      and choke. No risk gates bypassed.
    
    Made-with: Cursor
  1153. 88c3db6
    fix: align observability snapshot contract with L3 availability semantics
    Show commit body
    Add l3_symbol_count alongside total l3_count and propagate it through stats queries, export generation, and the snapshot schema. This makes L3 availability percentage derivation explicit and consistent with the documented contract.
    
    Made-with: Cursor
  1154. 14ed8d6
    fix: unblock execution sizing and surface signal blocker reasons
    Show commit body
    Always derive market-order quantity from intended quote notional and upscale to satisfy min notional/cost constraints, reducing submit rejections like FINAL_NOTIONAL_TOO_SMALL and MARKET_ORDER_COST_MIN. Also persist signal-stage reason on discovered outcomes so funnel blocker attribution no longer falls back to unknown.
    
    Made-with: Cursor
  1155. daf8ecf
    fix: classify no-outcome blockers and expose signal reasons
    Show commit body
    Classify empty pipeline cycles into explicit funnel blocker reasons (no_tradable_routes, no_feature_complete_symbols, no_pipeline_outcomes) and print signal blocker reason counts in report-trading-throughput. This makes throughput bottlenecks visible before admission/sizing stages.
    
    Made-with: Cursor
  1156. f91ac7d
    fix: add run-id fallback for V2 unlock section
    Show commit body
    Use run_symbol_state as fallback when freshest active run_id is unavailable on the decision DB so report-trading-throughput can still compute V2 unlock metrics.
    
    Made-with: Cursor
  1157. 12fd7ba
    fix: ensure funnel telemetry flows during no-trade cycles
    Show commit body
    Emit signal/admission funnel events for no-outcome and skip-only evaluation cycles in execution-only mode, so throughput reporting no longer stays empty when no order is submitted. Also extend V2 unlock reporting with regime/route-family edge distribution splits.
    
    Made-with: Cursor
  1158. 41d2efa
    feat: add throughput funnel telemetry and report-trading-throughput
    Show commit body
    Build a DB-backed trading funnel telemetry layer across signal, admission,
    capital, execution, and fill stages. Add structured events and persistence via
    trading_funnel_events, and introduce report-trading-throughput with 2h funnel
    metrics, drop-off analysis, regime/route-family breakdowns, and V2 unlock
    estimates (extra trades/hour, extra fills/hour, edge distribution).
    
    Also wire route_family through pipeline outcomes and use a combined
    priority score (ignition + tail asymmetry + freshness bonus) for V2 ranking.
    
    Made-with: Cursor
  1159. acd92f3
    docs: align doctrine docs with live ignition trading
    Show commit body
    - Document event-driven evaluation wake (EVAL_WAKE_IGNITION)
    - Document mid-trade exit re-routing via ExitMode switching
    - Document capital allocation gate and log evidence
    
    Made-with: Cursor
  1160. 27199f5
    feat: doctrine-level trading architecture — 3 critical categories
    Show commit body
    1. Mid-trade re-routing: exit lifecycle is now a state-driven decision
       loop. ExitMode (Trailing, AggressiveTrail, ContinuationHold,
       ExhaustionExit, ImmediateExit) is re-evaluated every 250ms based on
       live ignition state. Mode transitions trigger trail tightening,
       time-stop extension (Continuation +120s), or immediate market exit
       (state regression to Quiet). All mode changes are logged as
       EXIT_MODE_REROUTED events.
    
    2. Event-driven evaluation: IgnitionMetricsStore now fires
       transition_notify when metrics cross ignition thresholds on minute
       boundaries. The evaluation loop uses tokio::select! to wake
       immediately on ignition events instead of waiting for the full
       sleep interval. This captures alpha from state transitions without
       latency loss.
    
    3. Capital allocation doctrine: new CapitalAllocator with per-regime
       slot limits (Ignition:2, Expansion:2, Continuation:2, Other:1),
       regime-bucketed capital budgets (30/25/25/10%), edge-weighted
       sigmoid sizing, liquidity-aware scoring, drawdown throttling, and
       symbol deduplication. Wired into both live and execution-only loops
       with register/release lifecycle.
    
    Made-with: Cursor
  1161. 1e7fbc6
    fix: comprehensive trading execution fixes — 8 critical bugs resolved
    Show commit body
    1. Pass live IgnitionMetricsStore to exit context (was empty/new store)
    2. Fix market order qty: notional→base conversion via best bid/ask
    3. Fix cost_min bypass for market orders (price=None skipped check)
    4. Add minimum notional gate ($10) in submit_order pre-submission
    5. Make equity configurable via EXECUTION_EQUITY_QUOTE (was hardcoded 1000)
    6. Persist IgnitionStateMachine across exit loop iterations (hysteresis)
    7. Persist panic exit orders to DB + update SL/TP status on panic
    8. Track last_known_price for accurate EXIT_COMPLETED realized PnL
    
    Made-with: Cursor
  1162. bedf58c
    doctrine compliance audit: observability, structural fixes, validation report
    Show commit body
    Phase 1 — Observability:
    - Add IGNITION_STATE_TRANSITION per-symbol event with metrics (state.rs)
    - Add TRADING_ENTRY_DECISION event before order submission (live_runner.rs)
    - Replace ORDER_SUBMITTED_TO_EXCHANGE with ORDER_LIVE_SUBMIT including
      all required fields: side, price, qty, strategy_context, ignition_state
    - Add EXIT_PLAN_CREATED with exit_mode, sl_price, tp_price, trailing_enabled,
      trail_distance_bps to both ignition_exit and exit_lifecycle
    - Add TRAIL_UPDATE event with highest_price, new_tp, amend_success, retry_count
    - Add EXIT_COMPLETED to all exit paths (panic_pnl, market_fill_timeout)
    
    Phase 3 — Structural fixes:
    - Log exit order insertion failures instead of silent discard (7 sites)
    - Conservative fallback on symbol_lock_check failure (block instead of allow)
    - Consolidate symbol execution lock: check ALL open orders regardless of side
    - Full EXIT_COMPLETED coverage on all termination paths
    
    Phase 4 — Validation CLI:
    - New report-trading-doctrine-alignment command with 10 diagnostic sections:
      trades per state/strategy, edge capture, exit reasons, exit order persistence,
      trail efficiency, re-entry violations, position check, safety state, compliance
    
    Made-with: Cursor
  1163. 4686ad6
    production safety: implement ignition trading architecture v1
    Show commit body
    14 structural risks identified in audit, all addressed across 5 phases:
    
    Phase 1 - Safety Foundation (R1-R7, R9):
    - Persist exit orders (SL/TP/trail/time-stop) to execution_orders with parent_order_id
    - Enforce MAX_OPEN_NOTIONAL_PCT_OF_EQUITY (10%) as hard gate before submission
    - Enable circuit breakers: drawdown $50 default, 10 orders/min rate limit
    - Wire panic PnL exit (-200bps) in ignition trailing loop
    - Re-enable cl_ord_id in kraken_adapter for order correlation
    - Fix partial fill under-protection: track cum_qty and amend SL qty
    - Sync symbol_safety_state hard_block from ingest to decision pool
    - Market fill timeout returns Err and triggers protect_exposure
    
    Phase 2 - Exit Robustness (R8, R10):
    - amend_order retry (3x, 500ms backoff) with cancel+replace fallback
    - WS disconnect during exit marks orders suspect and returns Err
    
    Phase 3 - Latency Reduction (R12):
    - Adaptive evaluation interval: 60s when ignition-active, base otherwise
    - IGNITION_ADAPTIVE_INTERVAL_ENABLED env var (default true)
    
    Phase 4 - Telemetry (R13, R14):
    - Unified LIVE_DATA_MAX_AGE_SECS via env var across all freshness checks
    - Structured EXIT_COMPLETED event with hold_secs, pnl_bps, exit_reason
    
    Phase 5 - Edge Optimization:
    - Reduce trail check interval from 1000ms to 250ms
    
    Made-with: Cursor
  1164. 4f1e3d8
    contract: all CLI reads use ingest pool + freshest_active_run_id
    Show commit body
    Structural audit fix: CLI analysis/report commands now use
    pools.ingest() instead of pools.decision() for raw data reads.
    All 25 latest_run_id() calls in analysis_commands.rs replaced with
    resolve_run_id() which prefers freshest_active_run_id (active ingest
    data within 5 min) and falls back to latest_run_id.
    
    Contract: ingest pool for market data, decision pool for execution,
    freshest_active_run_id for current run resolution.
    
    Made-with: Cursor
  1165. 1a0b244
    fix: propagate correct run_id through entire readiness/pipeline chain
    Show commit body
    run_readiness_analysis now prefers freshest_active_run_id over
    latest_run_id. run_strategy_pipeline uses run_readiness_analysis_for_run
    with the caller-provided run_id instead of re-querying latest_run_id.
    This ensures the entire one-shot chain uses the same fresh run_id
    from the active ingest, not the highest-ID execution_live run.
    
    Made-with: Cursor
  1166. 49ca7be
    fix: use freshest active run_id for one-shot commands instead of MAX(id)
    Show commit body
    latest_run_id() returns the highest run ID, which may be an
    execution_live run with no fresh data. The active ingest runner
    writes to an older run_id. New freshest_active_run_id() finds
    the run with ticker data within the last 5 minutes. Falls back
    to latest_run_id() when no fresh run exists.
    
    Made-with: Cursor
  1167. d99bcf9
    fix: use ingest pool for readiness/pipeline in one-shot commands (physical separation fix)
    Show commit body
    run_execution_once and run_proof were passing the decision pool to
    run_readiness_analysis and run_strategy_pipeline, which query
    ticker_samples/trade_samples/pair_summary_24h — tables that only
    exist in the ingest database under physical separation. This caused
    data_stale=true and tradable_count=0 for all one-shot CLI commands.
    
    Now: ingest pool for data reading, decision pool for execution writes.
    Made-with: Cursor
  1168. 0db331d
    docs+scripts: classify SSOT/current/background, update DOC_INDEX, mark legacy scripts
    Show commit body
    Made-with: Cursor
  1169. 82f4494
    fix: persist ignition metadata (state, edge_before/after, trail_mode) to execution_orders
    Show commit body
    Added update_ignition_metadata() to stamp ignition columns right after
    order creation. The trade flow report now correctly identifies ignition
    trades. Extended IgnitionExitContext with edge_before/after_boost fields.
    
    Made-with: Cursor
  1170. 980f361
    diag: add ignition state distribution + trading summary logging
    Show commit body
    Logs per-state counts (Quiet/Compression/Ignition/Expansion/etc.) and
    a summary of how many symbols were boosted and admitted by ignition
    trading logic per evaluation cycle.
    
    Made-with: Cursor
  1171. 5c17fbf
    fix: pass ingest pool to ignition bootstrap (physical separation fix)
    Show commit body
    The ignition bootstrap queries trade_samples_p_default which lives in
    the ingest database, but the live runner was passing pools.decision().
    With physical separation active this yielded 0 trades and no ignition
    state classifications. Now passes pools.ingest() via a new
    run_v2_route_analysis_with_ingest() function.
    
    Made-with: Cursor
  1172. 8b6bad3
    feat: enable ignition trade flow — candidate boost, edge relaxation, trailing exit, telemetry
    Show commit body
    Translate validated Ignition state signals into actual trade flow:
    - IGNITION_TRADING_ENABLED flag (default false) controls all new behavior
    - Post-evaluation candidate-level expected_move boost (factor 1.35, not feature-level)
    - Negative-edge gate relaxed to -5 bps for Ignition/Expansion states only
    - Ignition-aware entry prioritization (ignition_admitted candidates ranked higher)
    - Event-driven trailing-stop exit (ignition_exit.rs) with state-aware time stops
      (Ignition: 120s, Expansion: 180s, Continuation: 300s)
    - Trail activation at +25 bps, tightens at +50 bps, exhaustion detection
    - amend_order integration with instrument price normalization
    - report-ignition-trade-flow CLI report (trades per state, edge distribution, MFE/MAE, winrate)
    - Migration: ignition_state + metadata columns on execution_orders
    - All 59 tests pass
    
    Made-with: Cursor
  1173. f25ebde
    feat: add report-ignition-followthrough — distribution, MFE/MAE, time-to-move
    Show commit body
    New CLI command shows for symbols in Ignition/Expansion state:
    - Follow-through distribution per window (1m/5m/10m/15m) with p50/p75/p90/max
    - Directional MFE/MAE (max favorable/adverse excursion) per symbol
    - Time-to-move analysis (+25/+50/+100 bps thresholds with hit rates)
    - Route relevance hint based on where p90 edge concentrates
    
    Made-with: Cursor
  1174. 63dfe4d
    feat: add ignition/state-transition engine with rolling metrics, state machine, and route filtering
    Show commit body
    New module src/ignition/ implements:
    - Per-symbol 1m candle ring buffer (IgnitionMetricsStore) with on_trade delta-update
    - Rolling metrics: vol acceleration, range expansion, signed persistence, density shift
    - Explicit state machine (Quiet/Compression/Ignition/Expansion/Continuation/Exhaustion)
      with hysteresis (min 2m state duration, 1m cooldown, confidence floor 0.6)
    - State → route family mapping (e.g. Ignition → breakout+fade, Quiet → passive only)
    - Feature repair: when IGNITION_ENABLED, vol_proxy and trend_strength are replaced by
      rolling realized values from trade-derived candles
    - Diagnostic CLI: report-ignition-state with state distribution, top movers, route
      activation, active vs quiet metric comparison, avg move after ignition trigger
    
    All behind IGNITION_ENABLED feature flag (default off = zero behaviour change).
    8 unit tests covering metrics computation, state classification, and hysteresis.
    
    Made-with: Cursor
  1175. 278a40e
    fix: use Krakenbot's own trade_samples for realized vol, not external candles
    Show commit body
    The public.candles table was populated by the old KapitaalBot Python
    service which is no longer running (73h stale). Switch to synthesizing
    1-minute close prices from krakenbot.trade_samples_p_default which has
    live data (637 symbols, sub-second freshness).
    
    Made-with: Cursor
  1176. 8c9a3c0
    fix: anchor candle window on latest available data, not now()
    Show commit body
    Candle data may be hours old; using now() as anchor yields empty
    results. Use max(timestamp_ms) from candles table instead.
    
    Made-with: Cursor
  1177. a2ebd96
    fix: route realized-vol-diagnostic to ingest pool (candles in public schema)
    Show commit body
    The candles table lives on the ingest DB, not the decision DB.
    Route the report-realized-vol-diagnostic command through pools.ingest()
    so the public.candles query resolves correctly.
    
    Made-with: Cursor
  1178. 948aa84
    feat: add realized-vol diagnostic CLI (Phase 1 what-if analysis)
    Show commit body
    New command `report-realized-vol-diagnostic [run_id]` computes realized
    volatility (median c2c 1m returns) and trend strength from candle data,
    substitutes into MarketFeatures in-memory, re-runs V1 route analysis,
    and reports the tradable frontier delta with full risk checks (R1-R4).
    
    No production code modified — diagnostic only.
    
    Made-with: Cursor
  1179. e144335
    feat(diagnostics): enhance report-adaptive-edge with full decision output
    Show commit body
    - Make evaluate_pair_adaptive/AdaptivePairResult public for diagnostics
    - Add optional run_id CLI argument (report-adaptive-edge [run_id])
    - Report sections: regime distribution, candidate stats with admission
      reason breakdown, profitability map, top-20 admitted candidates table
    - Auto-conclusion logic based on frontier quality
    
    Made-with: Cursor
  1180. 5046f2e
    feat(edge_engine): adaptive edge extraction architecture (EDGE_ENGINE_V2)
    Show commit body
    Implement regime-based adaptive trade engine that classifies market
    state per symbol and conditionally selects routes, fee models, exit
    paths, and admissibility logic — all feature-flagged behind
    EDGE_ENGINE_V2 (default false, zero behaviour change when off).
    
    Modules:
    - market_regime: 4-regime classifier (SpreadFarming/VolatilitySpike/
      TrendContinuation/MeanReversion) from MarketFeatures
    - route_selector_v2: regime-filtered candidate matrix + orchestration
    - move_distribution: probabilistic edge (p50/p75/p90/tail) +
      AdaptiveEdgeTrace explainability per candidate
    - fee_realism: maker fill probability blended fees + inventory unwind
    - exit_path: component exit drag (latency/adverse/sweep/timeout) +
      trailing stop path simulator with ATR bands
    - admissibility: tail-aware gate allowing median-negative but
      tail-positive trades in asymmetric regimes
    - diagnostics: report-adaptive-edge CLI command
    - shadow_compare: V1 vs V2 side-by-side comparison CLI
    
    Integration: minimal hooks in 4 existing files (main.rs mod decl,
    config bool, cli string match, analysis_commands handler).
    No edits to route_engine, analysis, execution, or edge modules.
    17 unit tests, all passing.
    
    Made-with: Cursor
  1181. 13c1427
    fix(script): remove set -e so background job and kill do not abort script
    Show commit body
    Made-with: Cursor
  1182. 769b2ac
    revert: remove stdin redirect and debug; use plain background run
    Show commit body
    Made-with: Cursor
  1183. 35da3e2
    debug: log REPORT_PID to .debug file
    Show commit body
    Made-with: Cursor
  1184. ec416ca
    fix(script): rm stale no_progress at start; start report with stdin from /dev/null
    Show commit body
    Made-with: Cursor
  1185. 114ac64
    fix(build): add stats_queries::realized_exit_durations_for_run, RouteCandidate.trace, exit_feasibility/route_selector for guarded reports
    Show commit body
    Made-with: Cursor
  1186. a443da0
    feat(cli): guarded report runner — file-based, fail-fast 2min, heartbeat, PHASE_* markers, diagnostic+strace
    Show commit body
    Made-with: Cursor
  1187. 83bc640
    feat(observability): systemd timer for snapshot export for website
    Show commit body
    - krakenbot-observability-export.service: oneshot export to OBSERVABILITY_EXPORT_DIR
    - krakenbot-observability-export.timer: every 2 min
    - OBSERVABILITY_EXPORT_SETUP.md: install and deploy instructions
    
    Made-with: Cursor
  1188. ab2bb52
    feat(cli): report-edge-chain — edge decomposition, stage comparison, lane classification, decision stage-fix vs lane-split
    Show commit body
    Made-with: Cursor
  1189. 4b88b0e
    feat(cli): report-edge-forensic for selected symbols — market context, candidate trace, kill-point, verdict
    Show commit body
    Made-with: Cursor
  1190. 45086a6
    feat(route): net_edge_before_l3_penalty + report-edge-autopsy sample trace (gross_move, fee, exit_drag, wait_risk, capture, net_raw, net_final, zero_reason, invalid_shortcut)
    Show commit body
    Made-with: Cursor
  1191. 32d965c
    fix(cli): economics reports use live fee provider; log maker_bps/taker_bps/source; warn on bootstrap
    Show commit body
    Made-with: Cursor
  1192. 2e724a0
    feat(cli): report-edge-autopsy — funnel, component-kill, distribution, exact-zero, top 20 near-tradable
    Show commit body
    Made-with: Cursor
  1193. b080c7a
    feat(cli): report-confidence-threshold-sensitivity — edge>0 only, MIN_CONFIDENCE, tradable at current/-10%/-20%/-30%, per route_type/horizon
    Show commit body
    Made-with: Cursor
  1194. 1c214e8
    feat(cli): report-edge-reject-split — edge≥0 reject split by reason, route_type, horizon, top 10
    Show commit body
    Made-with: Cursor
  1195. f727f04
    feat(cli): report-feature-incompleteness + economics-run (universe, near-miss, hard conclusion)
    Show commit body
    - report-feature-incompleteness [run_id]: cause split (no_l2, spread_null, micro_null, insufficient_snapshots, pipeline_state), top 50 incomplete, oorzaakverdeling
    - report-route-economics: Universe section, Near-miss frontier (edge bands), Harde conclusie
    - CHANGELOG: Model input kwaliteit + economics-run section
    
    Made-with: Cursor
  1196. 7d6cb5d
    fix(exec): add STATE_SYNC_OK/REQUIRED gate in execution-only loop for formal log proof
    Show commit body
    Made-with: Cursor
  1197. f7ac5cc
    feat(route): bind execution universe to feature-complete symbols
    Show commit body
    - CurrentRunMarketRow: l2_count, is_feature_complete (spread+micro NOT NULL, l2_count>=50)
    - Route analysis: filter to feature-complete only; FEATURE_FILTER_APPLIED, FEATURE_INCOMPLETE_SYMBOLS
    - CLI report-feature-completeness [run_id]; docs/FEATURE_COMPLETENESS_CONTRACT.md
    - CHANGELOG + CHANGELOG_ENGINE updated
    
    Made-with: Cursor
  1198. a6b18aa
    feat(exec): STATE_SYNC_REQUIRED/STATE_SYNC_OK gate + check-decision-state CLI + STATE_SYNC_CONTRACT.md
    Show commit body
    Made-with: Cursor
  1199. 14a0f38
    fix(observability): always log FEATURE_COVERAGE_L2 for proof (including status=no_state)
    Show commit body
    Made-with: Cursor
  1200. 6ee5dbc
    fix(dual-DB): schema sync, spread_std_bps sanitization, sync proof, gen_mismatch in logs
    Show commit body
    - run_symbol_state: explicit column list (19 cols), SELECT numerics as ::text, parse_decimal_for_sync (NaN/Inf→NULL), post-sync rowcount ingest vs decision
    - full/incremental refresh: spread_std_bps CASE to NULL when NaN/Inf/non-finite
    - migration 20260314100000: ADD COLUMN IF NOT EXISTS for generation_id, l3_n_* on run_symbol_state (run on both DBs)
    - live_runner: INGEST_DECISION_SYNC_VISIBLE logs visible_generation_id and gen_mismatch=0|1
    
    Made-with: Cursor
  1201. 584b256
    feat(observability): add FEATURE_READY_SIGNAL and FEATURE_COVERAGE_L2 to execution-only evaluation loop
    Show commit body
    - execution-only path had no feature readiness gate
    - added l2_raw_feature_ready check before refresh
    - log FEATURE_READY_SIGNAL ready=true/false
    - after sync log FEATURE_COVERAGE_L2
    - skip evaluation cycle if L2 coverage < threshold
    - aligns execution-only behaviour with combined runtime path
    
    Made-with: Cursor
  1202. 2229057
    fix(universe): only status online/limit_only in pool; no status = exclude
    Show commit body
    Made-with: Cursor
  1203. dbd2b01
    feat(pipeline): Model Input Pipeline Hardening + L2 feature lineage CLI
    Show commit body
    - DEEL 1: FEATURE_COVERAGE_L2 log + ROUTE_ENGINE_SKIP if coverage_pct_spread < 60%
    - DEEL 2: FEATURE_READY_SIGNAL + refresh gate (l2_raw_feature_ready) before refresh
    - DEEL 3: MarketFeatures l2_spread_missing/l2_micro_missing; path fallback confidence*0.6, move*0.5; ROUTE_FEATURE_FALLBACK_USED
    - DEEL 4: Direction fallback (density-based confidence 0.05..0.45) when micro missing; DIRECTION_FALLBACK_USED
    - DEEL 5: Telemetry FEATURE_COVERAGE_L2, FEATURE_READY_SIGNAL, ROUTE_FEATURE_FALLBACK_USED, DIRECTION_FALLBACK_USED, MISSING_FEATURE_SYMBOL_COUNT
    - DEEL 6: report-route-economics / report-direction-signal exit(2) when coverage < 60% (ECONOMICS INVALID)
    - CLI: report-l2-feature-lineage for raw L2 vs run_symbol_state diff
    - Docs: MODEL_INPUT_PIPELINE_HARDENING.md, L2_FEATURE_LINEAGE_DEBUGGING.md
    - CHANGELOG.md + docs/CHANGELOG_ENGINE.md updated
    
    Made-with: Cursor
  1204. 2298649
    fix: add l2_raw_feature_ready and l2_feature_coverage_from_state to stats_queries (required by live_runner)
    Show commit body
    Made-with: Cursor
  1205. 88b7cf4
    feat(universe): override-only caps, unbounded default, FX excluded, explicit logging
    Show commit body
    - Config: *_OVERRIDE env only; Option<usize> limits; no default 200/50
    - UniverseManagerConfig + select_layer: cap only when Some(n)
    - universe_source: exclude FX pairs (EUR/USD, GBP/USD, etc.); USD crypto only
    - Observe: select_l2/l3_subset take Option<usize>; None = all
    - Startup logs: UNIVERSE_SCOPE usd_only fx_pairs_excluded; UNIVERSE_LIMIT_OVERRIDE_ACTIVE/INACTIVE
    - docs/UNIVERSE_CAPS_AND_CONFIG.md, LOGGING.md updated
    
    Made-with: Cursor
  1206. e826ebe
    fix(dual-DB): read run_symbol_state from ingest after refresh when physical separation; log sync result
    Show commit body
    - After refresh, universe state (per_symbol_counts, l2/l3_stats, raw_counts) now read from ingest pool when pools.is_physical_separation(), so symbol_count is correct regardless of sync.
    - Sync result logged: RUN_SYMBOL_STATE_SYNC ingest→decision (synced_rows) or RUN_SYMBOL_STATE_SYNC failed.
    - Applied in: initial warmup, periodic universe refresh, L3 resync block, pinned/exit-only L3 checks.
    - Fixes symbol_count=0 in UNIVERSE_REFRESH_STATE_TIMING when using separated ingest/decision DBs.
    
    Made-with: Cursor
  1207. 34936e4
    cli: report-path-source-inspect for expected_move_bps/confidence source inspection
    Show commit body
    - Add report-path-source-inspect CLI: upstream counts (micro/spread) and per-candidate path inputs -> output sample.
    - Doc: RAPPORT_EXPECTED_MOVE_BPS_SOURCE_INSPECTION.md with trace per route and fix direction.
    
    Made-with: Cursor
  1208. 1ca5b9d
    chore: CHANGELOG + CHANGELOG_ENGINE for LIVE_USE_OWN_RUN_ONLY warmup poll (capacity-test)
    Show commit body
    Made-with: Cursor
  1209. c460ac7
    cli: add report-direction-signal (continuation/reversal/confidence, expected_move_bps, by route_type and horizon)
    Show commit body
    Made-with: Cursor
  1210. 8c862fe
    db: run_symbol_state VerifyMode, detailed_differences, debug_refresh_diff, run_replay_compare for CLI verify/debug/replay
    Show commit body
    Made-with: Cursor
  1211. e69d0c0
    cli: add report-route-economics for V2 candidate frontier and economics tuning
    Show commit body
    Made-with: Cursor
  1212. a9778a1
    live_runner: LIVE_USE_OWN_RUN_ONLY poll raw tables after flush (max 90s) for sufficient warmup data
    Show commit body
    Made-with: Cursor
  1213. 6c72ab5
    live_runner: flush writer before warmup check when LIVE_USE_OWN_RUN_ONLY (ensure symbol_count > 0)
    Show commit body
    Made-with: Cursor
  1214. 7cc7201
    docs: risk 3 dicht — verify-refresh-equivalence 114 WithinTolerance; controleplan + correctness-proof updated
    Show commit body
    Made-with: Cursor
  1215. 4f10b00
    verify: allow WithinTolerance with up to 30 symbol differences when max diffs within relative tolerance (e.g. ingest growth between full and inc)
    Show commit body
    Made-with: Cursor
  1216. a777f9d
    verify: use watermarks -1 so id > -1 includes all rows (incl. id=0 if any)
    Show commit body
    Made-with: Cursor
  1217. 0f27b96
    LIVE_USE_OWN_RUN_ONLY: integrated run for clean 400-symbol capacity test
    Show commit body
    - Config LIVE_USE_OWN_RUN_ONLY: warmup uses data_run_id=run_id, 60s warmup; epoch binding only to own run.
    - epoch_queries: select_valid_epoch_for_run, current_epoch_for_exit_only_for_run; EpochBindingReason::OwnRunOnly.
    - run_l3_capacity_test.sh: EXECUTION_UNIVERSE_LIMIT, LIVE_USE_OWN_RUN_ONLY; doc integrated run usage.
    
    Made-with: Cursor
  1218. ceba7a4
    verify: relative tolerance (10%) for large numerics; NULL vs value no longer as 0 vs value for diff
    Show commit body
    Made-with: Cursor
  1219. e5c878d
    fix: incremental refresh match full — STDDEV sample (n-1), L3 per-metric counts for AVG
    Show commit body
    - STDDEV: use sample variance (n-1) and NULL when n<=1 to match PostgreSQL STDDEV()
    - L3 averages: add l3_n_efit, l3_n_refill, l3_n_cancel, l3_n_qt so merge uses COUNT(column)
      and matches AVG() (which ignores NULLs); migration + full/incremental/sync updated
    
    Made-with: Cursor
  1220. f520537
    fix: verify-refresh-equivalence snapshot via float8 to avoid Decimal decode errors
    Show commit body
    Made-with: Cursor
  1221. 81fa498
    EXECUTION_UNIVERSE_LIMIT, evaluation scaling metrics, pair_summary_24h FK fix
    Show commit body
    - Config: EXECUTION_UNIVERSE_LIMIT (env, default 200); use for pool/fetch in live + ingest. Test 200/300/400 without rebuild.
    - Live runner: EVALUATION_SCALING log (evaluation_symbol_count, evaluation_duration_ms, route_build_duration_ms, pipeline_duration_ms); route vs pipeline timing split.
    - pair_summary_24h FK: ensure_observation_run_on_pool(ingest, run_id) before persist when run created on decision DB.
    - LOGGING.md: EVALUATION_SCALING, EXECUTION_UNIVERSE_LIMIT.
    
    Made-with: Cursor
  1222. 58c9692
    scripts: L3 capacity test usage 5/50/400, target scale 400
    Show commit body
    Made-with: Cursor
  1223. c13576a
    chore: CHANGELOG + CHANGELOG_ENGINE for incremental refresh (9863d64)
    Show commit body
    Made-with: Cursor
  1224. 9863d64
    feat(refresh): incremental/watermark refresh for run_symbol_state (risk 3 closure)
    Show commit body
    - Add refresh_watermarks table (run_id, table_name, last_id); migration 20260313160000.
    - First refresh per run_id = full scan; subsequent = only id > watermark per raw table.
    - Merge deltas (counts + running avg/var for L2/L3) in SQL; set generation_id on all rows.
    - Log REFRESH_INCREMENTAL with delta_ticker_rows, delta_trade_rows, delta_l2_rows, delta_l3_rows.
    - Doc: REFRESH_INCREMENTAL_DESIGN (design + proof); CONTROLEPLAN risk 3 dicht on incremental.
    
    Made-with: Cursor
  1225. 927b1de
    fix: add BufRead import for resource_telemetry (Linux reader.lines())
    Show commit body
    Made-with: Cursor
  1226. 9b3de1a
    Docs: refresh bounded-proof, writer metrics, telemetry, L3 scale and multi-WS design
    Show commit body
    - REFRESH_COMPLEXITY, L3_SCHAALBEPERKINGEN: cap required, REFRESH_MAX_DURATION_SECS
    - REFRESH_INCREMENTAL_DESIGN.md: incremental refresh options (watermark/delta)
    - WRITER_PARALLEL_DESIGN.md: optional dedicated L3 / sharded writer
    - L3_MULTI_WS_INGEST_DESIGN.md: multi-WS ingest when Kraken symbol limit applies
    - LOGGING: WRITER_METRICS, resource telemetry and event-loop proxy
    
    Made-with: Cursor
  1227. 1f8c671
    Bottleneck work: refresh cap+timeout, writer metrics+L3 batch, telemetry, l3-subscribe-test
    Show commit body
    - Refresh: REQUIRE_INGEST_MAX_RUN_DURATION, REFRESH_MAX_DURATION_SECS (60s), ingest_runner exit when required and cap missing
    - Writer: WriterSender with pending count, WRITER_METRICS every 10s, L3 batching (flush at 50 or interval)
    - Telemetry: RESOURCE_TELEMETRY_INTERVAL_SECS, observability/resource_telemetry (RSS on Linux), spawn in ingest + live_runner
    - L3: l3-subscribe-test CLI (L3_SUBSCRIBE_TEST_SYMBOLS, L3_SUBSCRIBE_TEST_DURATION_SECS)
    
    Made-with: Cursor
  1228. e2288b3
    controleplan: fix-or-prove-close uitkomst na 15-min run (server 2d1cd83)
    Show commit body
    Beslistabel ingevuld: 1 dicht, 3 blocker (cap niet gezet), 4 dicht, 5 dicht, 11 dicht.
    Conclusie: Blocker(s) eerst — INGEST_MAX_RUN_DURATION_HOURS op server zetten.
    
    Made-with: Cursor
  1229. 2d1cd83
    fix-or-prove-close: env logging, ingest cap warn, evaluation/sync timing, controleplan
    Show commit body
    - main: log DECISION_DATABASE_URL and INGEST_MAX_RUN_DURATION_HOURS at startup (no silent default)
    - ingest_runner: warn when INGEST_MAX_RUN_DURATION_HOURS not set
    - REFRESH_COMPLEXITY: recommend cap 6/24h for production
    - live_runner: EVALUATION_CYCLE_DURATION_MS and SYNC_LAG_MS for contention/sync-lag measurement
    - CONTROLEPLAN_SYSTEEMRISICOS: add Fix-or-prove-close uitkomst (drempels, beslistabel, conclusie)
    - scripts/check_stale_decision_logs.sh: check gen_mismatch and ROUTE_FRESHNESS in logs (risico 1)
    
    Made-with: Cursor
  1230. 8e48016
    Docs: dual-DB epoch dual-write fix + 15min validation passed (section E)
    Show commit body
    Made-with: Cursor
  1231. 70f4c25
    Dual-DB gate: use synced cycle for gate (avoid ingest overwrite race); log visible=cycle for validation
    Show commit body
    Made-with: Cursor
  1232. 7f31d23
    Generation gate: use visible_gen from right after sync (avoid ingest overwrite race)
    Show commit body
    Made-with: Cursor
  1233. a75c126
    Fix dual-write epoch/snapshot: ensure observation_run on decision DB (FK); log dual-write errors
    Show commit body
    Made-with: Cursor
  1234. b3b1a4e
    Rapport: dual-DB uitvoering — tweede instance gebouwd, 15min validatie, resultaat
    Show commit body
    Made-with: Cursor
  1235. e380072
    Script: set decision instance password from DATABASE_URL (server-side)
    Show commit body
    Made-with: Cursor
  1236. 857f458
    Dual-DB: plan tweede instance, doc-definitie, validatiescript sync/generation checks
    Show commit body
    Made-with: Cursor
  1237. 4b23919
    Docs: validatierapport single/dual-DB; DOC_INDEX, CHANGELOG, README bijgewerkt
    Show commit body
    Made-with: Cursor
  1238. a653d6f
    Validatie: deprecated-check alleen bij groei tussentijds; rapport B/C ingevuld; scriptfix
    Show commit body
    Made-with: Cursor
  1239. edb24d7
    Refresh-schaalbaarheid: run-duur cap (INGEST_MAX_RUN_DURATION_HOURS) + 15min validatiescript + db_full_reset_ingest
    Show commit body
    Made-with: Cursor
  1240. 1a845a0
    Docs: flow schema's tonen dubbele DB (DB Ingest vs DB Decision) expliciet
    Show commit body
    Made-with: Cursor
  1241. b3dfee8
    Docs SSOT: state-first, partition, generation in ENGINE_SSOT, DOC_INDEX, ARCHITECTURE, CHANGELOG_ENGINE, LIVE_RUNBOOK, LOGGING, README
    Show commit body
    Made-with: Cursor
  1242. 5cc8322
    DB architecture: partition cutover, generation contract, sync gate, refresh O(rows) doc
    Show commit body
    - Raw ingest: L3 + ticker/trade/l2 cutover to partitioned tables (migrations 20260313120000–40000)
    - run_symbol_state: generation_id + sequence; RefreshOutcome; state_generation_id; sync copies gen
    - live_runner: cycle_generation_id, INGEST_DECISION_SYNC_VISIBLE log, EXECUTION_BLOCKED_GENERATION_MISMATCH gate
    - docs: REFRESH_COMPLEXITY_AND_GENERATION (O(rows) proof), EXECUTION_REPORT/DB_ARCHITECTURE updated
    - CHANGELOG: 2025-03-13 partition cutover, generation contract, sync gate
    
    Made-with: Cursor
  1243. a240cc0
    docs: add PG DBA autovacuum proposal (table settings, l3 policy, refactor block)
    Show commit body
    Made-with: Cursor
  1244. 813ad46
    refactor(db): state-first query boundary + SQL type safety in live paths
    Show commit body
    - Explicit SQL casts: ::bigint for COUNT/MAX(epoch_id); ::text for aggregates
      mapped to Decimal (ingest_epoch, proof_runner, exposure_reconcile,
      deterministic_proof, stats_queries).
    - Remove direct NUMERIC→Decimal in critical/live paths: use ::text + parse
      in exposure_reconcile, fills_ledger, positions, stats_queries.realized_pnl,
      proof_runner MAX(expected_edge_bps).
    - Add STATE_FIRST_AND_SQL_TYPE_SAFETY_DELIVERABLE.md (live paths, casts,
      Decimal fixes, error-family confirmation).
    
    Made-with: Cursor
  1245. 94397fa
    fix(db): run_symbol_state NUMERIC decode — read as text, parse to Decimal (avoid NaN/overflow)
    Show commit body
    Made-with: Cursor
  1246. 38e263f
    chore(dba): add PostgreSQL DBA health scan script and report template
    Show commit body
    Made-with: Cursor
  1247. 7b7b36e
    fix(db): cast SUM() to bigint in run_raw_counts_from_state (Option<i64> vs NUMERIC)
    Show commit body
    Made-with: Cursor
  1248. bef764e
    feat(execution): L3 soft at system level — l3_integrity out of system_live_ready AND
    Show commit body
    Deel 2: L3 no longer systemically blocks execution; symbol-level safety unchanged.
    
    - data_integrity: system_live_ready() no longer ANDs l3_integrity
    - l3_integrity still computed and logged in DATA_INTEGRITY_MATRIX
    - hard_blocked symbols still filtered from exec_allowed (symbol-level)
    - Verified: no second systemic L3 choke in codebase
    
    See docs/ROUTE_HOT_PATH_STATE_PROOF.md (Deel 2 section).
    
    Made-with: Cursor
  1249. 1675ff9
    feat(route): hot path state-only — analyze_run_from_state, no raw/legacy readers
    Show commit body
    Deel 1: Route hot path depends only on run_symbol_state + run_by_id.
    
    - stats_queries: RunSymbolStateRow + run_symbol_state_rows_for_run (single SELECT)
    - current_run_analysis: analyze_run_from_state (state rows + run_by_id only; same formulas as provisional)
    - route_selector: run_v2_route_analysis uses analyze_run_from_state (no fallback to analyze_run)
    
    Readers no longer in route hot path:
      pair_summaries_for_run, l2_symbol_stats_for_run, l3_symbol_stats_for_run,
      symbol_trade_counts, symbol_avg_spread_l2
    
    See docs/ROUTE_HOT_PATH_STATE_PROOF.md.
    
    Made-with: Cursor
  1250. 0679f49
    feat(route_engine): exit regime architecture — kapitaalbescherming primair
    Show commit body
    - ExitRegime enum + exit_mode_to_regime; select_allowed_exit_regimes(route,horizon,features,path)
    - RouteCandidate: exit_regime, capture_factor; filter matrix on allowed regimes
    - EXIT_REGIME_ALLOWED/REJECTED/CHOSEN/EXPECTANCY_APPLIED logging
    - Pipeline: v2_exit_strategy_from_route(..., exit_regime); no legacy fallback
    - docs/EXIT_REGIME_ARCHITECTURE.md + CHANGELOG
    
    Made-with: Cursor
  1251. 28fc6e7
    docs: CHANGELOG 2025-03-13 Route Engine Evolution (Live Edge Machine)
    Show commit body
    - Added/Changed/Internal per module (l3_quality, run_metrics, fill_probability,
      route_expectancy, expected_path, route_selector, sizing, strategy_pipeline,
      universe, ingest_runner, live_runner, readiness_gate, run_symbol_state, data_integrity)
    - Validatie: cargo check + cargo test (29 passed)
    
    Made-with: Cursor
  1252. 6b47325
    feat(route): Fase 5 run metrics, ROUTE_RUN_METRICS log, cost/ROI stubs, run_symbol_state refresh log
    Show commit body
    - run_metrics: RouteRunMetrics, market_cost_from_counts/edge_roi stubs
    - route_selector: aggregate and log ROUTE_RUN_METRICS (valid_route_count, avg_edge_velocity, neutral_path_ratio, l3_quality_avg)
    - run_symbol_state: RUN_SYMBOL_STATE_REFRESH duration log; l2 CTE filter spread_bps IS NOT NULL
    
    Made-with: Cursor
  1253. 5a66d1e
    feat(route): Fase 4 opportunity_score in universe, refresh timing logs
    Show commit body
    - SymbolQualityInputs.opportunity_score; L3Ingest/Execution layer boost
    - ingest_runner/live_runner: opportunity_score: None in quality map
    - UNIVERSE_REFRESH_STATE_TIMING logs (refresh_ms, state_reads_ms, total_ms, symbol_count)
    
    Made-with: Cursor
  1254. 5a3c006
    feat(route): Fase 3 dynamic sizing (size_quote_v2), edge velocity in pipeline
    Show commit body
    - sizing: size_quote_v2 with sigmoid(edge) * confidence * liquidity_score * capital_availability
    - strategy_pipeline v2: use size_quote_v2 for route sizing (expected_net_edge_bps, confidence, spread_bps)
    
    Made-with: Cursor
  1255. 0fb9229
    feat(route): Fase 1 L3 soft + Fase 2 path model
    Show commit body
    Fase 1 — L3 economisch veilig:
    - l3_quality module: compute_l3_quality_score (floor when no L3)
    - MarketFeatures.l3_quality_score, fill_prob Hybrid/Fallback, floor 0.05
    - route_expectancy: confidence *= l3_quality_score, edge lerp penalty, no L3 hard block
    - validate_candidate: confidence/fill_prob soft gate (no reject)
    - data_integrity doc: L3 does not invalidate routes
    
    Fase 2 — Direction & path:
    - DirectionalBias enum, ExpectedPath base_move/upside_tail/downside_tail/move_confidence
    - expected_path: directional_bias_from_features, horizon elasticity (no PathNotHorizonConsistent reject)
    - route_expectancy: time_efficiency = move/sqrt(duration), edge_velocity_bps_per_sec
    
    Made-with: Cursor
  1256. 4dd0228
    fix: L2 count in run_symbol_state includes all rows (remove spread_bps IS NOT NULL filter)
    Show commit body
    Made-with: Cursor
  1257. e2be75d
    fix: refresh run_symbol_state before L3 safety checks (resync, hard-block, ExitOnly)
    Show commit body
    Made-with: Cursor
  1258. 873f915
    db: add run_symbol_state to retention cleanup and CLI output
    Show commit body
    Made-with: Cursor
  1259. 334f9fb
    execution: wire run_symbol_state refresh and state reads in live and ingest runners
    Show commit body
    Made-with: Cursor
  1260. 608d3bc
    db: add refresh_run_symbol_state and from_state read functions
    Show commit body
    Made-with: Cursor
  1261. 6082039
    db: add run_symbol_state table and index (hot-path state)
    Show commit body
    Made-with: Cursor
  1262. af6ec1f
    DB optimization: raw table indexes, retention cleanup, drop execution_event_buffer, truncate script
    Show commit body
    Made-with: Cursor
  1263. 11e40f8
    Add L3 netto edge report: SQL queries, shell script, rapport template
    Show commit body
    Made-with: Cursor
  1264. 0e4cb70
    route-engine: economic calibration — move distribution, relative move, vol-scale, momentum, capital velocity
    Show commit body
    - ROUTE_MOVE_DISTRIBUTION: per (route_type, horizon) p50/p75/p90/p95/p99/avg (confidence>0.10)
    - Relative move: move_valid = expected_move_bps/fee_bps > 0.85; path MIN_MOVE_BPS check removed
    - Vol-scaled move: vol_reference = run p75 vol_proxy; vol_scale clamp(0.7,2.5); all builders
    - Momentum boost 1.15–1.35 for Breakout/Pullback when trend_strength/vol_expansion/trade_density above thresholds
    - time_adjusted_score *= sqrt(capital_velocity_norm); capital_velocity = move_bps/hold_secs
    - ROUTE_ECONOMIC_PRESSURE_STATS: valid_move_ratio, avg_relative_move_score, top_relative_move_symbol
    
    Made-with: Cursor
  1265. 900cdb2
    route-engine: Breakout fallback-band confidence uplift
    Show commit body
    - When 0.20 < vol_expansion <= 0.35, trend_strength > 0.20 and direction set:
      add BREAKOUT_FALLBACK_CONFIDENCE_UPLIFT (0.10) so confidence reaches
      MIN_CONFIDENCE and paths pass path_confidence_too_low -> move_below_fees or valid
    - No move/other routes/MIN_MOVE_BPS/safety changes
    
    Made-with: Cursor
  1266. 483786a
    route-engine: BREAKOUT direction trace (guarded) + direction fallback
    Show commit body
    - BREAKOUT_DIRECTION_TRACE: log symbol, micro_bps, trend_strength, vol_expansion,
      builder/validated direction, rejection (guarded by ROUTE_V2_PATH_DIAG, max 8 per run)
    - reset_breakout_trace_count() at start of run_v2_route_analysis when diag on
    - Breakout direction fallback: vol_expansion in (0.20, 0.35] and trend_strength > 0.20
      -> set direction from micro_direction/micro_bps (breakout_direction_from_features)
    - No move/confidence/safety changes; only Breakout direction
    
    Made-with: Cursor
  1267. 0dcdb8d
    tune(route_engine): Breakout mid-band (0.35–0.5) base_move +20% (0.9 -> 1.08)
    Show commit body
    Single small step: more paths clear move_below_fees. Diag stays on for calibration.
    
    Made-with: Cursor
  1268. 41e837f
    fix(route_engine): Breakout vol_expansion threshold 0.5 -> 0.35 for direction/move/confidence
    Show commit body
    So more pairs get Up/Down instead of Neutral; diag showed 100% Neutral for breakout.
    
    Made-with: Cursor
  1269. 8765fbd
    feat(route_engine): v2 path diag guard + direction stats + almost-tradable ranking + route_family impact
    Show commit body
    - ROUTE_V2_PATH_DIAG env guard: ROUTE_PATH_DIRECTION_STATS and
      ROUTE_ALMOST_TRADABLE_RANKING (top 20) only when guard set
    - V2_ROUTE_ANALYSIS_COMPLETE extended with route_family counts
      (pullback, breakout, pump_fade, passive) for impact reporting
    - Path heuristic: micro_direction band 0.5 -> 0.25 bps so more
      paths get Up/Down, fewer path_not_directional
    
    Made-with: Cursor
  1270. ea159d1
    feat(route_engine): Route Decision Engine v2 — market-first, path-first
    Show commit body
    Replace strategy-first economic decisioning with market-first route
    engine. Per pair: market features → expected path per horizon → route
    candidates (route × horizon × entry × exit) → expectancy → winner or
    explicit NoTrade.
    
    New modules (src/route_engine/, 7 files, 1282 LOC total):
    - types.rs: RouteType, EntryMode, ExitMode, RouteHorizon, ExpectedPath,
      RouteCandidate, RouteSelection, V2RouteReport
    - market_features.rs: MarketFeatures from CurrentRunMarketRow
    - expected_path.rs: first-class ExpectedPath with path_source,
      heuristic builders per (route_type, horizon), validation
    - route_expectancy.rs: expected_net_edge_bps, time_adjusted_score,
      full cost decomposition, max_hold_secs invariant
    - route_selector.rs: candidate matrix, winner selection, orchestration
    - shadow.rs: mandatory counterfactual logging with numeric why_lost
      deltas (alt vs winner edge/confidence/score)
    - mod.rs: module wiring
    
    Integration:
    - live_runner.rs: v2 route analysis replaces readiness+v1 pipeline
    - strategy_pipeline.rs: run_strategy_pipeline_v2 bridges v2 routes
      to existing execution layer via legacy strategy mapping
    
    Shadow output shows winner + all alternatives with concrete numeric
    differences explaining why each alternative lost.
    
    Docs: design document + 12 investigation reports + changelog updates.
    Made-with: Cursor
  1271. 650a909
    fix(readiness): Volume strategy EdgeNegative bypass + check_entry_readiness lookup
    Show commit body
    - readiness_gate: Volume strategy was blocked by general EdgeNegative
      check before its own -5 bps floor could apply. Now excluded from
      general EdgeNegative (mirrors existing SurplusBelowFloor exemption).
    - check_entry_readiness: lookup found first PairReadinessRow for symbol
      regardless of strategy, causing StrategyMismatch. Now matches both
      symbol AND selected_strategy.
    
    Made-with: Cursor
  1272. 939d1fd
    fix(execution): max_epoch_age_secs race with evaluation_interval_secs
    Show commit body
    max_epoch_age_secs was (freshness*2).max(300) which equals the 300s evaluation
    interval — leaving zero margin for processing jitter. Epochs produced every 300s
    are 300-310s old at evaluation time, causing global_liveness=false permanently.
    
    Changed to (freshness*2).max(evaluation_interval + 120), giving 420s with default
    config. This applies to both the normal and execution-only loops.
    
    Made-with: Cursor
  1273. a25b35c
    epoch validity: exclude hard_blocked symbols from entry-validity set
    Show commit body
    Analogous to the earlier pinned exclusion: symbols that are hard_blocked
    (permanently lacking L3 data because the exchange does not provide L3 for them)
    are already excluded from trading by the per-symbol safety mechanism. They should
    not count against the 90% entry-validity threshold.
    
    Entry-validity set is now: execution_symbols \ (pinned ∪ hard_blocked).
    
    Without this fix, 4/23 non-pinned execution symbols with permanent l3_count=0
    (ATH/USD, AVNT/USD, CC/USD, DOT/USD — Kraken does not provide L3 for them)
    caused every epoch to be degraded (17.4% > 10% tolerance), keeping the engine
    permanently in ExitOnly mode even though system_live_ready=true.
    
    EPOCH_VALIDITY_COMPUTED now logs hard_blocked_excluded_count alongside
    pinned_excluded_count and entry_validity_count.
    
    Made-with: Cursor
  1274. f47cac4
    fix(execution): l3_integrity checked global hard_blocked list instead of exec-set intersection
    Show commit body
    Two bugs fixed:
    
    1. `list_hard_blocked_symbols()` returns ALL globally hard_blocked symbols (from all
       runs/processes). The code passed `!hard_blocked.is_empty()` as `hard_blocked_any_exec_symbol`,
       meaning any hard_blocked symbol anywhere — even outside the execution set — made
       l3_integrity=false and blocked the entire engine. Fixed: only count symbols actually
       present in the execution set (BTreeSet::remove returns true only if present).
    
    2. l3_integrity was binary: any single hard_blocked exec symbol → system down.
       This is overly aggressive when a minority of symbols permanently lack L3 data on
       the exchange (Kraken does not provide L3 for all pairs). Hard_blocked symbols are
       already removed from exec_allowed and will not trade; universe_viability separately
       checks if enough symbols remain. Changed to fraction-based: l3_integrity=false only
       when >50% of exec symbols are hard_blocked (systemic L3 failure), not when a few
       illiquid symbols lack L3.
    
    Observability: DATA_INTEGRITY_MATRIX now logs hard_blocked_exec_count and
    total_exec_before_filter. EXECUTION_SYMBOL_FILTERED only logged for symbols
    actually in the execution set.
    
    Made-with: Cursor
  1275. cea8bcc
    docs: CHANGELOG_ENGINE correct commit hash for observability export
    Show commit body
    Made-with: Cursor
  1276. 61f96fa
    Observability read-model export voor KapitaalBot-Website
    Show commit body
    - docs/OBSERVABILITY_SNAPSHOT_CONTRACT.md: versioned contract 1.0
    - src/db/read/observability_queries: order/fill/regime/strategy counts, latest_epoch_summary
    - src/observability: snapshot DTOs + export naar JSON (public_* families)
    - CLI: export-observability-snapshots (OBSERVABILITY_EXPORT_DIR)
    - CHANGELOG + CHANGELOG_ENGINE bijgewerkt
    
    Website BFF leest alleen deze snapshots; geen directe DB-queries.
    
    Made-with: Cursor
  1277. a6fa319
    epoch validity: entry-validity set = execution \ pinned (pinned excluded from 90% rule)
    Show commit body
    - compute_epoch_criteria_and_status uses only non-pinned execution symbols for 90% check
    - Pinned stay in scope/snapshot/universe; illiquid pinned no longer block entry gate
    - EPOCH_VALIDITY_COMPUTED log: entry_validity_count, pinned_excluded_count, status
    - symbol_count in EpochCriteria = entry-validity set size
    - docs/EPOCH_ENTRY_VALIDITY_SET.md: design + live validation checklist
    
    Made-with: Cursor
  1278. 70857f2
    feat(execution): explicit epoch binding policy (LatestValid / PreferCurrentLineage) + EXECUTION_EPOCH_BOUND logging
    Show commit body
    Made-with: Cursor
  1279. 5086221
    docs: TRADABLE_COUNT_NEXT_STEPS + run71 degraded symbols diagnostic script
    Show commit body
    Made-with: Cursor
  1280. f6a603b
    feat(epoch): valid epoch when ≥90% symbols meet criteria (fix run 71 degraded)
    Show commit body
    Made-with: Cursor
  1281. 29e4117
    chore: CHANGELOG_ENGINE commit hash for execution-only fix
    Show commit body
    Made-with: Cursor
  1282. 2e525b4
    fix(config+execution): EXECUTION_ONLY=1 parse + first-binding lineage break
    Show commit body
    - config: parse_bool_env() accepts 1/0/true/false so systemd EXECUTION_ONLY=1
      enables split mode (no own ingest, bind to ingest epochs).
    - live_runner: lineage_break_detected only when previous lineage differs from
      current; set last_bound_lineage_id after every binding. Fixes spurious
      ExitOnly on first epoch binding.
    
    CHANGELOG + CHANGELOG_ENGINE updated.
    
    Made-with: Cursor
  1283. 158d285
    chore: correct commit hash in CHANGELOG_ENGINE
    Show commit body
    Made-with: Cursor
  1284. 9a1a48c
    fix(analysis): capturable move strategy-aware in readiness report
    Show commit body
    CapturableMoveInputs used pre-loop maker-oriented expected_move_bps for
    all strategies; Momentum received systematically underestimated capturable
    move -> SurplusBelowFloor/EdgeNegative. Now: capturable move computed per
    strategy with strategy_move_bps inside the loop; NoTrading branch keeps
    maker expected_move_bps.
    
    CHANGELOG + CHANGELOG_ENGINE updated.
    
    Made-with: Cursor
  1285. 13781a8
    fix(fill_prob): calibrate for Kraken liquidity levels
    Show commit body
    The fill probability model was calibrated for high-volume exchanges
    (5 trades/sec normalization, 40s fill reference). On Kraken, even
    liquid pairs like EUR/USD only do 0.5 trades/sec, giving fill_prob
    of 7% — making edge permanently negative.
    
    Changes:
    - Fallback density normalization: 5.0 → 0.5 (Kraken-realistic)
    - L3 fill time reference: 40s → 300s (maker orders rest for minutes)
    
    EUR/USD expected fill_prob: 0.07 → 0.70 (with trade_density=0.53)
    
    Made-with: Cursor
  1286. 0dcd344
    fix(edge): strategy-aware edge and expected_move calculations
    Show commit body
    The economic model was originally built for market making (maker) and
    applied fill_probability uniformly to all strategies. This penalized
    momentum/taker strategies where fill is instant (market order).
    
    Changes:
    - Momentum edge: no fill_prob discount, costs = 2*taker_fee + slippage
    - Maker edge: fill_prob * (move - maker_fee - taker_fee - slippage)
    - Momentum expected_move: vol*1.2 + micro*1.0 + spread*0.1 (directional)
    - Maker expected_move: spread*0.8 + vol*0.4 + micro*0.6 (spread capture)
    
    Made-with: Cursor
  1287. 8693309
    feat(warmup): skip 60s warmup when ingest is already producing epochs
    Show commit body
    If a valid ingest epoch exists in the DB, the execution runner now
    reduces warmup from 60s to 5s (WS connect only) and uses the ingest
    run_id for initial universe selection data. This avoids redundant
    waiting when run-ingest is already running continuously.
    
    Made-with: Cursor
  1288. 6ba5998
    fix(fees): parse Kraken TradeVolume nested fee object correctly
    Show commit body
    The TradeVolume API returns fees as nested objects:
      fees["XETHZUSD"]["fee"] = "0.3500" (percentage)
    
    The code called as_str() on the object directly, which returned
    None, silently falling back to hardcoded defaults (20/26 bps).
    Actual account fees (18/35 bps) were never read from the API.
    
    Fix: navigate into nested object with .get("fee"), parse the
    percentage string, convert to bps with * 100 (not * 10_000).
    
    Made-with: Cursor
  1289. 1a3909e
    fix: readiness gate economic model bugs + complete exposure protection logging
    Show commit body
    Root cause fixes for 0/200 tradable pairs:
    
    1. Regime metric mapping: spread_stability measured width not stability
       (divisor 20→80), midprice_volatility threshold too aggressive (divisor
       10→40). Most altcoins falsely tagged CHAOS.
    
    2. Slippage volatility multiplier: vol_proxy * 5.0 used raw bps input
       (e.g. 15 bps → 75 bps slippage). Changed to * 0.3 for sane values.
    
    3. Edge formula: fill_prob * move - costs → fill_prob * (move - costs).
       Costs only incurred when filled, not every attempt.
    
    4. Capturable move: removed fill_probability discount (double-counted
       with edge formula). Capturable move = execution quality given fill.
    
    5. Bootstrap fees: 35/35 → 25/40 bps (match Kraken base tier).
    
    Also includes previously uncommitted exposure protection causal logging
    (exit_lifecycle, exposure_reconcile, runner, ws_handler).
    
    DB cleanup: zeroed phantom positions (ETH/USD, EUR/USD) and expired
    stale orders that caused "Insufficient funds" protection failures.
    
    Made-with: Cursor
  1290. 475f5ab
    feat: causal exposure validation + maker/taker route evaluation
    Show commit body
    - Exposure: validate per-symbol UNPROTECTED→STARTED→outcome chain in validate script
    - Maker/taker: evaluate both execution modes per route, choose on net edge
    - Logging: ROUTE_MAKER_EVALUATED, ROUTE_TAKER_EVALUATED, ROUTE_EXECUTION_MODE_CHOSEN
    - slippage_estimator: estimate_slippage_for_entry_mode(maker)
    - readiness_gate: slippage_for_entry_mode()
    - EDGE_NEGATIVE_HARD_BLOCK uses chosen_edge_bps (max of maker/taker)
    
    Made-with: Cursor
  1291. e3554fe
    feat(fees): live fee model via Kraken TradeVolume API
    Show commit body
    STAP 1 — Live fee source:
    - auth_rest: get_trade_volume() via POST /0/private/TradeVolume
    - LiveFeeProvider: fetch maker/taker fees, volume_30d from account
    - Refresh at startup + periodiek (24h)
    - Fallback: cached of bootstrap met expliciete FEE_MODEL_FALLBACK_USED log
    
    STAP 2 — Fees als pure input:
    - FeeTier.fee_source (live|cached|bootstrap)
    - ROUTE_FEE_PROFILE: fee_entry_bps, fee_exit_bps, total_fee_bps, fee_source
    - ROUTE_EDGE_BREAKDOWN: fee_source toegevoegd
    - tier_from_provider_or_bootstrap() i.p.v. hardcoded current_fee_tier
    
    STAP 4 — Traceability:
    - FEE_MODEL_REFRESH_STARTED, FEE_MODEL_REFRESHED, FEE_MODEL_REFRESH_FAILED
    - FEE_STATE_ACTIVE, FEE_DIFFERENTIAL
    - log_fee_state_active() bij startup
    
    STAP 5 — Geen hardcoded defaults als waarheid:
    - Bootstrap (35 bps) alleen bij no prior fetch, met expliciete log
    - Alle fee-consumers via LiveFeeProvider of tier_from_provider_or_bootstrap
    
    STAP 6 — Server validatie:
    - validate_live_engine_server.sh: FEE_STATE_ACTIVE vereist
    - Fee model counters in output
    
    Made-with: Cursor
  1292. 1d8155a
    feat: auto-protect unprotected exposure with real stop/market orders
    Show commit body
    - Add protection_flow.rs: places emergency stop-loss (with market fallback)
      for any unprotected position; persists order to execution_orders so
      symbol-execution lock detects it immediately.
    - Startup reconcile (both live and execution-only paths) now calls
      protect_exposure() for each unprotected position instead of only logging.
    - Periodic reconcile returns unprotected records; live_runner triggers
      protect_exposure() for any newly detected unprotected positions.
    - detect_partial_fill_protection updated: checks for existing exit orders
      and logs EXPOSURE_PROTECTION_CONFIRMED or EXPOSURE_PROTECTION_STARTED
      (reason=exit_lifecycle_triggered) accurately.
    - New log markers: EXPOSURE_PROTECTION_STARTED, EXPOSURE_PROTECTION_CONFIRMED,
      EXPOSURE_PROTECTION_FAILED, EXPOSURE_PROTECTION_FALLBACK_MARKET,
      EXIT_PLAN_RECONCILED, EMERGENCY_STOP_PERSISTED.
    
    Made-with: Cursor
  1293. 3db29f2
    feat(safety): proof mode isolation + exposure auto-protection + symbol lock
    Show commit body
    STAP A — Proof mode isolation:
    - PROOF_MODE_STATE log at start of every pipeline run (active=true/false)
    - PROOF_MODE_BYPASS_APPLIED at system gate and per-pair gate
    - EDGE_NEGATIVE_HARD_BLOCK: hard guard — when proof_mode=false, any route with
      expected_edge_bps < 0 is blocked before plan_execution; log + Skip outcome
    
    STAP B — Exposure auto-protection:
    - exposure_reconcile.rs: new module
      - reconcile_exposure_at_startup(): load positions, check exit orders,
        log EXPOSURE_DETECTED / EXPOSURE_UNPROTECTED / EXPOSURE_PROTECTION_CONFIRMED
        / RECONCILE_POSITION_STATE
      - check_symbol_execution_lock(): per-symbol guard before new entry;
        blocks if active exit orders, open entry orders, or unprotected net position;
        logs SYMBOL_EXECUTION_LOCK_ACQUIRED / SYMBOL_EXECUTION_LOCK_RELEASED
      - detect_partial_fill_protection(): called on every fill (incl. partial);
        logs PARTIAL_FILL_PROTECTION_UPDATE, EXPOSURE_UNPROTECTED if no exit orders
      - run_periodic_reconcile(): lightweight periodic position/exit order mismatch check
    
    - live_runner.rs:
      - reconcile_exposure_at_startup() called at startup before evaluation loop
      - check_symbol_execution_lock() called before every order submit
      - run_periodic_reconcile() called every 10 evaluations
    
    - ws_handler.rs:
      - detect_partial_fill_protection() called on every entry fill (partial + full)
      - ORDER_FILL log extended with partial=true/false
    
    Logging additions:
      PROOF_MODE_STATE, PROOF_MODE_BYPASS_APPLIED, EDGE_NEGATIVE_HARD_BLOCK
      EXPOSURE_DETECTED, EXPOSURE_UNPROTECTED, EXPOSURE_PROTECTION_STARTED
      EXPOSURE_PROTECTION_CONFIRMED, PARTIAL_FILL_PROTECTION_UPDATE
      RECONCILE_POSITION_STATE, SYMBOL_EXECUTION_LOCK_ACQUIRED, SYMBOL_EXECUTION_LOCK_RELEASED
    
    Made-with: Cursor
  1294. 3eb3a81
    fix(pipeline): ROUTE_SELECTED shows effective proof-capped size and proof_mode flag
    Show commit body
    Made-with: Cursor
  1295. 457c92f
    fix(pipeline): cap proof mode trade size + log ROUTE_PROOF_ENTRY_FORCED on normal path
    Show commit body
    Previously, when proof mode bypassed readiness checks and the normal risk+queue
    path returned Execute, the full sizing (e.g. 20 EUR) was used instead of the
    proof cap (ROUTE_PROOF_MAX_ORDER_QUOTE). The ROUTE_PROOF_ENTRY_FORCED log was
    only emitted in the fallback Skip→Execute conversion, not here.
    
    Fix:
    - Introduce `proof_mode_active` flag in execution loop (proof.enabled && execute_count==0)
    - Cap `effective_size_dec` to `route_proof.max_order_quote` when proof_mode_active
    - Emit ROUTE_PROOF_ENTRY_FORCED immediately when Execute is produced with proof cap
    - This ensures ROUTE_PROOF_MAX_ORDER_QUOTE=5 is always respected regardless of path
    
    Made-with: Cursor
  1296. a1cc39d
    feat(engine): PAIR_FILTER_RESULT, INGEST_WARMUP_STATE, proof auto-disable, latency split
    Show commit body
    strategy_pipeline.rs:
    - PAIR_FILTER_RESULT log for every pair entering/failing pair_allowed_for_ranking
      (result=allowed|blocked, fill_time_ms, spread_bps, strategy)
    - RouteRecord: add spread_net_bps field (pre-computed per strategy)
    - ROUTE_EDGE_BREAKDOWN: add spread_net_bps, spread_role, break_even_shortfall_bps
      per route (enables spread attribution in no-trade forensics)
    
    ingest_runner.rs:
    - INGEST_WARMUP_STATE state=warming_up logged at WS start
    - INGEST_WARMUP_STATE state=live_ready|degraded_warmup logged after warmup period
    - INGEST_WARMUP_STATE repeated in heartbeat when valid_epochs=0
      (explicitly distinguishes warming-up from stream_unhealthy)
    - INGEST_STREAM_HEALTH extended with warmup_state field
    
    live_runner.rs:
    - ROUTE_PROOF_DISABLED: auto-disable proof mode after first successful order submit
      (proof_lifecycle_proven flag; subsequent cycles run without forcing)
    - PIPELINE_LATENCY_PROFILE: log pipeline logic latency (ms) per evaluation
      (splits pure logic cost from DB/IO; complement to ROUTE_LATENCY_PROFILE)
    - proof_total_forced_trades counter for diagnostic reporting
    
    Made-with: Cursor
  1297. 30df0f6
    feat(ingest): IngestStreamHealth with gap detection and lineage recovery
    Show commit body
    - Add IngestStreamHealth struct tracking:
      - last_message_ts: most recent ticker/trade timestamp
      - consecutive_valid_epochs: epoch continuity counter
      - gap_detected: boolean flag for active gap state
      - gap_start_ts: timestamp when gap started
      - reconnect_count: reconnect event counter
    
    - At each heartbeat, query DB for last ticker/trade timestamp
      and compute last_msg_age_secs to assess stream liveness
    
    - Log INGEST_STREAM_HEALTH every heartbeat:
      healthy, last_message_age_secs, valid_epochs, gap, reconnects
    
    - Log INGEST_GAP_DETECTED when last_msg_age_secs > 120
    - Log INGEST_LINEAGE_RECOVERED when stream recovers after gap
    - Track consecutive_valid_epochs in stream_health (reset to 0 on EPOCH_DEGRADED)
    
    DATA_FRESHNESS_STATE can now rely on INGEST_STREAM_HEALTH markers
    instead of arbitrary time-window heuristics
    
    Made-with: Cursor
  1298. 027e9b9
    feat(engine): route transparency, pair classifier, ORDER_ID_CORRELATION_FALLBACK
    Show commit body
    pair_classifier.rs (new):
    - PairClass enum: MajorFx, CryptoMajor, CryptoMid, Altcoin, Unknown
    - classify_pair(): categorizes symbols by base asset
    - edge_model_justification(): regime+strategy context for each pair class
    
    strategy_pipeline.rs:
    - PAIR_CLASSIFICATION log per selected route
    - EDGE_MODEL_JUSTIFICATION log per selected route
    - ROUTE_DECISION_SUMMARY: full economics (edge, fill_prob, fees, spread_net, slippage)
    - ROUTE_PARAMETER_PROFILE: SL/TP/time_stop, maker_or_taker, spread breakdown per route
    - COUNTERFACTUAL_BEST_ROUTE: top 5 non-executed routes when execute_count=0
    - RouteRecord extended with entry_strategy, exit_strategy, spread_bps fields
    
    runner.rs:
    - ORDER_ID_CORRELATION_FALLBACK_USED logged when exchange bridge not found
      (HandleResult::Unknown → bridge correlation failed → event was unroutable)
    
    Made-with: Cursor
  1299. 1022953
    fix(engine): pair filter removed spread gate, instrument-driven exit price precision
    Show commit body
    pair_allowed_for_ranking refactored:
    - Remove spread_bps > 2.0 global gate (spread is route feature, not global blocker)
    - Remove fill_time=None blocking (None = no L3 data = not technically blocked)
    - Only hard blocker: fill_time >= 60s when explicitly known
    - All 42 L3 symbols including EUR/USD and crypto-majors now pass this filter
    
    exit_lifecycle.rs precision fix:
    - Replace hardcoded 5-decimal rounding with get_instrument_constraints() per symbol
    - normalize_order() uses price_increment from Kraken instrument metadata
    - InstrumentConstraints::fallback_5dp() added for resilience when API call fails
    - Log PRECISION_SOURCE=instrument_metadata and PRECISION_NORMALIZATION_APPLIED per exit
    
    Made-with: Cursor
  1300. 576dfea
    fix(engine): structural data freshness, blocker masking fix, spread strategy-aware
    Show commit body
    Data freshness (structural, not workaround):
    - Remove MAX_RUN_AGE_FOR_READINESS_SECS (48h run-age logic removed)
    - Add LIVE_DATA_MAX_AGE_SECS=60 based on actual last_ticker_ts/last_trade_ts
    - Remove live_freshness_secs parameter from run_readiness_analysis_for_run
    - Log DATA_FRESHNESS_STATE with mode=live/historical on every readiness check
    
    Blocker masking fix (check_entry_readiness):
    - Return actual primary_blockers.first() instead of hardcoded DataStale
    - Fallback to DataStale only when report.data_stale=true or primary_blockers is empty
    - Prevents economic blockers (RegimeChaos, EdgeNegative) from showing as DataStale
    
    Spread strategy-aware (cost_breakdown.rs):
    - Rename spread_cost_bps → spread_net_bps in CostBreakdown
    - Add spread_net_for_strategy(): Liquidity/Volume=-spread*0.5 (income), Momentum=+spread*0.5 (cost)
    - strategy_readiness_report uses spread_net_for_strategy per candidate
    - Log SPREAD_ROLE with spread_bps, spread_net_bps, role per evaluation
    
    live_runner.rs updated to call run_readiness_analysis_for_run without freshness parameter.
    
    Made-with: Cursor
  1301. df17f9e
    fix(engine): strategy-aware entry filter, slippage maker/taker split, sizing ratios
    Show commit body
    entry_filter.rs:
    - Add max_spread_bps and spread_is_income fields to EntryFilterConfig
    - Add entry_filter_for_strategy(): Momentum uses spread as UPPER bound (taker cost),
      Liquidity/Volume use it as LOWER bound (maker income)
    - Momentum: max_spread=30bps, max_fill_time=5s; Liquidity: max_fill_time=40s; Volume: default
    
    slippage_estimator.rs:
    - MAKER_SPREAD_FACTOR=0.05 (posting costs no spread crossing)
    - TAKER_SPREAD_FACTOR=0.50 (taker pays half-spread)
    - base_slippage_for_strategy(): Momentum=3-4bps, Liquidity=0.8-1.2bps, Volume=2.0bps
    
    sizing.rs:
    - equity_ratio_for_strategy(): Liquidity=0.05, Momentum=0.10, Volume=0.08
    - max_trade_ratio_for_strategy(): Liquidity=0.20, Momentum=0.25, Volume=0.40
    - size_quote_from_edge() now takes SelectedStrategy parameter
    
    Pipeline uses per-strategy entry filter and sizing ratios at both ranking and execution stages.
    
    Made-with: Cursor
  1302. 666fec4
    fix(engine): wire exit_config_for_exit_strategy and per-strategy SL/TP/time_stop
    Show commit body
    Three dead-code wiring gaps fixed:
    1. runner.rs: call exit_config_for_exit_strategy(selected_exit_strategy) instead of
       ExitConfig::default(); add ORDER_ID_CORRELATION_MODE log
    2. exit_lifecycle.rs: use config.time_stop_secs instead of hardcoded WAIT_TP_OR_TIMESTOP_SECS=90
    3. strategy_pipeline.rs: call momentum_execution_mode() for Momentum candidates to determine
       OrderIntent::TakerEntry vs MakerEntry; pass exit_strategy through Outcome
    
    Per-strategy ExitConfig values in strategy_selector.rs:
    - Liquidity→MakerLadder: SL=-20, TP=30, time=120s
    - Momentum→TSL: SL=-60, TP=150, time=30s
    - Volume→TimeDecay: SL=-25, TP=50, time=60s
    
    New OrderIntent::TakerEntry variant added; ExecutionIntent::TakerEntry matched accordingly.
    Outcome struct extended with exit_strategy field; live_runner.rs wired accordingly.
    
    Made-with: Cursor
  1303. 5107b39
    docs: full lifecycle proof report (EXIT_LIFECYCLE_COMPLETED confirmed live)
    Show commit body
    Full lifecycle proven on commit 6c14a4b, run_id=55, order_id=23:
    - ROUTE_SELECTED → ORDER_ACK → FILL_LEDGER_COMMITTED
    - EXIT_PLAN_CREATED → EXIT_RUNTIME_PLAN_ARMED
    - EXIT_REASON_TRIGGERED (time_stop) → EXIT_ORDER_SUBMITTED (market)
    - EXIT_LIFECYCLE_COMPLETED (market exit filled)
    - signal_to_submit=361ms, fill_to_exit_submit=7ms
    
    Made-with: Cursor
  1304. 6c14a4b
    fix: round SL/TP prices to 5 decimals + alphanumeric cl_ord_id for exit orders
    Show commit body
    Kraken rejects exit orders with too many decimal places (EOrder:Invalid price).
    Round stop_trigger_price and tp_price to 5 decimal places before submission.
    Also fix cl_prot/cl_tp id format to alphanumeric (no hyphens) for Kraken WS v2.
    
    Made-with: Cursor
  1305. 30c0b61
    fix: replay buffered execution events after add_order ACK populates bridge
    Show commit body
    Buffered events (pending_new, new, trade, filled) were released but not
    replayed through handle_execution_report. After ACK populates the
    exchange_bridge, replay each buffered event. If a fill is detected during
    replay, immediately trigger the exit phase (EXIT_PLAN_CREATED, etc.).
    
    This completes the full lifecycle: ORDER_ACK → FILL_LEDGER_COMMITTED →
    EXIT_PLAN_CREATED → EXIT_RUNTIME_PLAN_ARMED → EXIT_ORDER_SUBMITTED.
    
    Made-with: Cursor
  1306. abbfd5d
    fix: populate tracker bridge from add_order WS ACK + use exchange order_id for fills
    Show commit body
    Three combined fixes:
    1. Skip cl_ord_id in add_order (Kraken WS v2: not supported for this account type)
    2. On WS_METHOD_ACK for add_order: extract exchange order_id, call tracker.on_ack()
       and state_machine::on_ack() to populate the exchange_bridge for fill correlation
    3. Release buffered events for the exchange order_id after bridge is populated
    4. Keep WS_ADD_ORDER_SENT debug log and WS_METHOD_* logging for diagnostics
    
    Made-with: Cursor
  1307. 0fafab9
    test: temporarily remove cl_ord_id to test base order validity
    Show commit body
    Made-with: Cursor
  1308. cdba3c9
    debug: log WS_ADD_ORDER_SENT JSON for cl_ord_id invalid arguments diagnosis
    Show commit body
    Made-with: Cursor
  1309. 5f15fdc
    fix: cl_ord_id alphanumeric-only format for Kraken WS v2 compliance
    Show commit body
    Kraken WS v2 rejects cl_ord_id with hyphens (EGeneral:Invalid arguments:cl_ord_id).
    New format: "kb" + first char of prefix + UUID simple (32 hex) = 35 chars total.
    All alphanumeric, no hyphens, satisfies Kraken's character and length requirements.
    
    Made-with: Cursor
  1310. 89387c7
    fix: truncate cl_ord_id to 36 chars (Kraken EGeneral:Field cl_ord_id max)
    Show commit body
    Kraken WS v2 rejects add_order with cl_ord_id longer than 36 chars.
    Format "kb-strat-{uuid32}" = 41 chars exceeds limit.
    Now truncates to 36 chars while keeping kb-prefix and UUID entropy (≥27 hex chars).
    
    Made-with: Cursor
  1311. 6f431ed
    fix: log WS method responses (add_order ack/error) to diagnose silent order rejection
    Show commit body
    Previously all non-executions/non-ownOrders WS messages were silently
    dropped in `_ => {}`. This made it impossible to see Kraken error
    responses (e.g. EOrder:Unknown symbol) to add_order requests.
    
    Now logs WS_METHOD_ACK for successful method responses and
    WS_METHOD_ERROR for error responses.
    
    Made-with: Cursor
  1312. 4b3f63b
    fix: skip shadow_trade insert for NoEntry direction (LONG/SHORT constraint)
    Show commit body
    EntryDirection::NoEntry previously mapped to "UNKNOWN" which violates
    the shadow_trades_direction_check constraint (LONG/SHORT only).
    Skip shadow trade inserts entirely when direction is NoEntry.
    
    Also updates lifecycle proof report with session 9 evidence.
    
    Made-with: Cursor
  1313. 48dbc9e
    fix: bypass choke system_live_ready gate in ROUTE_PROOF_MODE
    Show commit body
    The choke_decide call in run_execution_once used system_live_ready
    (which is false in proof mode) as readiness_allows, causing Halt.
    
    In proof mode with ignore_regime_block, readiness_allows is set to true
    so the choke allows the order through.
    
    Made-with: Cursor
  1314. 4b8df30
    fix: proof mode bypasses pair_allowed_for_ranking and entry_filter gates
    Show commit body
    Track proof_candidate_forced flag; bypass all per-pair gates (readiness,
    pair_allowed_for_ranking, entry_filter) for exactly one candidate when
    ROUTE_PROOF_MODE=true && ROUTE_PROOF_IGNORE_REGIME_BLOCK=true.
    
    Made-with: Cursor
  1315. 0a232fc
    fix: ROUTE_PROOF_MODE bypasses per-pair readiness gate for first candidate
    Show commit body
    When route_proof.enabled && ignore_regime_block, the readiness-drop for
    non-StrategyMismatch blockers is bypassed for the very first candidate.
    This allows one proof candidate to reach the ROUTE_PROOF_ENTRY_FORCED logic.
    
    Logs ROUTE_PROOF_READINESS_BYPASSED when bypass applies.
    
    Made-with: Cursor
  1316. 3f4e780
    fix: wire route_proof_config_from_env() into run_execution_once pipeline config
    Show commit body
    run_execution_once used StrategyPipelineConfig::default() which has
    route_proof.enabled=false. Now reads ROUTE_PROOF_MODE env at startup so
    run-proof and run-execution-once both honour the env-based proof config.
    
    Made-with: Cursor
  1317. 1ca9868
    fix: ROUTE_PROOF_MODE bypasses system_live_ready gate (ignore_regime_block)
    Show commit body
    When ROUTE_PROOF_MODE=true and ROUTE_PROOF_IGNORE_REGIME_BLOCK=true, the
    readiness_system_block early-return is skipped so the pipeline can evaluate
    routes and force one micro trade for lifecycle proof.
    
    Logs readiness_system_block_bypassed instead of readiness_system_block when
    bypassed, so the blocker reason is still visible in logs.
    
    Made-with: Cursor
  1318. d0c6b93
    fix: pass live_freshness_secs=120 in run_readiness_analysis
    Show commit body
    A persistent ingest run never sets ended_at. With live_freshness_secs=None
    the data_stale gate always returned true for running ingest runs, blocking
    execution despite fresh data being available.
    
    Now: if last_ticker_ts or last_trade_ts is within 120s → data_stale=false.
    Ended runs still gated by MAX_RUN_AGE_FOR_READINESS_SECS (48h).
    
    Made-with: Cursor
  1319. 5f36350
    feat: persistent ingest service + economic forensics + foreground proof runner
    Show commit body
    Ingest (FASE 1):
    - heartbeat interval 300s → 60s
    - add INGEST_READY_FOR_EXECUTION_ATTACH after first valid epoch
    - systemd services: Restart=always, RestartSec=5/10
    
    Economic forensics (FASE 3/4):
    - collect RouteRecord during strategy pipeline loop
    - dump_route_forensics() when execute_count=0: ROUTE_EDGE_BREAKDOWN (top 5),
      ROUTE_FRONTIER, EDGE_ATTRIBUTION, BREAK_EVEN_THRESHOLD
    - write /tmp/edge_frontier_<run_id>.json artifact
    
    Foreground proof runner (FASE 2):
    - new run-proof command: loops run-execution-once with 20min timeout
    - prints PROOF_STATUS every 2s (eval_count, routes_evaluated, execute_count,
      highest_edge_bps, dominant_blocker)
    - auto-stops: lifecycle proven / ECONOMICALLY_BLOCKED / DATA_BLOCKED / timeout
    - dumps /tmp/route_analysis_<run_id>.log on exit
    
    Made-with: Cursor
  1320. ffa99bf
    fix: self-managed fail-fast migrations + fix duplicate version 20260310120000
    Show commit body
    - rename 20260310120000_execution_schema.sql → 20240308200000_execution_schema.sql
      to resolve duplicate version collision (two files sharing version 20260310120000)
    - db::create_pool now runs sqlx::migrate!() on every startup and aborts if it fails
    - removed warn-and-continue behavior; startup is blocked on DB_MIGRATIONS_FAILED
    - removed SKIP_MIGRATIONS escape hatch
    - logs: DB_MIGRATIONS_STARTED, DB_MIGRATIONS_APPLIED, DB_MIGRATIONS_UP_TO_DATE, DB_MIGRATIONS_FAILED
    
    Made-with: Cursor
  1321. acde7ca
    Docs: server migration + proof-run (EXECUTION_ENABLE + ROUTE_PROOF_MODE) sectie 8
    Show commit body
    - Migratie 20260311100000 handmatig toegepast op server (exit latency kolommen)
    - Proof-run uitgevoerd: commit 23e1396, run_id 54, no order (system_live_ready=false)
    - Rapport sectie 8: commando, uitkomst, conclusie
    
    Made-with: Cursor
  1322. 23e1396
    Docs: add multiregime route proof (Fase 6) and server validation checklist
    Show commit body
    Made-with: Cursor
  1323. 8348ff6
    Fase 2-5: Route discovery, proof mode, latency KPI, equity markers
    Show commit body
    Fase 2: Route discovery proof (multiregime)
    - ROUTE_DISCOVERY_STARTED, ROUTE_EVALUATED (per candidate), ROUTE_SELECTED, ROUTE_UNIVERSE_EMPTY
    - Track route_highest_edge_bps and route_blocker_counts for empty-universe log
    
    Fase 3: ROUTE_PROOF_MODE lifecycle force
    - RouteProofConfig + route_proof_config_from_env() (ROUTE_PROOF_MODE, MIN_EDGE_BPS, MAX_ORDER_QUOTE, etc.)
    - Pipeline: force one Execute with capped size when proof mode and no execute
    - ROUTE_PROOF_ENTRY_SUBMITTED, ROUTE_PROOF_FILL_DETECTED in runner; EXIT_LIFECYCLE_COMPLETED in exit_lifecycle
    - live_runner: pipeline_config.route_proof from env
    
    Fase 4: Latency KPI gates
    - log_route_latency_profile() in db/order_latency: signal→submit, submit→ack, fill→exit_submit
    - ROUTE_LATENCY_PROFILE and LATENCY_EDGE_PENALTY_APPLIED; called after exit phase in runner
    
    Fase 5: Balance/compounding markers
    - SIZE_FROM_NEW_EQUITY at pipeline sizing; EQUITY_UPDATED_AFTER_TRADE on exit completion (tp_filled + market exit)
    
    Made-with: Cursor
  1324. 030518c
    Fase 1: Universe/pinned state hard fix + recovery expand
    Show commit body
    - UniverseValidationError (PinnedSymbolMismatch + Other) in universe crate
    - validate_snapshot returns Result<(), UniverseValidationError> for downcast
    - L2 layer: force-include pinned symbols (same as L3/Execution) so pinned in pool get into L2
    - live_runner: UNIVERSE_VALIDATION_STARTED, UNIVERSE_VALIDATION_OK, UNIVERSE_PINNED_SYMBOL_MISMATCH, UNIVERSE_RECOVERY_EXPAND
    - On PinnedSymbolMismatch: extend pool_candidates with missing pinned, retry build_snapshot once
    - Initial snapshot and loop refresh both use recovery; no more hard abort on pinned symbol missing from L3/execution
    
    Made-with: Cursor
  1325. bf5de99
    docs: lifecycle proof report 2026-03-11 (server a347ae4, chain not yet proven; blockers external)
    Show commit body
    Made-with: Cursor
  1326. a347ae4
    Route-based engine: exit loop, timing instrumentation, route model, balance sizing, allocator, ingest heartbeat, lifecycle proof markers
    Show commit body
    - Exit loop in live runner: run_post_fill_exit_phase (SL+TP+time_stop/market)
    - Timing: execution_order_latency exit columns, update_on_exit_phase, exit latency report
    - Route model: SelectedExitStrategy, TradeRoute, candidate_routes_for_regime, PairReadinessRow.exit_strategy
    - Route-based edge: readiness per route; exit_config_for_exit_strategy
    - Balance-based sizing: EquitySource (Fixed/FromBalance), resolve_equity_quote
    - Capital allocator: next_equity_after_pnl, MAX_TRADE_PCT/MAX_OPEN_NOTIONAL caps
    - Ingest: INGEST_HEARTBEAT every 300s
    - Lifecycle proof markers: EXIT_PLAN_CREATED, EXIT_RUNTIME_PLAN_ARMED, EXIT_REASON_TRIGGERED, EXIT_ORDER_SUBMITTED, EXIT_MAKER_FALLBACK_TO_MARKET
    
    Made-with: Cursor
  1327. cbdc451
    docs: single source of truth, centralise and clean documentation
    Show commit body
    - Add ENGINE_SSOT.md as single source of truth (status matrix, references)
    - Add ARCHITECTURE_ENGINE_CURRENT.md with Mermaid diagrams
    - Add LIVE_RUNBOOK_CURRENT.md (ingest, execution attach, markers)
    - Add VALIDATION_MODEL_CURRENT.md (proof types, economic vs data/attach blocked)
    - Add CHANGELOG_ENGINE.md (git-based, per subsystem)
    - Add DOC_INDEX.md (lead vs historical docs)
    - Add DOC_AUDIT_RESULT.md and DOC_CONSISTENCY_REPORT.md
    - Move superseded docs to docs/superseded/ with SUPERSEDED banner
    - Update README to point to SSOT and DOC_INDEX; remove broken doc refs
    - Add systemd/README.md for ingest and execution units
    - Remove DEPLOY_REPORT_*.md (time-bound, no SSOT value)
    - Update CHANGELOG.md with 2025-03-11 entry
    
    No runtime or strategy changes; git-only codeflow.
    
    Made-with: Cursor
  1328. 5cb496b
    fix(scripts): handle unset SKIP_RUN in live validation
    Show commit body
    Keep the hardened validation script compatible with set -u by guarding the optional SKIP_RUN flag explicitly. This preserves fail-fast proof checks without crashing before runtime validation starts.
    
    Made-with: Cursor
  1329. cff7b72
    fix(scripts): fail validation when proof target is unmet
    Show commit body
    Require warmup, minimum evaluation counts, and proof-target-specific runtime evidence before the live server validation script can report success. Label blocked runs explicitly as DATA_BLOCKED or ECONOMICALLY_EMPTY so short or empty runs no longer masquerade as proof.
    
    Made-with: Cursor
  1330. dcdf2de
    docs: add live validation runs analysis (run_id=32-34, commit b4ad93b)
    Show commit body
    Documents the complete live validation attempt with findings:
    - Epoch contract proven: status=valid from epoch 3 onward (run_id=34)
    - Universe selection bug fixed (two commits: 128f314, b4ad93b)
    - DATA_INTEGRITY_MATRIX system_live_ready=true proven
    - ENGINE_MODE transitions: ExitOnly→NoNewEntries (correct)
    - NO_ORDER diagnosis: ECONOMIC_GATING (EdgeNegative/RegimeChaos) vs DATA_RUNTIME
    - Live order lifecycle pending: requires non-CHAOS market window for Execute decision
    
    Made-with: Cursor
  1331. 0715310
    fix(execution): restore multistrategy live candidate flow
    Show commit body
    Infer maker direction from microstructure inputs, fan out regime-driven strategy candidates, and carry the winning strategy into execution. Lower the default risk floor so pipeline-sized orders can pass the risk gate and emit runtime logs that prove candidate fan-out and sizing decisions.
    
    Made-with: Cursor
  1332. b4ad93b
    fix(universe): lower MIN_EXECUTION_TICKER_ACTIVITY to 10 (= epoch minimum)
    Show commit body
    Threshold of 20 was too strict for the 60-second warmup window, resulting
    in only 2 symbols qualifying for execution (run_id=33, symbol_count=2).
    
    10 = MIN_TICKER_PER_SYMBOL matches the epoch completion criterion exactly.
    Liquid symbols (ETH/USD, EUR/USD, ADA/USD etc.) accumulate 10+ tickers in
    the 60s warmup; structurally illiquid symbols (1INCH/USD: 1 ticker/25 min,
    AIXBT/USD: 1 ticker/25 min) remain filtered out.
    
    Made-with: Cursor
  1333. 128f314
    fix(universe): filter illiquid symbols from Execution layer selection
    Show commit body
    Symbols with ticker_samples < MIN_EXECUTION_TICKER_ACTIVITY (20) are
    now excluded from the Execution universe layer, unless they are pinned.
    
    Root cause: the execution universe was selecting symbols like 1INCH/USD
    (1 ticker per 25 min), ACH/USD (0.08/min), AIXBT/USD (0.17/min) that
    can never satisfy the epoch completion criterion of MIN_TICKER_PER_SYMBOL=10
    per epoch window (5 min). This caused epochs to remain perpetually degraded
    → ENGINE_MODE=ExitOnly → no Execute decisions possible.
    
    The threshold (20) = 2× the epoch minimum to account for the warmup-to-epoch
    timing gap. Pinned symbols always bypass this filter.
    
    Run_id=32 diagnosis (5 eval cycles, 0 orders, all epochs degraded):
      criteria_ticker_ok=false for epochs 1-5 due to 8 illiquid execution symbols
      EdgeNegative + RegimeChaos as secondary economic blockers
    
    Made-with: Cursor
  1334. f646c2e
    docs: label run-deterministic-proof as internal exerciser, distinguish from live validation
    Show commit body
    Made-with: Cursor
  1335. 3112cbd
    Add run-deterministic-proof: live edge-case validation
    Show commit body
    Exercises three deterministic lifecycle paths against the real DB
    without requiring exchange connectivity:
    
    CASE 1 (OOO): Fill arrives before ACK
      - ORDER_EVENT_BUFFERED (fill buffered, bridge not yet populated)
      - ACK arrives → bridge populated → ORDER_EVENT_OUT_OF_ORDER
      - Buffered fill released; buffer empty after ACK
    
    CASE 2 (Idempotency): Duplicate fill
      - fills_ledger::process_fill called twice, same exchange_trade_id
      - Second call: was_duplicate=true, FILL_DUPLICATE_IGNORED
      - DB verified: exactly 1 fill row per exchange_trade_id
    
    CASE 3 (Reconcile): WS disconnect → Suspect → ReconcileRequired → Recovered
      - handle_ws_disconnect → ORDER_SUSPECT in tracker + DB
      - start_reconcile → ORDER_RECONCILE_REQUIRED in tracker + DB
      - reconcile_from_snapshot (status=open) → ORDER_RECONCILED, AckedOpen
      - RECONCILE_COMPLETE
    
    All proof orders cleaned up from DB after run.
    CLI: krakenbot run-deterministic-proof
    
    Made-with: Cursor
  1336. 32aae98
    docs: update deterministic engine deliverable with OrderTracker wiring status
    Show commit body
    Made-with: Cursor
  1337. c4edd1c
    Wire OrderTracker + ws_handler into live execution loop
    Show commit body
    Replaces all direct DB-correlation bypasses in submit_and_wait_for_execution_reports
    with the deterministic OrderTracker/ws_handler flow:
    
    - DB-first: order persisted (PendingSubmit) before exchange submit
    - OrderTracker registered immediately after DB persist
    - All WS events (ACK/FILL/CANCEL/REJECT) routed through ws_handler::handle_execution_report
    - cum_qty_before looked up from tracker state (no local HashMap)
    - order_qty_base read from tracker (no caller-passed param)
    - FILL -> fills_ledger::process_fill (atomic tx: fill + position + PnL)
    - WS disconnect -> ws_handler::handle_ws_disconnect -> orders -> Suspect
    - Reconnect -> order_reconcile::start_reconcile -> ReconcileRequired
    - ownOrders snapshot -> reconcile_from_snapshot
    - Out-of-order events buffered by exchange_order_id, released on ACK
    - Duplicate events idempotently ignored (fills: ON CONFLICT DO NOTHING)
    
    Removes: pending_ack_order_id hack, cum_qty_per_order local map,
    inline ACK/FILL/CANCEL/REJECT handlers, direct DB-correlation in loop.
    
    Made-with: Cursor
  1338. 3b98ea8
    feat(execution): deterministic execution engine
    Show commit body
    DB-first order lifecycle, 13-state machine, thread-safe OrderTracker,
    ledger-based fill accounting, reconnect/reconcile, no f64 for price/qty.
    
    New modules:
    - order_state.rs: 13 states (Created, PendingSubmit, AckedOpen,
      PartiallyFilled, Filled, Canceled, Rejected, Suspect,
      ReconcileRequired, UnknownExchangeOrder, LostOrRejected, Error, Reconcile)
    - order_tracker.rs: DashMap primary + bridge; parking_lot event buffer
    - order_buffer.rs: BufferedEvent for out-of-order WS events
    - fills_ledger.rs: single-transaction fill+position+PnL+micro-alpha
    - order_reconcile.rs: WS disconnect → Suspect → ReconcileRequired → recovered
    - ws_handler.rs: cl_ord_id-first correlation; never symbol/side matching
    
    Key fixes:
    - cl_ord_id uses UUID v4 (not timestamp_nanos)
    - on_ack/on_amend/execution_report_to_event: Decimal not f64
    - PartialFill → PartiallyFilled state rename + backward-compat from_str
    - runner.rs fill loop: Decimal throughout
    - UNIQUE(order_id, exchange_trade_id) on fills for idempotency
    
    New migration: 20260310140000_deterministic_execution_engine.sql
    - +21 pre-trade audit + lifecycle fields on execution_orders
    - +6 micro-alpha fields on fills
    - execution_event_buffer table for buffered/unknown events
    - Dedupe indexes on fills + order_events
    
    Deps: dashmap = "5", parking_lot = "0.12" added to Cargo.toml
    Made-with: Cursor
  1339. 0d29b83
    fix(db): correct query_scalar return type from (i64,) to i64
    Show commit body
    query_scalar returns a single scalar value (i64), not a tuple (i64,).
    The (i64,) type caused a RECORD/INT8 mismatch at decode time, silently
    failing create_epoch and insert_execution_universe_snapshot on every
    call → all epochs stuck at 'pending', INGEST_EPOCH_STARTED never logged.
    
    Fix both create_epoch and insert_execution_universe_snapshot.
    
    Made-with: Cursor
  1340. 8de7afe
    fix(epoch): add sqlx json feature for JSONB binding; surface create_epoch errors
    Show commit body
    - Cargo.toml: add 'json' feature to sqlx (required for serde_json::Value → JSONB)
    - live_runner.rs: convert both `if let Ok(create_epoch)` to match arms with
      ERROR-level INGEST_EPOCH_CREATE_FAILED logging on failure
    - live_runner.rs: Err arm already present for snapshot insert (kept)
    
    Root cause: without sqlx json feature, serde_json::Value binding to JSONB
    columns silently fails at runtime; create_epoch itself likely OK but snapshot
    insert fails → epoch stays pending → execution always skips evaluation.
    
    Made-with: Cursor
  1341. 9c460d6
    fix(execution): log EXECUTION_UNIVERSE_SNAPSHOT_INSERT_FAILED when snapshot insert fails
    Show commit body
    Made-with: Cursor
  1342. 571d1d4
    fix(db): bind JSONB for execution_universe_snapshots via sqlx::types::Json
    Show commit body
    Made-with: Cursor
  1343. cae54c1
    feat(epoch+split): FASE 1 validation script/runbook, FASE 2 ingest/execution split
    Show commit body
    FASE 1:
    - scripts/validate_epoch_contract_fase1.sh: server validation of epoch contract (clean tree, run live N min, grep evidence)
    - docs/EPOCH_CONTRACT_FASE1_VALIDATION_RUNBOOK.md: runbook + hard conclusion template
    - live_runner: NO_ORDER_ECONOMIC_GATING / NO_ORDER_DATA_RUNTIME logging for no-order diagnosis
    
    FASE 2:
    - config: execution_only (EXECUTION_ONLY) for split mode
    - run_execution_only_loop: execution without ingest; binds to ingest epochs/snapshots only
    - run-execution-live when EXECUTION_ONLY=1: no run/lineage/WS ingest; only epoch reader + evaluation loop
    - run-ingest: new CLI mode; ingest_runner.rs (public WS, universe, epoch/snapshot, L3 health; no execution)
    - systemd: krakenbot-ingest.service, krakenbot-execution.service (EXECUTION_ONLY=1)
    - docs/EPOCH_SPLIT_DELIVERABLE.md: commits, paths, validation proof, systemd install
    
    Git-only deployment; no scp. Server: run validate_epoch_contract_fase1.sh for FASE 1 proof; then deploy split.
    
    Made-with: Cursor
  1344. 0d433c3
    feat(epoch): hard ingest/execution epoch contract — migration, lineage, epochs, snapshots, data-integrity matrix, engine modes, lineage break, STRATEGY_REJECTED/BLOCKER_DISTRIBUTION/NO_ORDER_SYMBOL_DROP
    Show commit body
    - Migration: ingest_lineage, ingest_epochs, execution_universe_snapshots
    - Epoch lifecycle: create_lineage at start, create_epoch + update_epoch_status on universe refresh, execution-centric criteria (valid = exec+pinned ok)
    - Epoch reader: current_valid_epoch_id (entries), current_epoch_for_exit_only (exit-only), execution_universe_snapshot_by_id
    - Live runner: bind cycle to one epoch, load snapshot, EXECUTION_UNIVERSE_BOUND, DATA_INTEGRITY_VERIFIED
    - Data integrity matrix: global_liveness, snapshot_coherence, market_freshness, l3_integrity, universe_viability (EXECUTION_MIN_UNIVERSE_SIZE)
    - Engine modes: Normal/NoNewEntries/ExitOnly/Halt, ENGINE_MODE_ENTER/EXIT, no new entries when degraded/lineage break
    - Lineage break: last_bound_lineage_id, INGEST_LINEAGE_CHANGED, EXECUTION_INGEST_LINEAGE_BREAK, EXECUTION_EMERGENCY_EXIT_ONLY
    - No-order diagnose: STRATEGY_REJECTED, BLOCKER_DISTRIBUTION, NO_ORDER_SYMBOL_DROP (Outcome.symbol)
    - Docs: docs/INGEST_EXECUTION_EPOCH_CONTRACT.md with contract, validation checklist, no-order reporting
    
    Made-with: Cursor
  1345. 778b788
    Safety: pinned invariants, WS reconcile logs, latency heatmap
    Show commit body
    Made-with: Cursor
  1346. e57a816
    Docs: universe audit current state (200/60/50/30); Cursor rule remote-execution-ssh; config/live_runner/runner safety and runtime fixes
    Show commit body
    Made-with: Cursor
  1347. f805468
    Docs: reference Cursor rules and Rust development mode in CURSOR_RULES.md
    Show commit body
    Made-with: Cursor
  1348. 1286f1e
    Add Cursor rule: Rust development mode (one change, cargo check, focused commits)
    Show commit body
    Made-with: Cursor
  1349. ca3b504
    Fix duplicate Decimal import and unwrap_or in stats_queries
    Show commit body
    Made-with: Cursor
  1350. c0ae3a4
    Allow skipping/failing migrations gracefully via env
    Show commit body
    Made-with: Cursor
  1351. 8c99445
    Fix shadow evaluator dependencies (uuid + FromRow) for server builds
    Show commit body
    Made-with: Cursor
  1352. e534f54
    Add shadow trade evaluator and blocker matrix
    Show commit body
    Made-with: Cursor
  1353. 0b44fea
    Fix ws_safety_report parsing (strip ANSI, avoid duplicate zero output)
    Show commit body
    Made-with: Cursor
  1354. a248b17
    Make ws_safety_report.sh executable for server proof bundle
    Show commit body
    Made-with: Cursor
  1355. c8b471c
    Fix run-execution-live shutdown timing by capping sleep to deadline
    Show commit body
    Ensure evaluation loop sleeps never overshoot the runtime deadline, so runtime window shutdown logs and post-run proof bundle steps reliably execute within configured runtime.
    
    Made-with: Cursor
  1356. c12d026
    Add safety integrity report CLI over symbol_safety_state and latency
    Show commit body
    Introduce report-safety-integrity command that summarizes symbol safety modes and submit->ack latency from execution_order_latency over the last 24h.
    
    Made-with: Cursor
  1357. 52262e9
    Document WS-native safety stack and add ws_safety_report script
    Show commit body
    Extend logging and server validation docs with ORDER_LATENCY, PRIVATE_STREAM_SILENT, QUIET_MODE, EXIT_ONLY and HARD_BLOCK events, and add a ws_safety_report.sh helper to summarise WS safety signals from logs and DB.
    
    Made-with: Cursor
  1358. dd48798
    Add WS-native safety extensions (latency, watchdog, exit-only, hard-block)
    Show commit body
    Introduce T0–T3 order latency persistence + ORDER_LATENCY logs, add private WS silent watchdog with reconnect, and persist per-symbol safety state for quiet-mode, exit-only (pinned + invalid L3), and L3 resync hard-blocking.
    
    Made-with: Cursor
  1359. c182d4e
    Fix universe pinning: use base_position in positions table
    Show commit body
    Made-with: Cursor
  1360. 068b67b
    Add dynamic universe manager with atomic snapshot rotation
    Show commit body
    - Add UniverseManager: atomic snapshot build/validate/swap, two-phase apply overlap, pinned symbols, rotation diff logs
    - Fetch pool symbols from Kraken AssetPairs (/USD) for 150–200 pool
    - Live runner: ticker/trade on pool, select L2/L3/exec after warmup, periodic refresh with rollback guard
    - Pipeline: optional execution-universe filter
    - Config: UNIVERSE_* limits and refresh controls
    
    Made-with: Cursor
  1361. 5d897ad
    L3 capacity test: plan, run script, report script
    Show commit body
    - docs/L3_CAPACITY_TEST_PLAN.md: degradation metrics, acceptance criteria, test matrix (L3=5/15/25/50), capacity table template, advice
    - scripts/run_l3_capacity_test.sh: one run with OBSERVE_SYMBOL_LIMIT_L3, fixed runtime/interval; optional MONITOR_RESOURCES=1
    - scripts/l3_capacity_report.sh: parse log + DB for run_id report (warmup, eval cycles, drift, samples)
    
    Made-with: Cursor
  1362. 985ff06
    TDD gaps + server validation + universe audit (git-only codeflow)
    Show commit body
    - live_runner: DATA_WARMUP_START/COMPLETE/INSUFFICIENT, min data requirements, execute_count/skip_count/primary_blockers in LIVE_EVALUATION_COMPLETED
    - state_machine: ORDER_SUBMIT/ACK/FILL/REJECT/CANCEL in logs for lifecycle proof
    - pipeline: UNIVERSE_VALIDATION log (symbols_seen, symbols_with_l3, funnel counts)
    - docs: ENGINE_TARGET_STATE, MODEL_CALIBRATION, SERVER_VALIDATION_LIVE_ENGINE, UNIVERSE_AND_DIVERSITY_AUDIT
    - scripts: validate_live_engine_server.sh (clean tree, build, run-execution-live 2+ cycles)
    - runbook: server validation section
    
    Made-with: Cursor
  1363. fa69932
    fix: live execution scheduling guard + min one evaluation; LIVE_EVALUATION_SCHEDULED/STARTED/COMPLETED/SKIPPED; runbook config
    Show commit body
    Made-with: Cursor
  1364. 838c8c3
    feat: run-execution-live — long-running live validation with WS data + periodic pipeline
    Show commit body
    Made-with: Cursor
  1365. a5f367a
    log primary_blockers in readiness_system_block root cause
    Show commit body
    Made-with: Cursor
  1366. fd73aec
    fix: root cause of system_live_ready=false — log tradable_count/data_stale/regime; add end-current-run to set ended_at for execution
    Show commit body
    Made-with: Cursor
  1367. 82d88a8
    fix: multiregime execution pipeline — per-pair strategy in readiness check, funnel drop counts, no_pair guard
    Show commit body
    Made-with: Cursor
  1368. b183ae9
    audit: instrument constraints (execution layer) + execution pipeline funnel + EXECUTION_ENABLE
    Show commit body
    - docs/EXECUTION_INSTRUMENT_CONSTRAINTS_AUDIT.md: where constraints live (runtime Kraken only), submit_order normalizes, amend_order/stop-loss gaps
    - docs/EXECUTION_PIPELINE_AUDIT.md: funnel logging, no test-forcing, lifecycle checks
    - Pipeline: EXECUTION_PIPELINE_FUNNEL + EXECUTION_PIPELINE_CANDIDATE logging
    - Config: execution_enable (EXECUTION_ENABLE); runner gate; main EXECUTION_ENGINE_START
    - git-only-codeflow + EXECUTION_AUDIT: EXECUTION_ENABLE for run-execution-once
    
    Made-with: Cursor
  1369. c2e5c8b
    chore: make start_live_validation_engine_server.sh executable
    Show commit body
    Made-with: Cursor
  1370. 212beee
    fix: validate_execution_on_server.sh load ~/.cargo/env when cargo not in PATH (ssh non-interactive)
    Show commit body
    Made-with: Cursor
  1371. a30e38c
    fix: execution_report_to_event use incremental fill (cum_qty_before); AmendOrderParams skip order_id when None
    Show commit body
    Made-with: Cursor
  1372. 5191d7c
    Doc: server validation steps for execution in git-only-codeflow rule
    Show commit body
    Made-with: Cursor
  1373. 00ea2f5
    Validate script: fail on dirty tree, add SKIP_RUN, server instructions
    Show commit body
    Made-with: Cursor
  1374. b793739
    Execution layer: migration, execution/*, db execution tables, runner, audit fixes, server validation script
    Show commit body
    - migrations/20260310120000_execution_schema.sql (orders, events, fills, positions, realized_pnl)
    - src/execution: intent, choke, order_state, kraken_adapter, state_machine, runner, exit (submit_exit_order)
    - src/db: execution_orders, order_events, fills, positions, realized_pnl
    - exchange: amend_order (messages + auth_ws), ExecutionReport.cl_ord_id
    - CLI run-execution-once; correlation by cl_ord_id/exchange_order_id; data array or object; reconcile path
    - positions: avg_entry only when increasing; realized_pnl on partial/full close
    - logging: order_ack cl_ord_id, order_submit queue_decision
    - docs/EXECUTION_AUDIT.md; scripts/validate_execution_on_server.sh (git-only codeflow)
    
    Made-with: Cursor
  1375. fd9c0cb
    fix: decode AVG(a) as Decimal, PERCENTILE_CONT p50/p95 as f64 in persist_pair_summary_24h
    Show commit body
    Made-with: Cursor
  1376. 5d2bc17
    fix: decode p50/p95/a as f64 in persist_pair_summary_24h (PostgreSQL PERCENTILE_CONT returns FLOAT8)
    Show commit body
    Made-with: Cursor
  1377. e64ed78
    fix: observe lifecycle — fractional OBSERVE_DURATION_HOURS + SIGTERM for clean ended_at
    Show commit body
    Made-with: Cursor
  1378. e157c5a
    Pro engine: readiness, cost/surplus, live validation, post-trade layers, deploy /srv/krakenbot
    Show commit body
    Made-with: Cursor
  1379. e4182b3
    Initial Kraken bot implementation
    Show commit body
    Made-with: Cursor

Full website changelog: docs/CHANGELOG_FINALISATIE.md

Donate…