The headline (read this first)
The amount decomposition Jane Street needs already exists — but in the wrong place. The delivered JSON (schema_version 2.0) already ships
fee_amount_typefee_amount_valuefee_amount_min_valuefee_amount_max_valuefee_amount_signfee_amount_unitfee_amount_currency
— but the database stores only the raw fee_amount text. Those typed fields are parsed on the fly at delivery time, unvalidated, and never persisted.
fee_amount_value, wrong fee_amount_currency, wrong sign — with nothing to catch it. There is no DB guarantee that the raw string and the parsed number agree. The fix is to move the parse left: persist the decomposition into typed, enum-constrained, validated columns, and have the serializer read stored fields instead of re-deriving them per delivery.
The design in one line: keep the exact delivery contract JS already consumes, but back every parsed field with a stored, enum-typed, CHECK-constrained column — so the delivery becomes a deterministic read of validated data, not a live re-parse.
1 · What the data actually looks like (measured)
1.1 The amount field is the core debt — but it's 88.6% clean
| Format bucket | Rows | Share | |
|---|---|---|---|
plain number ($0.50, ($0.25), $1,000) | 144,890 | 88.6% | |
number + extra text ($0.20 per side, $ (0.0020)) | 12,191 | 7.5% | |
percentage / bps (0.15% of Dollar Value, 0.398 bps) | 3,129 | 1.9% | |
non-actionable (varies, N/A) | 2,176 | 1.3% | |
| other free-text | 843 | 0.5% | |
| value + unit prose | 395 | 0.2% |
1.2 It's multi-currency — the USD assumption is wrong for ~7%
| Currency | Rows | Share | Examples in data |
|---|---|---|---|
| USD / bare number | 152,474 | 93.2% | $0.50, 1,000 |
| unknown / unattributable | 4,545 | 2.8% | parser can't confidently assign a currency |
| ZAR (South Africa) | 3,252 | 2.0% | R1,295.99, R3 074.69 |
| EUR | 3,141 | 1.9% | €0.15 bps, min €1.6, 1,500 EUR |
| GBP | 167 | 0.1% | £200 |
| NOK | 45 | 0.0% | NOK 3.21 |
Also in the wild: 2,375 bps-denominated amounts and 228 compound expressions (min/max/greater-of/€6k /100m). The 2.8% "unknown" bucket is the dangerous one — those are shipping today with a best-guess currency and no guard.
1.3 Enums already exist — but two are dominated by the "Other" catch-all
| Column | Type | Values | Catch-all load |
|---|---|---|---|
fee_action | enum | Make, Take, Open, Routed, Other, Close | Other = 58.9% (96,316) |
fee_participant | enum | Customer, Firm, Broker_Dealer, Market_Maker, Away_Market_Maker, Professional, Other, All | Other = 18.8% (30,746) |
fee_symbol_classification | enum | ETF, ETN, Equity, Index, N_A, All | N_A = 2.2% |
fee_symbol_type | enum | Penny, Non_Penny, N_A, Both | N_A = 4.6% |
fee_trade_type | enum | Index_Options, Multiply_Traded_Options, Simple_Order, Complex_Order | — |
fee_category | text (not enum) | 6 values only: Fees And Rebates, Market Data Fees, Port Fees And Other Services, Legal Regulatory Fee, Participant Fee, General Provisions | clean → promote to enum |
fee_charge_type | text | 6,530 distinct | descriptive label only — a cross-check that flags sign disagreements for review; NOT authoritative, NOT a tiebreaker (EDGA proved it can be wrong) |
fee_action='Other' at 59% means that enum isn't modelling reality (the true action is buried in fee_charge_type/fee_action_details); and fee_participant='Other' at 19% is a taxonomy gap. fee_category is a perfect 6-value enum still typed as text.1.4 Sparse free-text columns (null rates)
fee_basis 50.7% null · fee_monthly_volume 66.1% · fee_monthly_volume_criteria 68.1% · fee_conditions 56.5% · fee_action_details 36.0%. The two monthly_volume* columns mix units wildly (contracts, users, %, ADV, shares) — tier data trapped in prose.
1.5 Baseline (delivered) cut — the numbers that ship to Jane Street
The figures above are all-versions. The delivery ships baseline only (view_fee_schedule_results, is_baseline=true) — 75,426 rows / 45 venues. Delivery-accurate stats:
| Metric | Baseline (delivered) |
|---|---|
| plain-number amounts | 92.5% |
non-actionable (varies/N/A) | 1.2% (880) |
| sign by parentheses/minus only | 18.1% (13,647) — higher than all-versions |
| currency USD / unknown / EUR / ZAR / GBP·NOK | 95.6% / 1.9% (1,421) / 1.5% / 0.9% / 0.1% · bps 827 |
fee_action='Other' | 60.0% |
fee_participant='Other' | 18.2% |
fee_category distinct | 5 (baseline) |
2 · The delivery contract (what must not break)
Jane Street consumes, per exchange/day: detailed_fee_<date>.json = { metadata, fees[] }, plus an index.json manifest.
// index.json { "schema_version": "2.0", "generated_at": "…Z", "total_records": 558, "amount_types": { "flat": 548, "range": 0, "formula": 0, "varies": 10, "not_applicable": 0 }, "suppliers": [ { "exchange_name": "…", "record_count": 311, "confidence": { "average": 99.5, … } } ] } // each fees[] record (34 keys) — already carries the decomposition + confidence record_id, exchange_name, scraped_time, version_id, fee_type, fee_category, fee_charge_type, fee_amount, fee_amount_type, fee_amount_value, fee_amount_min_value, fee_amount_max_value, fee_amount_sign, fee_amount_unit, fee_amount_currency, fee_action, fee_action_details, fee_basis, fee_participant, fee_participant_details, fee_monthly_volume, fee_monthly_volume_criteria, fee_symbol_classification, fee_symbol_type, fee_trade_type, fee_symbol, fee_excluded_symbols, fee_conditions, fee_exclusions, fee_notes, fee_extra_info, changed, confidence, relationships
fee_amount_type/value/min/max/sign/unit/currency + confidence. Persisting those as stored, validated columns changes nothing in the delivered shape — same keys, same JSON — it only makes the values trustworthy. So the core hardening is backwards-compatible by construction.3 · Target model — persist & constrain the decomposition
Add stored columns on fee_schedules_result (keep raw fee_amount forever for audit). Every one already has a delivery-side equivalent, so the serializer just switches from "parse now" to "read column".
| Column | Type | Rule |
|---|---|---|
fee_amount (existing) | text | raw string, immutable audit trail |
fee_amount_type | enum flatrangeformulapercentagevariesnot_applicable | promote the delivery taxonomy to a stored enum |
fee_amount_value | numeric(18,6) | SIGNED; rebate/credit ⇒ negative. Required when type ∈ {flat, percentage} |
fee_amount_min_value / _max_value | numeric(18,6) | required when type = range; min ≤ max |
fee_amount_sign | enum chargerebatecredit | the single source of sign truth — no more parentheses-only encoding |
fee_amount_unit | enum per_contractper_shareper_tradepercentbpsflatper_monthper_port | parsed from trailing prose / basis |
fee_amount_currency | enum USDEURGBPZARNOK | was silently guessed — make it explicit + validated; unknown ⇒ review, never ship |
fee_category | enum (6 values) | promote from text — zero data loss (already only 6 distinct) |
fee_amount_basis | enum / null dollar_valueadvnotionalexecuted_share | for percentage/bps amounts |
completeness_score / correctness_score | int 0–100 | already delivered as confidence — make them first-class (drives the gate, §5) |
Existing enums stay; two get work: fee_action — add the real actions hiding in Other (59%) by mining fee_charge_type (e.g. routing, surcharge, cross, marketing); fee_participant — resolve the 19% Other via a value→enum alias map (e.g. Professional Customer, DPM, Lead Market Maker).
4 · Validation constraints (each ties to a measured anomaly)
| # | CHECK / validator rule | Guards (measured) |
|---|---|---|
| V1 | fee_amount_sign='rebate'/'credit' ⇒ fee_amount_value ≤ 0 and vice-versa | 25,469 fees (15.6%) sign-by-punctuation only |
| V2 | fee_amount_currency ∈ enum; unknown ⇒ review queue, never delivered | 4,545 (2.8%) currency-unattributable + 6.8% non-USD |
| V3 | type='flat' ⇒ value NOT NULL; type='range' ⇒ min ≤ max NOT NULL; type='varies' ⇒ notes/formula required | 2,176 bare varies/N/A |
| V4 | parsed fee_amount_value must reconcile to the numeric core of raw fee_amount (± tolerance) | the whole "unvalidated delivery-time parse" risk |
| V5 | chunk_id/chunk_index NOT NULL & resolvable → provenance | fabricated-row risk from scoring batch |
| V6 | fee_category / all enum cols reject unknown labels at write | keeps the 6-value category + enums clean |
| V7 | per (version × chunk): completeness reconcile — extracted ≥ stated atomic fees | MIAX Options ch234/244 (0%), PHLX ch103 (30%) |
| V8 | sign-vs-fee_charge_type disagreement → flag for source review before delivery (never auto-resolve) | the EDGA class — see below |
$0.50 vs $0.506), a source_footnote_ref + V4 tolerance records it as correct so the scorer doesn't false-flag it.Direction rule (locked — matches what goes to Jane Street)
fee_amount_signis derived from the amount form (the source's own parentheses /Fee/(Rebate)columns). It is the field to trust.fee_charge_typeis a descriptive label — NOT authoritative, NOT a tiebreaker. The EDGA equities rows proved it can be wrong in delivered data.- A disagreement means an extraction-defect candidate on our side — V8 flags it for source verification before ship;
fee_charge_typenever overrides the sign.
V8 in practice — the EDGA sweep (run 2026-07-31, 75,426 delivered rows)
Jane Street found 6 EDGA rows ((0.00270)/(0.15000%) parenthesised = rebate, but fee_charge_type='Fee'). Verified in prod: the sign is correct (traces to Cboe's "Fee/(Rebate)" columns; code W removes = plain 0.00300 charge confirms the bracket convention is Cboe's own) and fee_charge_type='Fee' is the mislabel. Running V8 across all delivered rows surfaces the siblings before the client does:
- 7,217 total sign-vs-label disagreements across 24 venues (candidates, not all defects — e.g.
Distribution Fee Waiver ($3,500)is a correct credit whose label just contains "fee"). - ~109 in the EDGA-layout family (EDGA 29 / EDGX 32 / BZX 42 / BYX 6) — the exact venues sharing EDGA's split-header template.
- Two sub-classes: 573 parenthesised-but-labeled-charge (the EDGA class) and 6,644 bare-positive-but-labeled-
Rebate/Credit— candidates for the "sign echoing printed text" defect (a rebate shipped as a positive charge; the next grounding-sweep target).
5 · Confidence-gated delivery
The scores already ride in the delivery as confidence. Make them an operational gate on the serializer:
- Green — correctness ≥ 90 & completeness ≥ 95 & 0 validation failures ⇒ auto-deliver.
- Amber — 60–90 or isolated V1/V4 flags ⇒ deliver with a flagged-rows manifest in
index.json. - Red — currency unknown (V2), completeness < 80 (V7), value/raw mismatch (V4), or correctness < 60 ⇒ hold + route to review before it reaches Jane Street.
6 · Rollout — backwards-compatible, JS never sees a break
- Add the stored columns (nullable) + enums alongside the raw text. Non-breaking DDL.
- Backfill with a deterministic amount parser over all 163,624 live rows; diff the result against today's delivery-time parse — same JSON keys, so any divergence is a real parser bug to fix, not a contract change.
- Shadow-validate V1–V7 in report-only for one delivery cycle; tune currency detection + participant/action alias maps against the residuals (esp. the 2.8% unknown-currency + 59% action=Other).
- Flip the serializer to read stored columns instead of re-parsing. Byte-compatible output ⇒
schema_versionstays 2.0. - Enforce CHECK constraints + wire the confidence gate into the SFTP delivery step. Additive changes (new enum values, new fields) ⇒ bump to
2.1under an additive-only contract rule; never remove/rename a delivered key.
7 · Priority order (highest ROI first)
- Persist + constrain
fee_amount_value+fee_amount_sign— kills the 15.6% sign-in-punctuation risk, the biggest correctness exposure. - Stored
fee_amount_currencyenum + V2 — stops the 2.8% unknown / 6.8% non-USD from silently shipping. - V4 raw↔parsed reconciliation — the guard that makes the delivery parse trustworthy.
- Promote
fee_categoryto enum — trivial, zero-loss, immediate. - Re-derive
fee_action(59% Other) + participant alias map — the biggest queryability upgrade. - Structured tiers from
fee_monthly_volume*— last, highest effort, mostly-null today.
8 · Implementation effort & phasing
Smaller than it looks — the hard parts half-exist (delivery already parses amounts; 5 enums already there).
Phase 1 — The hardening ("Jane Street can't break")
Persist + validate the amount decomposition, currency safety, raw↔parsed guard, promote fee_category, confidence gate, flip the serializer to read stored columns.
| Workstream | Effort |
|---|---|
| Amount parser (extract existing delivery logic → shared module; add currency/bps/compound) | 3–5d |
| DDL + enum types + Alembic migration | 1–2d |
| Backfill 163k rows + diff vs today's delivery parse (find real bugs) | 2–3d |
| V1–V7 constraints + pre-delivery validator (shadow report-only) | 2–3d |
| Serializer switch + confidence gate, byte-compat check | 2–3d |
≈ 10–16 person-days → ~2–3 weeks (1 engineer), ~1.5 weeks (2 engineers). Plus one delivery cycle of shadow-validation calendar time (waiting, not work).
Phase 2 — Enum enrichment (fee_action 59% Other, participant aliases)
Bigger/fuzzier — the fix lives in the extraction pipeline (the AI assigns these): new taxonomy + prompt/logic changes + re-run. ≈ 4–6 days + ongoing tuning. Needs Muizz/Haris's eyes.
Phase 3 — Structured tiers (fee_monthly_volume)
Lowest ROI (66% null), highest parse effort. ≈ 3–4 days. Optional / defer.