What this is for
The standing list. Everything below was asked for out loud; where the wording is Evan's it is quoted, and where it is my inference it says so. Correct anything that reads wrong — this file is the spec, and the code answers to it.
Last updated 2026-09-03.
The two objectives
Everything after these is a requirement in service of one of them. If a piece of work does not serve objective 1 or objective 2, it is not this service's job.
OBJECTIVE 1 — Post-mortem for human and machine learning
"postmortem for human and machine learning" "we are a new trading company, and we want to track what our traders did during their trades and how they can improve"
One scoring pass serves two consumers, and they need the same numbers for different reasons:
- The human gets a card and a weekly digest — what the plan would have done, what they did, and what the difference cost.
- The machine gets labelled rows. The mechanical outcome is the label (trainer#4): train on the realised outcome instead and the model learns your traders' habits, including their mistakes, and a good setup someone panicked out of at −0.4R teaches it to avoid setups like it.
The three lines exist so one computation answers both:
MECHANICAL the issued plan resolved against the tape -> the SETUP -> the label
REALISED what the account actually did, AFTER COSTS -> the ACCOUNT
TRADER DELTA realised - mechanical -> the PERSON -> the coaching
Status: scoring works end to end. The full closed book has been scored once (63 of 95 rides in the licensed window). Costs are not yet computed, so REALISED is currently overstated on every ride. Cards, digests and trainer rows do not exist.
OBJECTIVE 2 — Feed the learnings into candidate selection
"how can we feed learnings from the postmortem into things like stock candidate selection?" · "very good point — make that the second major objective"
The desk already runs a version of this loop, on the wrong basis: its live
grade:B- and source:catalyst-scan findings are computed from realised
outcomes, which are the setup plus the person. They cannot separate "B−
selection is bad" from "B− trades were mishandled", and those have opposite
fixes.
Mechanical R is the setup alone. That is the contribution: not a new loop, a decontaminated basis for the one that exists. Four feeds, in order of worth:
- Grade calibration, decontaminated — conviction attracts size and intervention, so the grades the desk believes in are the ones realised understates most.
- Scanner and source quality on the same basis.
- Level-kind attribution —
plan_jsonalready carries reasoned levels (prev-highweight 3,roundweight 1). Doprev-highstops resolve better thanswing-low? AGROUP BY, not a model. - Cost-aware selection — the desk's 2-to-1-after-spread floor uses a modeled spread. Measure the real one per liquidity band and it kills candidates before they are taken. On 34 BTC rides, $9.22 of an $11.54 loss was spread rather than strategy.
Nothing auto-applies. observe → propose → gavel → apply → measure. Thresholds and suppressions are settings (reversible, audited); the grade formula is code, so a finding there is a proposal for a human.
The ratchet, which must not be built: a finding that suppresses B− candidates stops the desk generating B− data, so the finding can never be revisited — the first noisy result closes a door and removes the evidence that would reopen it. Any suppression needs a holdout.
Status: not started, and correctly blocked. Every selection statistic available today is survivorship-biased, because grading a selector on TAKEN trades measures selector-plus-human. The correction is the cut plans, and those are ~51, not 228. Full detail: feeding selection.
Supporting requirements
3. Per trader, and the desk as a whole
"these have to be tracked per user" · "and as [a whole]"
Four reads over one table: the trader's own record, human-vs-machine (the Hands Tax), the lane's record, and the desk.
Status: BLOCKED, and it blocks everything per-person. No ride table carries
a user_id; attribution lives in a free-text decided_by splitting ~2,050
machine to 67 human, and two of those users rows are the same person. Every
delta the service produces today has no owner.
See attribution.
4. Findings go back into the system
"we then want to take the findings, and plug them into our system for improvements"
The general form of objective 2. A report nobody acts on is a diary.
alina_gavels exists for exactly this and has never been used.
Status: not started. learning_findings needs a user_id and a scope
before a finding can belong to a person.
5. Answer questions about the future, sometimes in real time
"we also want to use this information to potentially answer questions about the future based on past behaviors, sometimes in real time"
- "Will this trade work?" — outcome prediction. Not here; that is the trainer's, and this service feeds it labels.
- "What will this trader do next?" — behavioural, and a base rate is not a model: "you have cut here 14 of the last 18 times".
Real-time shape: a lookup, not an inference — profiles computed nightly, matched live. The box is database-latency-bound.
Status: not started, and gated behind the same survivorship problem as objective 2.
6. A separate service
"i don't want this tied directly to tradingdesk because this is probably going to be a separate service"
Its own repo, its own store, no imports from tradingdesk, no write path into
its ledger. Status: done, enforced structurally.
The market-data plane is also not ours: "we have slowsocket… it uses Massive as a source and then fans out to anywhere we need it." Massive's historical REST is the scorer's tape (≤ prior close); slowsocket is today's. They tile exactly.
7. Maximum free history, and never a bill
"lets do the max history that is free" · "i don't want to pay any additional pulls"
Bars free at any depth; ticks free inside a rolling ~12-month window; $6.00/GB outside it.
Status: daily bars, minute bars and splits landed for the traded set — 99,061 + 28,634,289 rows and 106 corporate actions, 4 years, $0.00. Ticks not pulled into the lake (~37 GB, free) — note the scorer does not need them, it fetches per ride window. The cost gate fails closed and is tested.
8. One store per job, not one merged database
"what db and structure will we merge all these datasources into?"
The desk's RDS for decisions, S3 + Parquet for the tape, this service's own Postgres for scores. The answer to "merge" is a contract and a lake. Status: built. See the lake layout.
9. A REST API and a WebSocket
"i want this service to have a REST API and a websocket"
Status: done. See the API. Compose adds api + worker + store + redis; slowsocket and DuckDB are deliberately not containers.
10. Docs that stay true
"make sure that goes in a docs folder with these kinds notes in .md format"
One finding per file, dated, evidence in it. Status: maintained. Two claims of mine have already had to be corrected here — the cut-plan sample and the issued-target recoverability — which is the point of writing them down.
11. The replay tape for the desk's 20-second lane
"give me the specs of what you would need built to replay a stock for a certain time period test against?" · "basically what slowsocket would deliver" (2026-09-04, tradingdesk#777)
The desk's replay reads one shape — the gzipped NDJSON its own recorder writes
— and that archive is two sessions deep and holds only held names. This
service already reads a year of free listing-venue ticks, so it writes the
recorder's shape from them: trades, quotes and status per symbol-day, sorted,
with a coverage manifest, whole or absent. The desk changes a base URL.
Status: built and checked against a fake vendor; not yet run against the live one (no key in the session that built it). See the replay tape, including the open questions the first real run answers.
What is actually blocking what
costs -> OBJECTIVE 1's third line, and cost-aware selection (obj 2 #4).
Fully unblocked: order.sent carries the quote on 1,869/1,869
rows and fill.slippage_bps is a built-and-null slot.
plan versioning -> cut plans at a useful size -> the survivorship correction
-> ALL of objective 2, and the real-time half of #5
attribution -> every per-trader number, the digest, #3, #4, #5
trainer rows -> objective 1's machine half
Open, in the order I would take them
- Cost drag. The only prerequisite that is fully unblocked, and it is a hole in the core unit rather than an addition.
- Emit labelled rows for the trainer — objective 1's other half.
- Plan versioning (desk-side) — unblocks objective 2 entirely.
- Attribution (desk-side migration) — needs Evan.
- Cards and digests — the surfaces a human reads. No UI without an explicit ask, then one whole pass.
- The tbbo lake pull, whenever; it blocks nothing.
- The first real tape build (#11) — one day of one name, to learn what mbp-1 and status price at and how big a day is. Then the population, once Evan names it.