Operating guide for the 12-week SANDBOX paper-validation run (ADR-010, ADR-030, PDR-004). This document is the system of record for the validation start date (Run Log, bottom of this file) and the first place to look when something does not behave as expected.
Audience: the operator (single dev box). Written S5-004, 2026-06-10.
The Azure Functions isolated worker runs locally on the dev box, not in Azure:
- TWS (paper, API port 7497) and claude-gateway (localhost:3131) are both
loopback-only (ADR-029). The worker must share the host with them — an Azure-hosted
worker cannot reach either. Full rationale and alternatives: ADR-031 in
DECISIONS.md. - Start the worker:
cd D:\Source\TradingSystem\src\TradingSystem.Functions func start # or: dotnet run - Machine-on requirement: the dev box and the worker must be up across the day's timer firings (all 4 timers — see section 3). Sleep/hibernate counts as down.
- Missed runs do not catch up. If the worker was not running when a timer was due,
that run is simply skipped — a gap day shows up as a missing entry in
data/snapshots.jsonand no Discord report for that date. Gap days are tolerated by the ≥12-week window (ADR-031); triage is in section 5, not panic.
Run this check each trading morning before 5:00 AM PT in winter (PST) / 6:00 AM PT in summer
(PDT) — the winter deadline is the earlier one (see section 3) — or run
tools/ops/preflight.ps1 (S6-004), which scripts the same checks with bounded timeouts:
TWS port probe (step 1), gateway health (step 2 — gateway-down is a WARN, not a FAIL),
worker admin-endpoint 4-function list (steps 3–5), and the snapshots data directory:
- TWS paper is running and logged in to the paper account; API enabled with socket port 7497 and trusted IP 127.0.0.1 (Configure → API → Settings).
- claude-gateway is up:
(In PowerShell, bare
curl.exe http://localhost:3131/health
curlaliasesInvoke-WebRequest— usecurl.exeorInvoke-RestMethod http://localhost:3131/health.) 200 = up. If it is down, the system degrades to deterministic rule-based regime classification (ADR-029/ADR-030) — this is expected behavior, not an incident. Restart the gateway (D:\Source\claude-gateway) when convenient. - Functions worker started (
func startper section 1). - Host boot smoke: after
func start, the function-list banner must appear with noTypeLoadException(or repeated "worker process" restarts) in the startup output. The banner proves the isolated worker process actually loaded its DI graph — a package-version mismatch (S5-004r) can pass the full unit suite yet crash the worker at bootstrap, because tests never load the Functions host. Run this check first: the function list in step 5 is meaningless until the worker has provably booted. - Startup-log smoke check: the worker banner lists all 4 functions —
DailyOrchestrator_PreMarket,DailyOrchestrator_EndOfDay,IncomeSleeve_MonthlyReinvest, andIncomeSleeve_QuarterlyAudit. If any is missing, the build is broken or the wrong project started; do not assume the day will run.tools/ci/host-boot-smoke.sh(S6-002) is the scripted equivalent of steps 4–5 — it boots the worker and asserts the same four names.
The NCRONTAB expressions are UTC-fixed; PT wall-clock times therefore shift by one hour across DST transitions. Document only — do not "fix" the crons.
| Function | Cron (UTC) | UTC | PT (PDT, summer) | PT (PST, winter) |
|---|---|---|---|---|
DailyOrchestrator_PreMarket |
0 0 13 * * 1-5 |
13:00 | 6:00 AM | 5:00 AM |
DailyOrchestrator_EndOfDay |
0 30 20 * * 1-5 |
20:30 | 1:30 PM | 12:30 PM |
IncomeSleeve_MonthlyReinvest |
0 30 13 1-7 * 1-5 |
13:30 | 6:30 AM | 5:30 AM |
IncomeSleeve_QuarterlyAudit |
0 0 14 1-7 1,4,7,10 1-5 |
14:00 | 7:00 AM | 6:00 AM |
Which column applies: the planned validation window (2026-06-11 → 2026-09-03) falls entirely in PDT (summer); the PST (winter) column takes over only if the run extends past US DST end on 2026-11-01.
Weekdays only (1-5). The orchestrator's own calendar/no-trade-window logic decides what
actually happens inside a run; the timer just fires.
Income timer footnote: the two income crons restrict day-of-month (1-7) AND
day-of-week (1-5), so each can fire on multiple days — potentially every weekday — of the
first week of its month(s); the audit additionally fires only in Jan/Apr/Jul/Oct (1,4,7,10).
That is expected: the in-code gate decides what actually runs — the first trading
weekday of the month for the reinvest (every other firing logs an Information skip and
returns), and the not-implemented skip for the audit (see the expected-behavior subsections
below). Do not "fix" the crons.
The first IncomeSleeve_MonthlyReinvest run of the validation window lands Wed 2026-07-01
(the first trading weekday of July) at 6:30 AM PDT. The default posture is
recommendation-only (IncomeSleeve:OrderPlacementEnabled=false — S6-001 locked
decision): the run computes the reinvestment plan and reports it; it places nothing.
- A neutral (unbordered) Discord message titled
Income Reinvest Plan — Wed Jul 1, 2026arrives, with a Posture field readingRecommendation-only; IncomeSleeve:OrderPlacementEnabled=false. NO orders were placed. - NO orders appear in TWS paper. Orders on this timer exist only if the owner explicitly
flipped the flag to
truebeforehand. - An empty sleeve legitimately yields
No buys proposed.— that message arriving at all is proof the timer ran, not a failure. - Other weekday firings in the Jul 1–7 window log an Information skip in the worker log
(
Monthly reinvest skipped — Not the first trading weekday of the month …) and send nothing — expected (worker console or%LOCALAPPDATA%\TradingSystem\logs— section 5). - If the gate day is a market holiday, the run degrades like any closed-market day — the recommendation-only output is benign (accepted edge; no holiday calendar on this timer).
- The income sleeve's PDR-004 ≥12-week clock starts at its first actual trade, not at plan generation — recommendation-only months do not advance it.
Before flipping IncomeSleeve:OrderPlacementEnabled to true (an owner action, never an
ops/triage step), confirm all of:
- the prior recommendation-only
Income Reinvest Plan — <date>Discord report arrived and its proposed tickers match known income-sleeve holdings, quantities are non-zero, and estimated cost is within the available-cash estimate shown in the message; - TWS is running in paper mode;
- the IB LIVE account is NOT the active TWS session.
IncomeSleeve_QuarterlyAudit is not implemented (deferred per backlog, S7+). On each of
its firing day(s) in the Jul 1–7 window, the worker console/log shows one Warning line:
IncomeSleeve_QuarterlyAudit is not implemented — skipped (deferred per backlog, S7+). RunId: <id>
No Discord message is sent — silence here is expected, not a dropped alert. Do not triage it.
All messages arrive on the same webhook channel. Titles below are exact. Embed border color tells you the class at a glance: red border = risk-stop alert, orange border = operational failure, unbordered/neutral = normal digest.
| Message (exact title) | Meaning | Operator action |
|---|---|---|
Daily Report — <date> (e.g. Daily Report — Wed Jun 11, 2026) |
EOD digest: trades, realized/unrealized P&L, positions, market regime, ADR-023 cost breakout | None — read it. Its absence on a trading day is a triage trigger (section 5) |
Sleeve Readiness (paper-validation gate) (embed appended to the Friday daily report) |
PDR-004 gate progress per sleeve (hit rate, profit factor, drawdown, weeks observed) | None — track gate progress week over week |
Income Reinvest Plan — <date> (e.g. Income Reinvest Plan — Wed Jul 1, 2026) |
Monthly income reinvest digest (neutral): posture line, available-cash estimate, sleeve value, category drift, proposed buys (or No buys proposed.) |
Verify NO orders landed in TWS paper unless IncomeSleeve:OrderPlacementEnabled=true was explicitly set (section 3 expected-behavior); note the firing in the Run Log |
Broker Connect Failure — Pre-Market |
Worker could not connect to TWS; options sleeve skipped for the day | Check TWS is running/logged in, port 7497, trusted IP; fix before the EOD timer |
Broker Connect Failure — End of Day |
Worker could not connect to TWS at EOD; no snapshot written for the day | Fix TWS; the day is a snapshot gap (tolerated, but log it in the Run Log) |
Broker Connect Failure — Monthly Reinvest |
Worker could not connect to TWS on the reinvest gate day; no plan generated, no report sent | Fix TWS as above. No catch-up run: the next reinvest evaluation is next month's gate day — drift-based plans self-correct |
Orchestration Run Failure — Pre-Market |
Unhandled exception in the pre-market run (exception type in the message) | Check the worker console / local log files (section 5) for the failure; rerun is not automatic |
Orchestration Run Failure — End of Day |
Unhandled exception in the EOD run; snapshot may not have been written | Same as above; verify whether data/snapshots.json has today's entry |
Orchestration Run Failure — Monthly Reinvest |
Unhandled exception in the reinvest run (exception type in the message); plan/report may not have been produced | Check the worker console / local log files (section 5) for the runId in the alert |
Daily Risk Stop Triggered |
Daily P&L breached the daily stop threshold (alert-only in paper — nothing is halted) | Note it; verify the EOD snapshot carries the flag. No parameter changes |
Weekly Risk Stop Triggered |
Weekly P&L breached the weekly stop threshold (alert-only in paper) | Same as daily stop |
Drawdown Halt Triggered |
Drawdown breached the halt threshold (alert-only in paper) | Same as daily stop |
Discord:Enabled=false— every send is skipped by design. The worker logs a one-time Information notice at startup ("Discord risk alerts are disabled (Enabled=false)…") and per-call Debug skips. For the validation run this flag must betrue.- Dropped alert (
AlertDropped=Truein the rendered log) — Discord was reachable in principle but delivery terminally failed (non-429 4xx/5xx, retry budget exhausted, timeout, or transport error). The alert is gone and will NOT be retried; the only record is the log signature described in section 5. - Dead-man gap (worker not running) — no worker means no runs, no alerts, and no "I'm down" message. Nothing can tell you from Discord. Section 5, first entry.
IncomeSleeve_QuarterlyAuditnot implemented (S7+) — fires on weekdays in the Jul 1–7 window; one Warning log line, no Discord message; see section 3 for the exact log line.
Check entries top-down; the first one is the one Discord can never tell you about.
Where the logs are: every triage row below points at the local sinks — the worker
console, and the log files under %LOCALAPPDATA%\TradingSystem\logs when the worker runs as
the TradingSystem-FuncHost scheduled task (that task's output location). (App Insights is
intentionally disabled for this run — APPLICATIONINSIGHTS_CONNECTION_STRING is unset,
KD-005/KD-006 — so its absence is by design, not a gap to fix.)
| Symptom | Likely cause | Action |
|---|---|---|
| Nothing at all — no report, no alert, on a trading day | Worker (or the whole box) was not running — the dead-man case | Check this first each morning. Confirm the worker process is up and the startup banner shows all 4 functions (section 2 step 5). Distinguish crash from never-started: startup banner present in the console = the worker ran and then exited (check the exit reason in the console tail / %LOCALAPPDATA%\TradingSystem\logs); no banner at all = it was never started. If the box slept through a timer, that run is gone (no catch-up); log the gap day in the Run Log. The TradingSystem-GapDayMonitor task (2:30 PM weekdays — see the gap-day monitor subsection below) automates this check and alerts when the day's snapshot is missing |
Dead-Man Alert — No EOD Snapshot for <date> (orange; sent by the gap-day monitor task, not the worker) |
The 2:30 PM TradingSystem-GapDayMonitor task found no entry for today in data/snapshots.json — the EOD path never ran (worker down, box asleep, or EOD failure: the dead-man case above). On a market holiday this is an expected false positive — the monitor is weekday-only with no holiday calendar (~9/year, accepted by design); ignore it |
Holiday → ignore. Otherwise triage the dead-man row above (worker up? banner? local logs), then record the gap day in the Run Log |
| Worker IS running, timers fired (visible in log), but no Discord message arrived | Discord unreachable (or webhook broken) at the moment an ops/risk alert was sent — the alert was dropped (dead-man gap on the alert path) | The ONLY signal is the local worker log (console, or the files under %LOCALAPPDATA%\TradingSystem\logs): the signature is a LogError line from DiscordRiskAlertService matching "… alert NOT delivered and will not be retried — … AlertDropped=True …" (rendered console form — capital T); search the log files for AlertDropped=True. Operator action: treat the logged title as the alert you never received — triage its underlying failure first, then fix Discord delivery (webhook URL valid? Discord up?), and log the incident in the Run Log |
| No daily report by ~15 min after the EOD timer, but no orange alert either | Report send failed silently is not possible without a log — check worker log for report-path warnings; also re-check the dead-man cases above | Worker log around 20:30 UTC (12:30 PM PT winter / 1:30 PM PT summer); data/snapshots.json tells you whether the EOD run itself happened |
Broker Connect Failure — … alert |
TWS not running, not logged in, API disabled, or port/client-id mismatch | Match TWS API settings against IBKR:Host/Port/ClientId in local.settings.json; restart TWS; pre-market failure skips the options sleeve, EOD failure skips the snapshot |
Broker Connect Failure — Pre-Market on a Monday morning |
TWS weekly auto-restart (Sunday) left it at the restart prompt or logged out — the restart can also reset API settings | Accept the restart prompt, re-login to the paper account, then re-verify API settings: socket port 7497 and trusted IP 127.0.0.1. If the pre-market timer already passed while TWS was down, that run is gone — log a gap day in the Run Log |
Orchestration Run Failure — … alert |
Unhandled exception (type name is in the alert) | Worker console / %LOCALAPPDATA%\TradingSystem\logs — search for the runId in the alert |
Gateway down (health check fails, or log line Direct API fallback disabled; gateway miss → deterministic rules) |
claude-gateway process not running or CLI session expired | Expected degrade, not an incident: regime classification falls back to deterministic rules with zero metered spend. Restart the gateway; check GET /health/cli for credential status. Do NOT enable the metered fallback as a remediation |
Snapshot missing for a day (data/snapshots.json) |
Any of the above on the EOD path | Identify which from logs/alerts; record the gap day in the Run Log |
tools/ops/Check-DailySnapshot.ps1 (S6-004) closes the dead-man blind spot in the first
triage row: a down worker can never tell you it is down, so an independent scheduled task
checks each weekday at 2:30 PM local (≥45 min after the EOD timer in both DST regimes)
that today's entry exists in data/snapshots.json, and posts an orange
Dead-Man Alert — No EOD Snapshot for <date> embed when it does not. The webhook URL is
read from the gitignored local.settings.json (Discord:WebhookUrl) and is never logged.
Registration is a one-time manual operator step — it is never performed by CI, tests, hooks, or agent pipelines:
pwsh -NoProfile -File D:\Source\TradingSystem\tools\ops\Register-GapDayMonitorTask.ps1Run this as the normal dev-box user — no elevation required; the task is registered to run as the current user.
The script is idempotent (re-running replaces the task). Verify with
schtasks /query /tn TradingSystem-GapDayMonitor /v /fo LIST.
At the end of the 12-week run, decommission the task:
Unregister-ScheduledTask -TaskName TradingSystem-GapDayMonitor -Confirm:$false.
Exit-code legend (visible in Task Scheduler history — the code tells you what happened without opening logs):
| Exit code | Meaning |
|---|---|
| 0 | Snapshot present (or weekend — nothing expected) |
| 1 | Snapshot MISSING; alert skipped (Discord disabled or webhook not configured) — still investigate |
| 2 | Snapshot MISSING; alert delivery FAILED (Discord unreachable) — investigate both |
| 3 | Snapshot MISSING; alert delivered — triage per the table above |
Optional controlled drill (renders a real alert without waiting for a genuine gap day):
Check-DailySnapshot.ps1 -Date <past-weekday-with-no-snapshot> — note it in the Run Log as
a drill. Use -WhatIf to evaluate without posting.
Restating the repo trading rules as standing ops policy for the entire validation window:
- SANDBOX only. SANDBOX→LIVE requires explicit human approval — it is not an ops action and nothing in this runbook authorizes it.
Claude:DirectApiFallbackEnabled=falsestays false. Zero metered API spend; the gateway-or-rules degrade (ADR-029/ADR-030) is the designed behavior. No triage path in this document ends with "turn the fallback on".- Stops are alert-only in paper. Risk-stop alerts (red) record threshold breaches; they do not halt anything, and no halt behavior is to be added mid-run.
- No risk-parameter or sleeve-allocation changes during the run without explicit human approval — changing them invalidates the validation window.
- All trading logic is deterministic; AI (gateway) is analysis-only.
- (a) Retrieve the Discord webhook URL from the Bitwarden "ClimbOn Co" org vault into
the gitignored
src/TradingSystem.Functions/local.settings.json(.gitignore:16). The URL must NEVER be committed, logged, or echoed — write it into the file with an editor/file tool, never via shellecho/string interpolation or a shell variable (it would land in shell history). SetDiscord:Enabled=true,Reporting:WeeklyScorecardDay=Friday. Also retrieveClaude:GatewayApiKeyfrom the same vault into the same file; leaveClaude:ApiKeyas its placeholder — the metered key is not needed under the defaultDirectApiFallbackEnabled=falseposture. - (b) Send a test operational alert through the production
DiscordRiskAlertServicepath (S5-003 /DiscordWebhookGuard) and confirm it renders in the Discord channel. - (c) Start gateway + worker; confirm gateway preflight (regime call resolves gateway-or-rules and logs its source) — first scheduled run covers this.
- (d) Record the validation start date in the Run Log below.
- (e) Fill KD-001's resolution date in
DECISIONS.md— wording owned by S5-006, date filled at sprint close (not this document's job).
Day-0 steps (a), (b), (d) executed 2026-06-10 during S5-004 — test alert delivered (HTTP 204). Update Run Log dates if the run start slips past 2026-06-11.
- Day 1: after the first scheduled EOD run,
data/snapshots.jsongains an enriched entry for the day (trades, commissions, realized P&L, SPY/VIX close, regime) and the daily report arrives on Discord. - First Friday: the daily report carries the
Sleeve Readiness (paper-validation gate)embed; non-Friday reports carry only the core digest.
The first row anchors the validation start date (locked decision: the clock starts at sprint close). Target ≥12 weeks of observed paper trading per ADR-010/PDR-004 before any gate evaluation.
| Date | Event | Notes |
|---|---|---|
| 2026-06-10 | Day-0 configuration completed | Webhook provisioned from Bitwarden "ClimbOn Co" into gitignored local.settings.json; test alert delivered through the production alert path. Validation run starts the next trading session: 2026-06-11. Earliest 12-week gate evaluation: on/after 2026-09-03 (12 calendar weeks; verify against trading-day count if PDR-004 means observed trading weeks) |