The Morning Run

One oracle artifact — analysts ground on it, the heat map renders it, the client receives it.

viska.morning-run/0.1 · Spec, status proposed · ViskaStrat · 2026-08-14
vault: research/2026-08-14-morning-run-proposal.md · basis: invested_capital, USD only

ANALYSTSHEAT MAPCLIENT SLACK

The fundstate oracle emits one payload per statement day, focused on performance between IBKR statements — newest book vs previous book, plus live drift since the newest book. All three consumers read the same stored database row. The heat map gets the live, asset-level feed the removed heatmap surface has waited on since 06-28 — data-backed, never the sketch. d7/d30 windows ride along from viska.fund-positioning/0.1 (shipped, 2f5aef9).

Payload skeleton

{
  "schema": "viska.morning-run/0.1",
  "status": "fresh | stalled",          // stalled = book did not advance by deadline
  "basis":  "invested_capital (broker marks, IBKR sleeve); USD only",
  "clocks": { "book_as_of", "prev_book", "fx_as_of", "quotes_as_of" },
  "fx":     { "rates", "as_of", "source" },        // ONE fx clock per document

  "statement_window": {                 // the between-snapshots leg
    "delta_usd", "trade_flow_usd", "market_contribution_usd",
    "transitions": { "added": [], "dropped": [], "resized": [] }
  },
  "live_drift_usd",                     // null if any line unquoted — never 0, never stale

  "heatmap": [                          // exactly what a tile needs
    { "ticker", "theme", "weight_pct",  // invested_capital basis
      "d1_pct",                         // prev_book -> book, broker marks
      "live_pct",                       // book -> live quotes
      "unrealized_pct" }                // vs cost basis — total the fund sits on
  ],

  "windows": { "d7": {…}, "d30": {…} }, // from viska.fund-positioning/0.1
  "totals":  { "invested_capital_usd", "unrealized_usd" },
  "gaps": []                            // oracle gaps + stalled-clock entries, passed through
}

The chain — producer computes, N8N transports, the row is the truth

  1. Oracle (STRAT) grows one authenticated read route /api/morning-run — a composition over legs the server already serves. All figure logic in the producer with a --check; the oracle stays a read-only publisher.
  2. N8N triggers on statement arrival, not wall clock: poll /api/latest for book_as_of advance (observed at the ~14:00Z capture), deadline ~16:00Z. On deadline without advance the run fires anyway with status: stalled — a first-class state, never a silent absence.
  3. DB (ViskaDB) owns morning_run_payloads (PK report_date) — the proven daily_analysis_payloads posture verbatim: authenticated SELECT, no anon, handle scan on the LOAD path, a row IS the grant.
  4. Slack renders from the STORED row, not the API response — render-after-persist, so the client reads exactly what seats and agents query. English copy, every figure basis-labelled, no handles, no third-party prose.

Analysts ground on the same row: the day's dispatch packet cites report_date, and orientation starts from a payload nobody hand-composed.

Why this is cheap

No new data source anywhere — every figure already exists in oracle snapshots. The producer is a composition (~1 day with checks; the d1/live/counterfactual math shipped this week). The DB table is a sibling of a proven pattern. The N8N leg is a poll, two HTTP calls and a Slack post on existing credential lanes. FE's heat map is the one genuinely new render, and it reads one table.

Schedule posture

Born HAND-RUN. Earns its timer per every-leg-earns-a-schedule-once-proven: scrubbed scheduler env · idempotent re-run per report_date · exit codes distinguishing fresh / stalled / blocked / failed. Terminal state SCHEDULED+ALERTING — a failed morning run reaches a human, not a log.

Seat cut

seatowns
STRATpayload contract, producer + --check, route
DBDDL, grants, loader, handle scan
N8Ntrigger, transport, Slack render, alerting
FEheat map render from the row
viska-pmcontract registration (one step, one contract)

Amendment 14b — embed the built heatmap (adopted)

AssetHeatmap.tsx is already live: treemap, tile area = share of invested capital, colour = 24h market change or unrealized P&L, grouped by the four majors. The morning report embeds this component — FE's slice shrinks to a data adapter. Same component, two data modes: dashboard stays on live feeds; the morning report runs payload-driven from the stored morning_run_payloads row, so the page shows the frozen morning state analysts ground on and the client received.

Basis discipline: the payload's d1_pct is broker-mark book-to-book; the live view is feed-clocked. The embedded views are therefore relabelled from the envelope — "Statement · d1" and "P&L" — never an unlabelled mix of clocks on one surface. Slack cannot mount React: the delivery carries compact figures plus a link to the report page; PNG export is out of scope v0.1.

Open questions

1 · Trigger. Statement-advance detection (recommended) vs fixed wall clock. The day's statement lands ~14:00Z — a 07:30Z run would carry a two-day-old book. If "morning" must mean Iceland morning, the honest 07:30Z variant leads with live drift and labels the book one day older.
2 · Slack shape. Figures in the message vs a one-line signal linking into the app. Channel and audience list.
3 · d30 fills in. The oracle store was born 2026-07-28, so d30 is clamped (declared in-payload) until ~08-27.