Fortress Desk · living spec · source _md_files/desk/DESK_REALTIME_SPEC.md

Fortress Desk real-time: spec, decisions and where we are

The ONE place for the Desk real-time work: the goal, how the real-time path works, every owner decision (date, why, commit), what was deliberately not done, the open items, how to measure, and the rules for changing anything. Living doc: update it IN PLACE when a real-time decision is made or a number moves. Frozen thread dumps: history/2026-10-09_DESK_REALTIME_PASS_CONTEXT.md; new-thread brief (decisions + TODO): history/2026-10-13_DESK_REALTIME_NEW_THREAD.md. CF copy: /reference/DeskRealtimeSpec (re-render with 2026-10-13_realtime_bench/render_spec.py, then deploy_cf.sh). Per-tool mechanics live in _md_files/SCRIPT_CATALOG_DETAILS.md (the *_live.py sections); this file is the decisions and the map.

1. Goal

Owner, 2026-10-09: make the Desk app "as real-time as possible, just like TWS". The app is the surface the owner trades from (native-first; Classic Chrome pages are the fallback, CLAUDE.md "Desk: native-first").

2. Where we are (measured)

App = how old the data on the screen is, sampled every second. Chrome = the same tab's Classic page in headless Chrome, same window.

TabApp tab data, p50 / p90App spot + held-call ticks, p50 / p90Chrome Classic, p50 / p90Measured
Rebuild1.2 / 3.4 s0.5 / 0.9 s49 / 75 s2026-10-13 01:02-01:17 (app), 00:04 (Chrome)
FIGHT1.2 / 3.5 s0.7 / 0.8 s41 / 73 s2026-10-12 23:51-00:01
Scanner1.1 / 2.0 s0.5 / 0.9 s10 / 23 s2026-10-13 00:04
Coverage1.2 / 3.3 snot ticked16 / 28 s2026-10-13 00:04
Income1.6 / 5.8 snot ticked7.9 / 13 s2026-10-13 00:04

3. How the real-time path works (one paragraph per piece)

4. Market-data lines (the budget behind every real-time choice)

Account cap 578 simultaneous lines (TWS dialog); TWS's own windows hold ~72; 50 kept free for order tickets. module_ibkr_fetch.MD_LINE_ALLOTMENT / MD_LINE_TWS_OWN / MD_LINE_BUDGET / MD_LINE_CAPS; line_budget_report().

HolderLines (caps today)
Spots, shared by all five watchers~42
Rebuild: streams + windows + sweep250 = 120 + 80 + 50
Scanner: streams + windows130 = 80 + 50
FIGHT streams80 cap (43-57 used)
Coverage / Income snapshot burstssmall
Order tickets: hot streams on the warm previewer (150178f, off switch)owner line-budget test
TWS's own windows~72

Peak seen: 472 watcher lines + ~72 TWS = 544 of 578 (2026-10-12 22:08-23:00). Every watcher publishes md_err_101 (refusals) and line counts in live.json stats.

5. Owner decisions (newest first)

Date (SGT)DecisionWhyCommit(s)
10-13 01:2xLEAVE Rebuild's remaining blocking fallback as is (~9 s/min: sweep DELAYED dead-contract verdicts + verify FROZEN/DELAYED leftovers; ~3 pauses of 10-11 s per 15 min)ticks keep flowing, decisions unaffected, the fix (windows after a tidy step) costs hours for ~5 s off the worst pause; revisit only if the owner says the tab visibly freezesnone
10-13 01:01No blocking SNAP tier on the LIVE feed in RTHSNAP = 15+ min-old snapshot of strikes with no live market: 21 of 33 fallback calls, 27% quoted, 235 s blocked per 16 min98dbc8f
10-13 00:34On-demand grids (roll chain, cc / hatch re-plan) ask LIVE + FROZEN only in RTHDELAYED / SNAP made 14-25 s blocks (COPX, AMZN)10bf96f
10-13 00:04Second Chrome-vs-app readingowner asked for one morereading in 2026-10-13_realtime_bench/readings/
10-12 23:5xFIGHT: display-only Monte Carlo off the loop (rung note without the ladder table; detail panels only for a Desk-followed label)owner: "the main card is what I trust", the MC detail is rarely opened037bb07
10-12 23:30FIGHT Classic pages in a render worker; orphan-chain yardstick 'pos' fixrender 1.9 s p50 on the loop; tick stalls 7.4 s4e28657
10-12 22:20Open checks for Monday (launch fix proof, err 101, Coverage/Income loop, held legs, Rebuild)first session after the weekend shipsall passed; see section 7
10-10 11:21Coverage / Income: 2 s loop + publish first (no 1 s ticks for them)tab age 3.3 s p50 vs ~1 s elsewhere; ticks would add little once the loop is 2 s8598ea2
10-10 04:07Launch fix: Homebrew python pinned, watchers at PRI 31 from any caller (launchd Interactive), Launchpad KILL stops them, FortressList Interactivecron / Launchpad starts ran at PRI 20 (renders ~2x slower), Xcode 3.9 oncef889003
10-10 01:58Sweep backoff for unquotable rows (_sweep_tried)the faster sweep re-asked dead rows every 15 s14e6398
10-10 01:13Rebuild fallback item waits up to 45 s for window lines (owner "go ahead with change 2")fewer demotes to blockingfabb705
10-10 00:2xRebuild sweep gets its OWN 50 lines (Rebuild 200 -> 250)sweep waited for lines; owner: "give the sweep 50 more lines"52fbb9e, fb9baf9, bfd0b40
10-10 00:53Scanner split-phase windows, 80 streams + 50 window lines (owner "A: 80 + 50", another thread)Scanner p90 tail = blocking verify87ab8b9
10-09 23:0xHeld legs: option (a) skip covered re-checks (not (b) focus-band slice, not (c) +88 lines)no lines; smaller win4659166
10-09 23:0xSpeed-ups, ship during RTH: Scanner MC / verify after publish; Rebuild fallback as windows; sweep at full 50no budget change8209bb2, 1b3ac12, c2d646f, 1dcf5a2
10-09 23:0xFIGHT verify budget: NO change (5 s p90 is fine)ownernone
10-09 22:3xFix FIGHT's per-cycle Monte Carlo regression (from 6d5d6bc) and shipDesk publish 1.6 s per cycle77469e6
10-09 22:1xRebuild "120 streams + 80 windows" (inside 200)windows replace blocking verify bursts1ec01d6 and follow-ups
10-09 10:2xMD_LINE_ALLOTMENT 578 + TWS-own term; spot de-dupthe real cap; ~100 lines freed with no cute1b5ad3
10-09"Keep full limits. Do NOT trim until you confirm with me." On err 101: report numbers + escalation order, wait for the OKowner rule (memory feedback_line_caps_confirm_before_trim)9c37802
10-09No "wait N min after a TWS restart" anywhere; a wedged TWS needs a restartowner rule1c5be86, b18668b, 78c2125
10-09 01:20Restarts during RTH approved for this workownernone

Standing owner preferences that shape this work: live pick flipping is fine (picks are read as real time); the Rebuild routing verdict (ROLL CC / SKIP / WAIT) is not used; light mode only; center-aligned tables.

6. Deliberately not done (and when to revisit)

ItemWhy notRevisit when
Rebuild fallback calls as windowsowner 2026-10-13: leave itowner reports the Rebuild tab freezing
FIGHT split-phase windowsneeds window lines; peak 544 of 578owner frees or raises lines
Coverage / Income 1-second ticks2 s loop already ~1.2-1.6 s; spot is live via spot.quotestheir p90 matters to the owner
Held legs (b) / (c)(a) chosen(a)'s skips prove too few (cc_check had 27 skips / 10 min on 10-12)
Lower the Classic pages' reload throttlethe app does not need it; Classic is the fallbackowner uses Chrome again
Any cap / budget cutowner rule: confirm firsterr 101 in RTH, with numbers

7. Open items / watch

8. Rules for changing real-time code

  1. Measure first, in RTH, 10-15 min, read-only (section 9). Compare against section 2's numbers, never against memory.
  2. Never block the loop on a fetch you can window, and never put display-only work (Monte Carlo detail, HTML) on the per-cycle path.
  3. Line budget: a new stream / window / burst goes through MD_LINE_BUDGET / MD_LINE_CAPS; any cap change needs the owner's OK; on err 101 report and ask, never trim.
  4. Rebuild: tidy-as-you-go (CLAUDE.md) plus rh_gate check; re-capture only the cases whose differences you confirmed.
  5. Restarts: a PLAIN restart that resumes from the watcher's own dump (launch_live.sh --only <tool> --action restart); never delete the dump unless fetch planning changed AND a batch bundle under 30 min exists (2026-10-09: Rebuild dark 13 min). Check no order ticket is WORKING first.
  6. Ship Python changes to a running watcher outside RTH unless the owner says ship now.
  7. Commit only your own hunks; other sessions edit the same tree (stage HEAD + your hunk via git hash-object -w / git update-index --cacheinfo).
  8. Record the decision here (section 5) and the mechanics in SCRIPT_CATALOG_DETAILS.

9. How to measure (tools in 2026-10-13_realtime_bench/)

ToolWhat it measuresRun
chrome_classic.mjs + freshness.pyChrome vs app on-screen age, same window, 10 min all visible + 10 min one visiblenode chrome_classic.mjs OUT.json 600 SCRATCH_DIR rebuild then python3 freshness.py OUT.json
beat_sampler.py + beat_report.pya watcher's Desk beat (live.json stats + index writes, 1 s)python3 beat_sampler.py OUT.jsonl MINUTES [runs_subdir]
fight_sample.py / rb_sample.pyFIGHT / Rebuild Desk gap, data age, tick age, phase timingspython3 fight_sample.py SECONDS OUT.json
open_checks.pyopen-of-session checks: launch PRI, err 101, line totals, publish gapspython3 open_checks.py OUT_DIR (edit the 21:50 / 22:20 / 23:00 gates)

All read-only: GET only, never :8924, no IBKR connection. Chrome uses a scratch profile.

Rendered from the repo spec (last edit 2026-10-13 01:24 SGT). Edit the .md, then re-run render_spec.py and deploy_cf.sh.