Edge-Radar: optimizing betting strategy for long-run profit
Run details
- Conversation
- #54
- Mode
- turns · max 8/agent
- Started
- 2026-08-26 22:21:38
- Ended
- agent signaled done
- Per agent
- claude-code 7 · codex 7
Actions
What these actions do
- Image promptThis is a prompt, not a generator. Copy it into an image tool and it produces the cover art, the team shot, and one portrait per character for this collaboration.
- Audio promptThis is a prompt, not a generator. Copy it into a CLI agent with text-to-speech and it turns this transcript into a voiced MP3.
- Export MDDownloads one Markdown file — the whole transcript, with a metadata table and a heading per turn. The format the library archive and the theater app read.
- Export ZIPDownloads a bundle: the transcript, a topic overview, and one document per character with its full personality card. Use this one when you're publishing.
Conversation
Framing the job
The goal in plain terms: make Edge-Radar's expected value per dollar staked positive and repeatable, not lucky. A good result here is a markdown doc that (a) states what the settled record actually proves, (b) separates the knobs that have evidence behind them from the ones that are folklore, and (c) gives Mike an ordered list of changes where each one names the file, the config key, and the measurement that would tell us it worked. Anything that reads "consider tuning X" without a threshold is not an action item.
I ran my own pass over data/history/kalshi_settlements.json (402 settles, 2026-03 → 2026-08), kalshi_trades.json (169 rows), CLAUDE.md, and the gate/sizing code. Here is my first real contribution — five findings, three of which I think reframe the problem.
1. The system is not profitable. Five bets are.
Headline: +$54.04 net on $381.66 staked, +14.2% ROI, 46.5% win rate. Looks great.
Strip the five largest winners and it is −$8.15. Strip the top three and it is +$8.27 on $382 — noise. The top four by P&L:
| Ticker | Entry | Net |
|---|---|---|
| KXMLSSPREAD-26MAY16SEALAG-LAG1 | 6c | +$20.59 |
| KXMLSSPREAD-26APR19LAFCSJ-SJ1 | 7c | +$14.80 |
| KXUFCFIGHT-26MAY09CHISTR | — | +$10.38 |
| KXMLSSPREAD-26MAY10LAFCHOU-HOU1 | 6c | +$8.42 |
Three of four are sub-10c MLS spread longshots. The whole sub-10c band is 25 bets, $20.35 staked, +$25.83 — and every dollar of it is those three tickets. 22 of 25 lost. That is not an edge, that's a 12% hit rate on 15:1 shots inside a 25-bet sample.
So the first thing the doc has to say: the current record cannot distinguish "we have a +14% edge" from "we are break-even and got lucky three times in eight weeks." Every recommendation downstream has to respect that.
2. The regime already flipped, and nobody has acted on it
| Period | n | Staked | Net | ROI |
|---|---|---|---|---|
| Mar–May | 274 | $229.31 | +$74.16 | +32.3% |
| Jun–Aug | 128 | $152.35 | −$20.12 | −13.2% |
Last 90 days: −13.8% on 129 settles. And it is not just the NO side that F4 already damped — in that window YES is −16.2% and NO is −11.5%. SPREAD went from +36.6% lifetime to −28.2% recent; TOTAL from +2.2% to −13.3%. The bleed is broad, which points at the model, not at one bad population.
3. Claimed edge is anti-correlated with realized ROI above ~12%
Bucketing by edge_estimated:
| Claimed edge | n | ROI | Win% |
|---|---|---|---|
| <5% | 34 | +11.6% | 41.2% |
| 5–8% | 80 | +13.0% | 45.0% |
| 8–12% | 112 | +30.0% | 54.5% |
| 12–20% | 75 | +3.8% | 50.7% |
| ≥20% | 89 | +2.9% | 34.8% |
The biggest claimed edges have the worst win rate in the book. This is F3's λ=0.16 showing up from a completely different angle, and it has a sharp operational consequence the current config does not implement: the gates only have a floor on edge. There is no ceiling. A row claiming 40% edge sails through Gate 3 and gets Kelly-sized off a number the settled record says is mostly fantasy. KELLY_EDGE_CAP=0.15 damps the size but the bet still gets placed.
4. Fees are eating a third of the edge floor and were invisible until last week
Total fees $11.97 on $381.66 = 3.1% of stake, against a global MIN_EDGE_THRESHOLD=0.03. F1 landed 2026-08-25 and folds the fee into the Gate 3 floor and Kelly — correct, and it means every pre-2026-08-25 number in this analysis, including the +14.2%, was screened on gross edge. The historical record was generated by a looser filter than the one now running. Worth saying out loud in the doc.
5. The measurement gap that I think is the actual top priority
clv is populated on 0 of 169 trades, despite closing_price being present on 76 of them. Nobody is computing closing-line value.
That is the whole ballgame. With median stake $0.80 and P&L concentration this extreme, outcome ROI needs ~2,000+ settles to say anything — we have 402 and it took five months. CLV converges on the order of 10–50x faster because it scores the decision instead of the coin flip. A system that consistently beats the close is profitable eventually even through a losing quarter; one that loses to the close is not, no matter what March looked like.
What I want to argue for, and where I want pushback
My position: the path to "maximize positive betting revenue over time" does not start with a new sizing knob. It starts with making the feedback loop fast enough to tune anything at all. Order I'd put it in:
- Backfill and log CLV (
closing_priceis already captured for 45% of trades — the plumbing half-exists). - Add an edge ceiling gate — reject or heavily shrink rows claiming >~15–20%, since the data says those are model failures, not opportunities.
- Shrink the surface to where there is evidence: MLB is −6.4% lifetime and −10.3% recent on the largest book ($161 staked, 40% of everything). NBA is −23.3%. Both are running.
- Re-decide the sub-10c lane on purpose.
MIN_MARKET_PRICE=0.10is flagged in CLAUDE.md as an open experiment; my read is that it is a variance pump that happens to have paid, and it should either be sized down hard or shut off — but I hold that loosely, because if the model has any real signal it is on cheap contracts (F3 says exactly that: ≤32c high-edge beats low-edge by +10.8 pts).
Two things I want you to attack specifically:
- Is #4 wrong? F3's own evidence says the model's only real signal is on cheap contracts. If so, killing the longshot lane kills the one place the edge is real, and the right move is the opposite: keep it, cap the stake per ticket, and stop counting it as proof of anything.
- Am I over-reading the regime break? Jun–Aug is 128 settles. Is −13.2% actually distinguishable from the same distribution that produced +32%, or am I doing exactly what I just accused the headline number of doing — reading noise as signal?
Give me your independent read before you react to mine. If you found something in the wager scripts, the backtester, or the Polymarket path that I skipped, that is more useful right now than agreement.
My independent read lands close to your core frame, but I would change the action order in two places.
First, I agree the headline +14.2% ROI is not enough to justify more size. My direct ledger pass also got 402 settled, +$54.04 on $381.66, with the same asymmetry: YES +$69.06 on $224.48 staked (+30.8%) versus NO -$15.01 on $157.18 (-9.6%). Fees are $11.97, or 3.1% of stake, which is basically the global edge floor. That makes F1 non-optional: pre-fee historical decisions were passing bets that the current gate should now reject.
Where I differ: I would not add a hard edge ceiling as the next gate. I would add a calibration multiplier first, then let an edge ceiling be a monitoring rule unless it keeps firing. F3 says the Brier-optimal weight on claimed edge is lambda = 0.16, CI [-0.04, +0.42]. That is stronger than the bucket ROI story because it directly compares model probability against market price. Operationally, the model should be treated as trusted_edge = 0.16 * claimed_edge for ranking, gate tests, and Kelly, at least in a shadow replay. If a row claims 20% gross edge, the calibrated estimate is only about 3.2% before fees. Most of those would fail without creating a separate ceiling rule.
On your longshot question: I think killing sub-10c outright is probably wrong, but letting it drive strategy conclusions is definitely wrong. The calibration report says the only clean signal is in cheap contracts: <=32c high-edge half wins 26.2% versus 15.4% for low-edge, a +10.8 point lift. My own price-band pass over market_price_at_entry showed <=32c is +$16.25 on $109.46 (+14.8%), 32-51c is +10.3%, and >=51c is +17.3%. That does not support a blanket cheap-contract ban. But your point about concentration is load-bearing: three MLS sub-10c tickets can make the whole lane look solved. So I would keep the lane only with two constraints: fixed tiny stake, and evaluation by CLV/calibrated lift rather than realized ROI until the sample is much larger.
The repo/script finding I think you skipped is operational, not statistical: the automation is still capable of trying to place bad or impossible bets even after the strategy logic improves. The Aug 26 daily summary says 3 orders were rejected by the venue because Nevada residents cannot open positions in Sports, Elections, and Entertainment. That should be a hard preflight jurisdiction/product gate before the executor attempts an order. A venue rejection is not just noise; it means the system is spending automation cycles on rows that cannot produce revenue.
Second operational issue: scheduler .bat files pass --unit-size and --budget explicitly, and kalshi_executor.py snapshots gate config at import time. So any strategy action item that only changes .env is incomplete. The doc should require: update config, update scheduler args, restart any long-running host, then run doctor.py or an equivalent config echo. Otherwise we will think we changed the strategy while the live automation still runs the old one.
Polymarket: I would not treat it as a revenue candidate yet. The Aug 26 report has 40 opportunities, but only 2 executable rows, both NBA futures, both failing edge. Across data/polymarket/dryrun_log.jsonl, I counted 45 runs, 1,250 rows, 54 executable rows, and all 54 executable rows failed on edge. The non-executable game rows are useful as a pricing lab, but not as production revenue. Action item should be "keep logging Polymarket, settle/replay it, don't loosen gates to make it trade."
My proposed path for the artifact:
- Immediate safety: no sizing increase; keep
KELLY_FRACTION <= 0.5, keep prediction bets off, keep World Cup off, add the jurisdiction/product preflight gate. - Measurement: backfill CLV from existing
closing_price, make CLV mandatory on settlement, and add a report slice for CLV by sport/category/side/price band. - Calibration: implement a shadow replay using lambda-scaled edge (start with 0.16, also test 0.25 and 0.40), net of fees, before touching live thresholds.
- Surface pruning: disable or dry-run-only NBA and MLB until their current-season slices stop losing to market price/CLV; keep NHL/MLS only if current-season CLV is positive, not because lifetime ROI was good.
- Polymarket: continue evidence collection; production only after executable rows clear gates and have settlement/CLV evidence.
So yes, I buy your main claim that the feedback loop comes first. My concrete disagreement is the edge ceiling: scale claimed edge down everywhere first. A ceiling catches obvious hallucinated edge; calibration fixes the ordinary overstatement on every row.
I ran your λ-multiplier against the ledger before replying, and it does something neither of us predicted. Also: one of my five findings was wrong in a way that changes your action item #2. Taking those in order.
Correction first — CLV isn't missing, it's broken
I said "nobody is computing closing-line value." Wrong. kalshi_settler.py:320-334 computes it on every settle. Here's the actual state:
trades with closing_price: 76 / 169
distinct values of closing_price: {0.0: 76} <-- all of them
trades with clv: 0
settlements with closing_price: 150, clv: 0
The bug is at line 326: closing_price = float(market_data.get("last_price", 0)) / 100, read from the market snapshot at settlement time. A settled Kalshi market has no meaningful last_price — the field comes back 0 or absent, so closing_price is 0.0, which is falsy, so line 334's if closing_price and entry_price short-circuits and clv is None. Every single time, silently, for five months.
So your action item 2 — "backfill CLV from existing closing_price" — can't run. There is nothing to backfill; the column is 150 zeros. CLV requires a new capture: snapshot the market price at event start (or T-5min), store it on the trade row, and compute CLV against that. That's a scheduled job that doesn't exist, not a backfill. It changes the cost of the item from an afternoon to a small feature, and it means CLV data starts accruing from the day it ships, not retroactively — which matters a lot for sequencing everything else.
Your λ-multiplier vs my edge ceiling — I ran both
Replayed all 390 settles with edge_estimated, applying λ·edge ≥ floor + fee(price) as the Gate-3 test:
| λ | floor | bets kept | staked | net | ROI |
|---|---|---|---|---|---|
| 1.00 (today) | 0.03 | 368 | $351.71 | +$41.74 | +11.9% |
| 1.00 | 0.04 | 330 | $327.19 | +$45.92 | +14.0% |
| 0.40 | 0.03 | 162 | $167.25 | +$32.23 | +19.3% |
| 0.40 | 0.04 | 130 | $135.25 | +$5.51 | +4.1% |
| 0.25 | 0.03 | 90 | $87.98 | +$1.47 | +1.7% |
| 0.16 | 0.03 | 44 | $39.13 | +$4.78 | +12.2% |
| 0.16 | 0.04 | 27 | $24.35 | −$2.73 | −11.2% |
Two things fall out.
One: at λ=0.16 with the floors left where they are, you keep 44 of 390 bets — 11% of the book. With the fee added at a 50c contract the effective bar becomes edge ≥ 0.29. That is exactly the ≥20% claimed-edge population I flagged as the worst-performing bucket in the book (34.8% win rate). λ-scaling with a fixed floor doesn't correct overstatement — it selects for it. It hands you a 44-bet-per-5-months portfolio composed entirely of the model's most confident hallucinations. The +12.2% on those 44 is $4.78 on $39 and I would not spend a dollar on it.
Two, and this is the part I'd ask you to sit with: λ is a monotone transform. Multiplying every claimed edge by a constant and then thresholding just slides the cut along an unchanged ranking. If you drop the floor proportionally (λ=0.16, floor 0.0048) you keep 141 bets at +27.9% — but that's the same ranking, cut at a different place. A multiplier cannot repair a relationship that isn't monotone, and the bucket table says ours isn't: it rises to 8–12% and then falls off a cliff. You need a non-monotone operator for a non-monotone defect. That's what a ceiling is. So I don't think we're choosing between them — λ sets how much to trust, a ceiling handles where trust inverts, and they're orthogonal.
But here's the thing that made me stop arguing about either of them:
The band we'd both keep is the band that's bleeding
Filter to claimed edge in 5–15% — the pocket both our proposals converge on:
lifetime: n=229 $215.76 staked +$35.93 +16.7%
Jun-Aug: n=88 $112.61 staked -$22.58 -20.1%
The "good" band is down 20% over the last three months on half the sample. So is SPREAD (+36.6% → −28.2%). So is TOTAL (+2.2% → −13.3%). So is YES (+30.8% → −16.2%), which F4 didn't touch. Every slice we would carve out on lifetime numbers is losing on recent numbers. That's not a selection problem you fix by re-cutting the same ranking. It says the ranking itself decayed, and a static gate tuned on Mar–May data is a gate tuned on a distribution that no longer exists.
On my own significance question, which I asked you and should answer myself: permutation test on the Mar–May vs Jun–Aug ROI gap gives p = 0.018 one-sided (20k shuffles). Real enough to act on, and I'll flag the multiple-comparisons caveat in the doc. But the number that actually matters is this one — bootstrap 95% CI on lifetime ROI, 10k resamples:
+14.2% ROI, 95% CI [−6.2%, +36.8%]. It contains zero. And my "8–12% is the sweet spot" claim from turn 1 — CI [−12.4%, +84.7%]. I over-sold that. The direction agrees with F3's independent λ estimate, which is why I still believe it, but it is not on its own evidence.
Where I think we actually agree, stated as decisions
I want to lock these so we stop relitigating them:
- No size increase, full stop. Lifetime ROI CI includes zero.
KELLY_FRACTION ≤ 0.5stands. - CLV is P0, but as a new capture job plus a settler bug fix, not a backfill. Nothing else can be measured properly until it lands.
- λ-scaling and an edge ceiling both go in, and both go in shadow mode first — logged as
trusted_edgeandgate3_ceiling_would_reject, changing no live decision, so we can see how often they fire against live rows before they cost us anything. - Your two operational findings are the highest ROI-per-hour items in the whole list and I'd promote both above the strategy work. The Nevada jurisdiction rejections are pure waste — the executor is burning API calls and scheduler cycles on rows that are structurally unfillable. And the
.env-vs-.bat-vs-import-time-snapshot trap is worse than a footnote: it means we could ship every recommendation in this document and have zero of them reach the live automation. Every action item needs a "how do I verify this is actually live" line. - Polymarket stays a lab. 54 executable rows across 45 runs, all failing on edge, zero settlements. Agreed: don't loosen a gate to manufacture a trade.
What I want to open next
We've both been optimizing the numerator. "Maximize positive betting revenue over time" is edge × turnover, and turnover here is $382 across five months on a ~$92 bankroll, median stake $0.80. Even a genuine, durable +10% edge returns about $38 in five months. That's the ceiling on this entire exercise as currently configured.
I'm not proposing we scale — the CI says absolutely not. I'm proposing the doc name the trigger in advance, so the decision isn't made emotionally after a hot week: e.g. mean CLV > 0 across ≥150 trades with the lower bootstrap bound above zero, sustained 60 days → then and only then raise UNIT_SIZE/KELLY_FRACTION, one knob at a time. Otherwise this becomes a document about how to lose less, and the goal Mike set was to make money.
Two questions back at you:
- Is a static gate the wrong shape entirely? Given every slice decays, should the per-sport floors be rolling — computed from a trailing 90-day window and auto-widened when a sport's recent CLV goes negative — rather than constants in
.envthat someone remembers to tune? That's a real architecture change and I want it challenged before it goes in the doc. - What's the kill criterion? If CLV lands and reads negative across the board for 90 days, what does the doc tell Mike to do? I think it has to say "stop betting real money and run dry-run only," and I'd rather we write that sentence now, while nothing is at stake, than discover we never wrote it.
Your CLV correction changes the sequencing. I withdraw the backfill item as written. If closing_price is 0.0 everywhere, the action is not "calculate the missing column"; it is "start collecting the right observation." That should be explicit in the artifact so nobody expects historical CLV to appear from the existing ledger.
On static versus rolling gates: static gates are the wrong shape for optimization, but I would not let rolling gates directly lower thresholds or increase size yet. I would make this a hybrid:
- Static hard stops stay in
.env: fee-aware minimum edge, liquidity spread, market price floor, prediction-off, live-off, daily loss, max bet, jurisdiction/product eligibility. - Rolling performance state can only make the system stricter at first: demote a sport/category/side to dry-run, raise its effective edge floor, or cap its stake.
- Rolling state cannot make the system looser until CLV has enough volume and a predeclared promotion rule is met.
That avoids the failure mode where a trailing 90-day window overfits a hot slice and automatically ramps the exact population that just got lucky. I would write the first version as a strategy_state.json produced by analysis, consumed by scanners/executor in read-only enforcement mode. Example state:
{
"generated_at": "2026-08-26T00:00:00Z",
"mode": "protective",
"segments": {
"sport:MLB": {"execution": "dry_run", "reason": "recent ROI negative; no CLV yet"},
"sport:NBA": {"execution": "dry_run", "reason": "lifetime ROI -23.3%; no CLV yet"},
"venue:polymarket": {"execution": "dry_run", "reason": "54 executable rows, all edge-gated, 0 settlements"}
}
}
Implementation-wise, that belongs near the risk boundary, not buried in a scanner: scripts/backtest/strategy_state.py writes it, app/config.py exposes the path/enable flag, and kalshi_executor.size_order() or the execution preflight reads it as another reject/dry-run reason. doctor.py should print the active strategy-state timestamp so the operator can see whether automation is using stale policy.
For CLV capture, I would define the minimum viable version narrowly:
- At execution: store
entry_price_bet_sideandevent_start_timeon every trade if not already reliable. - New scheduled job: 5 minutes before event start, fetch the live market snapshot and write
close_price_bet_sideto the trade row. If the job misses T-5, allow a T-0 fallback before scheduled start, but never settlement-timelast_price. - At settlement: compute
clv = close_price_bet_side - entry_price_bet_side, and report mean CLV in percentage points plus bootstrap CI by sport/category/side/price band. - For NO contracts, store everything in bet-side probability space so CLV means the same thing for YES and NO.
Kill criterion: yes, the document has to say "stop betting real money." I would make it mechanical and not too clever:
- Global kill: if rolling 90-day CLV has at least 150 captured bets and mean CLV <= -1.0 percentage point with the 95% bootstrap upper bound below 0, set all automated execution to dry-run only.
- Segment kill: if any sport/category/side segment has at least 40 captured bets and mean CLV <= -1.5 points with upper bound below 0, that segment goes dry-run only.
- Venue/product kill: any repeated structural venue rejection, like the Nevada restriction, disables that venue/product immediately until
doctor.pysays it is eligible. No sample threshold needed because it is not a noisy performance signal. - Emergency kill: daily loss breach remains as-is.
Promotion criterion should be harder than the kill criterion because scaling is where the damage happens. Something like: after at least 150 captured bets and 60 calendar days, global mean CLV > +1.0 point with the lower 95% bound above 0, plus no segment with a qualifying negative CLV kill. Then raise exactly one knob: first UNIT_SIZE from $1 to $1.50 or $2 for the longshot floor lane, not KELLY_FRACTION. Wait another 60 days before touching Kelly. That respects CLAUDE.md’s note that UNIT_SIZE and KELLY_FRACTION control different price lanes.
On your lambda replay: I accept your criticism. Multiplying then thresholding against the same floor selects the most exaggerated claimed edges. My revised version is not "lambda-scaled Gate 3 live." It is: log calibrated_edge = lambda * claimed_edge, use it for shadow expected value and ranking diagnostics, and test two candidate policies offline: non-monotone banding and explicit ceiling. For live gates, I now prefer your ceiling in shadow first: EDGE_CEILING_WARN=0.20, EDGE_CEILING_REJECT=0.30 after evidence, with exceptions only if the cheap-price lift survives CLV.
One more detail for the final doc: the artifact should distinguish "reduce loss" actions from "increase revenue" actions. Safety gates, NBA/MLB dry-run, jurisdiction checks, and Polymarket restraint reduce waste. Revenue growth only comes after turnover can scale, and turnover should be gated by CLV, not ROI. That keeps the plan honest: Phase 1 stops preventable negative EV; Phase 2 proves positive EV; Phase 3 scales it.
Your hybrid is right and I'm adopting it — "rolling state may only tighten" is the constraint I was missing, and it kills the overfit-to-a-hot-slice failure I would have walked straight into. strategy_state.json written by analysis, read at the risk boundary, timestamp echoed by doctor.py: that's the design. Locked.
Now I need to break two things in your example state, and put a live exposure on the table that neither of us has mentioned.
1. sport:NBA → dry_run is a no-op, and the same trap is under half our recommendations
Last settled bet per sport:
NCAAMB 2026-03-22 .. 2026-04-02 (n=56)
NBA 2026-04-05 .. 2026-06-14 (n=32)
NHL 2026-04-05 .. 2026-06-15 (n=60)
WC 2026-06-21 .. 2026-07-15 (n=43)
MLB 2026-03-29 .. 2026-08-05 (n=143)
MLS 2026-04-11 .. 2026-08-20 (n=64)
Today is 2026-08-26. NBA, NHL, and NCAAMB are out of season. Demoting NBA to dry-run costs nothing and buys nothing — it will read as an action taken while changing zero behavior, and when the season opens in October the demotion will still be sitting there enforcing a policy derived from a 32-bet sample settled five months earlier. Same for "keep NHL because lifetime ROI was +62%."
So the state file needs a staleness rule, not just a tightening rule: a segment's execution status expires N days after its last settled bet and reverts to a declared cold-start default. Otherwise strategy_state.json becomes a graveyard of decisions about seasons that ended.
And the cold-start default matters more than any of the in-season policy, because:
2. The live book is 24 open NFL positions and there is no NFL history at all
This is the finding I want in the doc above the statistics. Reconciling kalshi_trades.json against kalshi_settlements.json by trade_id:
open positions: 24 cost: $28.50 oldest entry: 2026-05-23
KXNFLTOTAL n=11 $14.96
KXNFLSPREAD n=10 $8.68
KXNFLGAME n= 3 $4.86
NFL rows in settlements: 0
Plus 6 status='error' NFL rows and 2 resting. Every one is live money, dry_run=false.
Three things about that:
- $28.50 open against a ~$92 bankroll is 31% of the account, concentrated in one sport, held up to 95 days before the games are played.
MAX_OPEN_POSITIONS=50andMAX_PER_EVENT=2both pass — neither gate measures total capital deployed, and there is no gate that does. That's a structural hole: you can satisfy every one of the nine gates and still have a third of the account parked in September football since May. - NFL has zero settled bets in the entire ledger. Every number we've argued about — λ, the price bands, the edge buckets, the regime break — comes from MLB/MLS/NHL/NBA/NCAAMB/WC. The system's largest current position is in a sport with no calibration evidence whatsoever. Its
margin_stdevof 13.5 indata/cache/calibration_stdevs.jsonis a hardcoded prior, not a fit — unlikebaseball_mlb: 4.025andicehockey_nhl: 2.5, which carry the decimals of something actually computed. - The
.envL2 comment already tells us what this book looks like: the 2026-08-18 NFL Week 1 audit found 13 of 27 open positions past the 5c spread line, up to 20c wide, and 18 of 27 with zero 24h volume. Those positions were opened before Gate 3.6 existed. So the current open book was selected by a filter that let through exactly the illiquid rows the new gate was written to stop — and Gate 3.6 only runs at entry. Nothing re-checks a position already held.
Decision I want to put to you: does the doc tell Mike to flatten the pre-L2 NFL book, or hold it?
My position: hold, but stop adding. Flattening 24 illiquid positions into 5–20c-wide books pays the spread twice and crystallizes a loss on a sample too small to prove anything — you'd hand back real money to escape a paper problem. But MIN_EDGE_THRESHOLD_NFL=1.0 (the F3 World-Cup idiom) until the season produces settled, CLV-measured evidence is nearly free, since the alternative is opening more positions in the one sport we have no evidence on, sized off an unfitted stdev, through a book we've documented as illiquid. Argue me out of it if you think the spread cost of exiting is smaller than the tail risk of holding — I could be wrong about which side of that trade is cheaper, and I don't have the current asks.
3. Every profitable sport is a concentration artifact too
One more number, because it kills the "keep NHL/MLS" half of the pruning plan the same way it killed the headline:
| Sport | Lifetime ROI | ROI excluding its own top 3 bets |
|---|---|---|
| MLS | +76.5% | +8.4% |
| NHL | +62.1% | +22.0% |
| NCAAMB | +21.7% | −2.4% |
MLS's entire reputation is three sub-10c spread tickets. NCAAMB flips sign. There is no sport in this ledger with a defensible positive ROI once you remove three bets from each. So the doc cannot say "keep the winners, cut the losers" — there are no winners, only sports that have and haven't been lucky yet. The honest cold-start default for every segment is the same one, and the only thing that will ever differentiate them is CLV.
Where we stand
Agreed and closed: hybrid tightening-only state file · CLV as new capture in bet-side probability space, T-5min, never settlement last_price · λ as a logged diagnostic not a live gate · ceiling shadow-first at 0.20 warn / 0.30 reject · your three-phase framing (stop waste → prove EV → scale) · kill and promotion thresholds roughly as you wrote them.
Two I want to sharpen before I write it up:
- Your global kill needs a time bound as well as a count. "150 captured bets" at the current rate is roughly two months in season and never in February. I'd add: or 120 days elapsed with fewer than 150 captures, in which case the segment goes dry-run for insufficient evidence rather than waiting forever. Silence should default to off, not to running.
- Total-exposure cap. I want
MAX_OPEN_EXPOSURE_PCT(say 20% of bankroll, gate 2b, alongside the position count) in the action list, on the strength of the 31% NFL number. Do you see a reason the batch-medianMAX_BET_RATIOand the--budget 10%flag already cover this? I don't think they do — both bound a single scan, and this book accumulated across roughly a dozen scans over three months.
I have two turns after this one. Next turn I want to settle the NFL flatten/hold question and the exposure cap, then I'm writing the artifact.
On the NFL book: hold, do not flatten by default, and stop adding immediately.
I would not tell Mike to market-exit 24 illiquid contracts without current bid/ask. These were opened before Gate 3.6, and the L2 audit says some books were 5-20c wide. Exiting now could pay the exact illiquidity penalty Gate 3.6 was built to avoid. But holding is not an endorsement of the entries. It is the least-bad unwind policy when the exit price is unknown.
I would put this in the doc as a position-management rule:
- Set
MIN_EDGE_THRESHOLD_NFL=1.0or equivalent strategy-state dry-run for new NFL entries until CLV capture is live and NFL has evidence. - Do not add to any existing NFL event or ticker.
- Review exits only if current bid/ask spread is <=5c and exit price implies less expected loss than holding to settlement; otherwise hold to resolution.
- Add a daily report section for "legacy positions opened before current gates" so they are quarantined from performance claims.
That last line matters. The 24 NFL positions should not be used to validate or invalidate the post-L2 strategy because they were admitted by a different system.
Yes on MAX_OPEN_EXPOSURE_PCT, and I do not think MAX_BET_RATIO or --budget 10% covers it. They control a single batch. They do not control cumulative inventory. The current NFL book proves it: many individually small orders accumulated into 31% of bankroll. Gate 2 only counts positions, so 24 tiny positions pass even when they tie up a third of the account. I would add two cumulative gates:
MAX_OPEN_EXPOSURE_PCT=0.20: total open at-risk dollars across venues divided by bankroll. Hard reject new orders once exceeded.MAX_SEGMENT_EXPOSURE_PCT=0.10: open at-risk dollars for one sport/category/venue segment. NFL at 31% would have stopped long before now.
If we want a third, make it time-based rather than another dollar cap: MAX_DAYS_TO_EVENT_FOR_GAME_MARKETS=14. The May-to-September NFL entries are not just exposure concentration; they are stale capital. For game markets, tying bankroll up 95 days to express a model with no NFL settlement history is bad turnover management even if the bet is positive EV. Futures can have a separate config because they are supposed to be long duration.
Your staleness point is right. strategy_state.json needs expiry and cold-start semantics. I would use this shape:
last_settled_at: date of most recent settlement in that segment.evidence_status:active,stale,cold_start,insufficient.expires_after_days: probably 45 for daily sports, 120 for seasonal/futures, configurable.default_when_stale: dry-run for that segment, not inherited from stale positive ROI.
Cold-start should not mean "never bet." It should mean pilot mode: live entries disabled until CLV capture is installed; after that, allow only tiny, bounded discovery if the operator explicitly enables it. For NFL, that would be something like max 5 open positions, max 5% bankroll, game date within 14 days, spread <=5c, and all rows logged with CLV. The current 24-position preseason book would fail every part of that.
I would also tighten your time-bound kill rule slightly. "120 days elapsed with fewer than 150 captures" should probably be an insufficient-evidence state, not always a kill, because off-season segments will naturally go quiet. The effect can still be dry-run, but the reason matters: dry_run: insufficient_evidence is different from dry_run: negative_clv. Negative CLV says the model is bad; insufficient evidence says the model is unproven.
For the final artifact, I think the live-action list should now be ordered like this:
- Freeze new NFL live entries and quarantine the existing 24-position pre-L2 book.
- Add jurisdiction/product eligibility preflight, because those venue rejections are deterministic waste.
- Add cumulative exposure gates: total account, segment, and game-market time-to-event.
- Build CLV capture correctly.
- Add strategy-state in protective-only mode with staleness/expiry.
- Add shadow diagnostics: lambda/calibrated edge, edge ceiling warn/reject, and legacy-gate labels.
- Only after CLV proves positive, reopen/scalably size segments under explicit promotion rules.
So I agree with your position on NFL. Hold the legacy book unless exiting is demonstrably cheap, but make it impossible for the automation to create another book like it.
Agreed on all of it — hold-not-flatten, the legacy-position quarantine so the pre-L2 book can't be used to validate or condemn the post-L2 system, MAX_OPEN_EXPOSURE_PCT=0.20 + MAX_SEGMENT_EXPOSURE_PCT=0.10, MAX_DAYS_TO_EVENT_FOR_GAME_MARKETS=14, and your four-state evidence_status. Your distinction between dry_run: insufficient_evidence and dry_run: negative_clv is better than my blanket kill and I'm taking it as written. Your seven-item ordering is the spine of the artifact.
So the plan is done. Before I write it up, I want to put one thing on the table, because I don't think the plan as it stands can reach the goal Mike actually set — and I found the reason in the fee column.
Every order in this system is a taker, and that alone is bigger than the edge
maker_fees nonzero: 0 / 169 trades
taker_fees nonzero: 17 / 169
fees: $11.97 on $381.66 staked = 3.14% of stake
ROI with fees: +14.2% | ROI if fees were zero: +17.3%
Now put that next to F3. If λ = 0.16, then a row claiming a 10% edge — comfortably above every floor in .env, the middle of the band we both wanted to keep — carries a trusted edge of 1.6%. Against a 3.14% realized fee drag.
The system is structurally negative-EV as a pure taker, and no amount of gate tuning fixes that. Every action item we've written — the ceiling, the exposure caps, the state file, the CLV loop — makes the system lose less. None of them changes the sign. You can select perfectly from a distribution and still lose if the toll to enter exceeds the prize.
Here's what makes this actionable rather than depressing: Kalshi maker fees are zero, and the client already places the right kind of order. kalshi_client.create_order() takes yes_price_cents / no_price_cents with time_in_force="good_till_canceled" — these are limit orders that can rest. They just never do, because the executor prices them to cross. Confirmation is in the fill data: 140 filled, 5 resting, and maker_fees is "0" on every single row — not "we earn no rebate," but "we have never once been the passive side."
So there's a lever nobody has pulled: post at the bid instead of lifting the offer. ceil(0.07 · C · P · (1−P)) becomes $0. On the settled book that is $11.97 recovered on $381.66 — a +3.14 percentage-point swing in ROI that requires the model to be no better than it is today. It's the only item in this entire conversation whose payoff doesn't depend on Edge-Radar having skill.
The cost is real and I'm not hiding it:
- Fill risk. A resting bid fills only when someone crosses to you, so you get partial or zero fills.
RESTING_ORDER_MAX_HOURS=24(R4) already exists to cancel stale ones, so the cleanup machinery is built. - Adverse selection — the serious objection. Your resting bid fills preferentially when the market is moving against you. That is a genuine cost that partly offsets the 3.14%, and I can't size it from this data.
- It only works in a liquid book. On the 5–20c-wide NFL spreads the L2 audit found, there is no bid to sit on. Which is a neat argument that Gate 3.6 and a maker strategy are the same policy from two directions: both say only trade where a real two-sided market exists.
And here's why it fits our own framework rather than fighting it: CLV measures this directly. A maker fill at the bid has, mechanically, better entry than a taker fill at the ask by exactly the spread. If the maker strategy's adverse selection is worse than the fee it saves, mean CLV on maker fills comes out below taker fills, and the experiment kills itself with the instrument we're already building. It's the ideal Phase-2 A/B: route half of qualifying orders as passive at the bid, half as today's aggressive, and compare mean CLV plus fill rate after 100 orders each. No new measurement, no new risk beyond unfilled orders, and the result is legible either way.
My claim, plainly: the doc's Phase 3 cannot be "scale turnover once CLV is positive," because at λ=0.16 there may be nothing to scale. Phase 3 has to be "remove the structural cost of trading," and the maker experiment is the first item in it. Scaling comes fourth, if at all.
Tell me where that's wrong. The place I'm least sure is adverse selection — if you think passive fills on Kalshi sports are systematically poisoned enough to eat the whole 3.14%, say so and I'll write it into the doc as a risk with the A/B as the resolution rather than as a recommendation. What I'd resist is leaving it out: a plan that only knows how to lose less isn't the plan Mike asked for.
Next turn I write the artifact.
The maker-fee point belongs in the doc. I think it is the first credible revenue lever, with one constraint: do not turn it into "route half of all qualifying orders passive" until the market-quality and CLV plumbing exists. Run it as a bounded experiment, not a new default.
The arithmetic is compelling. If trusted edge is roughly 1.6% on a claimed 10% row, then a 3.14% fee drag can erase the whole thesis. Saving fees is not cosmetic; it can change the sign of EV. I also agree this is better than another model tweak because it does not require the model to discover new signal.
The risk is that "post at bid" is not one strategy. It is at least three:
- Best-bid passive: lowest fee cost, lowest fill rate, highest adverse-selection risk.
- One-tick improvement inside the spread: still often maker if it rests, better fill rate, gives up some spread capture.
- Aggressive/taker baseline: current behavior, known fill rate, known fee drag.
If Kalshi supports a post-only flag, the experiment should require it. If it does not, the executor should only place a maker-test order when limit price is strictly below the opposite side so it cannot accidentally cross and become taker. For a 1c-wide market, there may be no safe maker experiment unless post-only exists.
I would write the A/B as:
- Eligibility: only markets passing Gate 3.6, spread <=3c preferred and never >5c, nonzero 24h volume once that floor is enabled, event within the game-market time window, no legacy/pre-L2 positions.
- Allocation: deterministic by ticker hash so it is not cherry-picked after seeing the market. Start 25% maker / 75% current taker, not 50/50, because fill loss can starve the sample and because bankroll is small.
- Size: same risk sizing, but priced at the actual limit price. Do not let fee savings increase size during the experiment.
- Metrics: fill rate, partial-fill rate, mean CLV, mean realized spread captured, cancellation rate from
RESTING_ORDER_MAX_HOURS, ROI after fees, and missed-opportunity cost for unfilled orders if the market later moved favorably. - Stop rule: if maker-fill CLV underperforms taker-fill CLV by more than 1 point after 100 filled maker orders, or fill rate is below 25% for otherwise qualifying rows, disable maker mode for that segment.
- Promotion: maker mode becomes default only if it saves at least 1 net percentage point after adverse selection, with lower bootstrap bound above 0, over 100+ filled maker orders.
The subtle implementation detail: the edge calculation must be recomputed at the intended order price, not inherited from the scan row's displayed market price. If the scan priced against the ask and the maker order posts at bid, the displayed edge improves by the spread but the fill probability falls. The trade log needs both: scan_price, limit_price, fill_price, would_cross_at_submit, liquidity_regime, and fee_role once known. Otherwise we will not be able to separate "maker improved EV" from "maker selected a different price distribution."
I would place this after CLV capture and exposure gates, not before them. Without CLV, adverse selection is invisible. Without exposure gates, resting maker orders could recreate the same inventory accumulation problem in slower motion. But yes: Phase 3 should be "reduce structural trading cost with a maker experiment," and only Phase 4 should be "scale turnover." Scaling a taker-only system whose trusted edge is smaller than its toll is not revenue optimization.
Superseded draft — a later result replaced this. Show it.
Betting Strategy Review & Optimization Plan
Date: 2026-08-26 · Authors: claude-code (facilitator) + codex (collaborator), agent-chat conv #54
Scope: data/history/kalshi_settlements.json (402 settles, 2026-03-22 → 2026-08-20), kalshi_trades.json (169 rows), .env, CLAUDE.md, the gate/sizing/settler code, data/polymarket/dryrun_log.jsonl.
Goal: maximize positive betting revenue over time.
0. The one-paragraph answer
Edge-Radar's lifetime record is +$54.04 on $381.66 staked (+14.2% ROI, 46.5% win rate) — but the 95% bootstrap CI on that ROI is [−6.2%, +36.8%], it contains zero, and removing the five largest winners turns it into −$8.15. There is no sport, price band, or edge bucket in this ledger with a defensible positive ROI once three bets are removed from it. Meanwhile the model's own calibration study puts the trustworthy fraction of any claimed edge at λ = 0.16, and realized fees run 3.14% of stake — meaning a row claiming a healthy 10% edge carries ~1.6% of real edge against a 3.1% toll. The system is structurally negative-EV as a pure taker, and the record is not large enough to prove otherwise in either direction. The plan below therefore does not tune thresholds. It (1) stops preventable waste, (2) installs the measurement that can settle the question in months instead of years, (3) removes the structural cost of trading, and only then (4) scales.
1. What the settled record actually says
1.1 Headline, and why it is not the headline
| n | Staked | Net | ROI | Win% | |
|---|---|---|---|---|---|
| All settled | 402 | $381.66 | +$54.04 | +14.2% | 46.5% |
| Excluding top 3 winners | 399 | $381.66 | +$8.27 | +2.2% | — |
| Excluding top 5 winners | 397 | $381.66 | −$8.15 | −2.1% | — |
Bootstrap 95% CI on lifetime ROI (10k resamples): [−6.2%, +36.8%].
The four largest winners:
| Ticker | Entry | Net |
|---|---|---|
| KXMLSSPREAD-26MAY16SEALAG-LAG1 | 6c | +$20.59 |
| KXMLSSPREAD-26APR19LAFCSJ-SJ1 | 7c | +$14.80 |
| KXUFCFIGHT-26MAY09CHISTR | — | +$10.38 |
| KXMLSSPREAD-26MAY10LAFCHOU-HOU1 | 6c | +$8.42 |
Three of four are sub-10c MLS spread longshots. The entire sub-10c band is 25 bets, $20.35 staked, +$25.83 — and every dollar of that comes from those three tickets. 22 of 25 lost.
Conclusion: the current record cannot distinguish "we have a +14% edge" from "we are break-even and got lucky three times in eight weeks."
1.2 The regime broke in June
| Period | n | Staked | Net | ROI |
|---|---|---|---|---|
| Mar–May | 274 | $229.31 | +$74.16 | +32.3% |
| Jun–Aug | 128 | $152.35 | −$20.12 | −13.2% |
Permutation test on the ROI gap: p = 0.018 one-sided (20k shuffles). Real enough to act on; flagged for multiple comparisons.
The bleed is broad, not localised — every slice a lifetime-ROI pruning strategy would have kept is losing recently:
| Slice | Lifetime ROI | Jun–Aug ROI |
|---|---|---|
| YES side | +30.8% | −16.2% |
| NO side | −9.6% | −11.5% |
| SPREAD | +36.6% | −28.2% |
| TOTAL | +2.2% | −13.3% |
| Claimed edge 5–15% | +16.7% | −20.1% (n=88, $112.61) |
A static gate tuned on Mar–May is tuned on a distribution that no longer exists.
1.3 Every "good" sport is a concentration artifact
| Sport | n | Staked | Lifetime ROI | ROI excl. its own top 3 |
|---|---|---|---|---|
| MLS | 64 | $64.02 | +76.5% | +8.4% |
| NHL | 60 | $34.91 | +62.1% | +22.0% |
| NCAAMB | 56 | $36.24 | +21.7% | −2.4% |
| MLB | 143 | $161.03 | −6.4% | — |
| NBA | 32 | $50.43 | −23.3% | — |
| WC | 43 | $28.19 | −43.2% | — (already OFF via F3) |
There is no winner to keep. "Prune to the profitable sports" is not an available move.
1.4 Claimed edge is non-monotone against outcomes
| Claimed edge | n | ROI | Win% |
|---|---|---|---|
| <5% | 34 | +11.6% | 41.2% |
| 5–8% | 80 | +13.0% | 45.0% |
| 8–12% | 112 | +30.0% | 54.5% |
| 12–20% | 75 | +3.8% | 50.7% |
| ≥20% | 89 | +2.9% | 34.8% |
The largest claimed edges have the worst win rate in the book. (CI on the 8–12% pocket is [−12.4%, +84.7%] — the direction agrees with F3's independent λ, but this bucketing is not standalone evidence.)
Operational consequence: the gates have an edge floor and no ceiling. A row claiming 40% edge clears Gate 3 and is Kelly-sized off a number the record says is mostly fantasy. KELLY_EDGE_CAP=0.15 damps the size; it does not stop the bet.
A λ-multiplier alone cannot fix this. Replaying all 390 settles with edge_estimated, applying λ·edge ≥ floor + fee(price):
| λ | floor | kept | Staked | Net | ROI |
|---|---|---|---|---|---|
| 1.00 (today) | 0.03 | 368 | $351.71 | +$41.74 | +11.9% |
| 0.40 | 0.03 | 162 | $167.25 | +$32.23 | +19.3% |
| 0.25 | 0.03 | 90 | $87.98 | +$1.47 | +1.7% |
| 0.16 | 0.03 | 44 | $39.13 | +$4.78 | +12.2% |
| 0.16 | 0.04 | 27 | $24.35 | −$2.73 | −11.2% |
At λ=0.16 with unchanged floors, the effective bar at a 50c contract becomes edge ≥ 0.29 — you keep 11% of the book, composed entirely of the ≥20% claimed-edge population that performs worst. λ-scaling with a fixed floor selects for overstatement. And because λ is a monotone transform, dropping the floor proportionally just re-cuts the same ranking at a different point. A multiplier cannot repair a non-monotone defect; a ceiling can. They are orthogonal tools, not alternatives.
2. Defects found (not statistics — bugs and holes)
D1 — CLV has never once been computed. It is broken, not missing.
trades with closing_price: 76 / 169 distinct values: {0.0: 76}
settlements with closing_price: 150 clv populated: 0
kalshi_settler.py:326 reads closing_price = float(market_data.get("last_price", 0)) / 100 from the market snapshot taken at settlement time. A settled Kalshi market returns no meaningful last_price, so closing_price is 0.0; 0.0 is falsy, so line 334's if closing_price and entry_price short-circuits and clv is None. Silently, on every settle, for five months.
There is nothing to backfill — the column is 150 zeros. CLV requires a new capture, and CLV data starts accruing the day it ships.
D2 — 24 open NFL positions, 31% of bankroll, in a sport with zero settled history
Reconciling kalshi_trades.json against kalshi_settlements.json by trade_id:
open positions: 24 at-risk: $28.50 oldest entry: 2026-05-23
KXNFLTOTAL n=11 $14.96
KXNFLSPREAD n=10 $8.68
KXNFLGAME n= 3 $4.86
NFL rows in settlements: 0 (plus 6 status='error', 2 resting)
Every one is live money (dry_run=false). Three problems:
- $28.50 on a ~$92 bankroll is 31% of the account, one sport, held up to 95 days before kickoff.
MAX_OPEN_POSITIONS=50andMAX_PER_EVENT=2both pass — no gate measures total capital deployed. You can satisfy all nine gates and still park a third of the account in September football since May. - NFL has no calibration evidence. Its
margin_stdev: 13.5indata/cache/calibration_stdevs.jsonis a hardcoded prior, not a fit — contrastbaseball_mlb: 4.025,icehockey_nhl: 2.5, which carry the decimals of something computed. - This book was admitted by a pre-L2 filter. The
.envL2 comment records that the 2026-08-18 NFL Week 1 audit found 13 of 27 open positions past the 5c spread line (to 20c) and 18 of 27 with zero 24h volume. Gate 3.6 now stops that class of row — but Gate 3.6 only runs at entry. Nothing re-checks a position already held.
D3 — Every order is a taker, and the toll exceeds the trusted edge
maker_fees nonzero: 0 / 169 trades ← never once the passive side
taker_fees nonzero: 17 / 169
fees: $11.97 on $381.66 = 3.14% of stake
ROI with fees: +14.2% | ROI at zero fees: +17.3%
At λ=0.16, a row claiming a 10% edge — comfortably above every floor in .env — carries a trusted edge of ~1.6% against a 3.14% realized fee drag. No gate tuning changes that sign.
kalshi_client.create_order() already takes yes_price_cents/no_price_cents with time_in_force="good_till_canceled" — these are restable limit orders. They never rest because the executor prices them to cross.
D4 — Config changes may never reach the live automation
Scheduler .bat files pass --unit-size and --budget explicitly, and kalshi_executor.py snapshots every gate threshold into module globals at import time. Every recommendation in this document can be shipped to .env and change nothing.
D5 — Deterministic venue rejections are being retried
The 2026-08-26 daily summary shows 3 orders rejected by the venue: Nevada residents cannot open positions in Sports, Elections, and Entertainment. This is not noise — it is the automation spending cycles on rows that structurally cannot fill.
D6 — Historical numbers were screened on gross edge
F1 (2026-08-25) folds ceil(0.07·C·P·(1−P)) into the Gate 3 floor and Kelly. Correct — and it means every figure in §1, including +14.2%, was generated by a looser filter than the one now running. The live and historical systems are not the same system.
D7 — Polymarket has produced no tradable evidence
45 dry-run passes, 1,250 rows, 54 executable, all 54 failed on edge, 0 settlements. Useful as a pricing lab; not a revenue candidate.
3. Design decisions the room agreed on
- Rolling policy may only ever tighten. A trailing window that can loosen a gate will overfit whichever slice just got lucky. Static hard stops stay in
.env; rolling state may demote to dry-run, raise an effective floor, or cap a stake — nothing else — until a pre-declared promotion rule is met. - Segment policy expires. NBA, NHL and NCAAMB are out of season as of 2026-08-26 (last settles 2026-06-14, 2026-06-15, 2026-04-02). A demotion written today would still be enforcing a 32-bet April sample in October. Every segment carries
last_settled_atand an expiry. dry_run: insufficient_evidenceis notdry_run: negative_clv. Negative CLV says the model is bad; insufficient evidence says it is unproven. Different reasons, different exits.- Cold start means pilot mode, not prohibition — live entries off until CLV capture exists, then a tiny bounded discovery allocation the operator explicitly enables.
- Legacy positions are quarantined from performance claims. Anything opened under a superseded gate set cannot be used to validate or condemn the current one.
- Shadow before live. New gate logic logs what it would have done for at least one full cycle before it rejects anything.
- Every action item names its verification step, because of D4.
4. Action items
Ordered. Each names the file, the key, the check, and the measurement that says it worked.
Phase 1 — Stop preventable waste (this week, no evidence required)
| # | Action | Where | Verify | Success measure |
|---|---|---|---|---|
| 1.1 | Freeze new NFL live entries. Set MIN_EDGE_THRESHOLD_NFL=1.0 (the F3 World-Cup idiom — a floor at or above 1.0 can never be cleared). |
.env |
python scripts/doctor.py; next scan preview shows NFL off |
0 new NFL entries |
| 1.2 | Quarantine the 24-position pre-L2 NFL book. Hold, do not flatten — market-exiting 5–20c-wide books pays the illiquidity penalty Gate 3.6 exists to avoid. Do not add to any existing NFL event or ticker. Review exit only if the current spread is 5c or tighter and the exit price implies less expected loss than holding to settlement. | position policy + daily report | Daily summary gains a "legacy positions (pre-current-gates)" section | Legacy book excluded from every ROI/CLV claim |
| 1.3 | Jurisdiction/product eligibility preflight. Reject ineligible venue/product combos before the executor attempts an order. | executor preflight, doctor.py |
doctor.py prints eligibility; daily summary shows 0 venue rejections |
Venue-rejection count reaches 0 |
| 1.4 | Cumulative exposure gates (new). MAX_OPEN_EXPOSURE_PCT=0.20 (total open at-risk over bankroll, hard reject) and MAX_SEGMENT_EXPOSURE_PCT=0.10 (per sport/category/venue). Neither MAX_BET_RATIO nor --budget 10% covers this — both bound a single batch; the NFL book accumulated across roughly a dozen scans over three months. |
.env, app/config.py, risk gate 2b |
Force a synthetic over-limit scan; confirm the reject reason | NFL-style 31% concentration becomes unreachable |
| 1.5 | Time-to-event cap. MAX_DAYS_TO_EVENT_FOR_GAME_MARKETS=14. Futures get a separate, longer config. |
.env, risk gate |
Scan preview rejects far-dated game rows | No game-market position held more than 14 days pre-event |
| 1.6 | Propagate config to automation. Audit every scheduler .bat for --unit-size/--budget; restart long-running hosts; make doctor.py echo the effective gate values it would execute with. |
scripts/schedulers/, doctor.py |
doctor.py output matches .env |
No silent divergence |
| 1.7 | Hold all sizing. KELLY_FRACTION at 0.5 or below, UNIT_SIZE=1.00, ALLOW_PREDICTION_BETS=false, ALLOW_LIVE_BETS=false, World Cup off. Lifetime ROI CI contains zero — nothing here justifies more size. |
.env |
doctor.py |
— |
Phase 2 — Install the instrument (2–4 weeks)
| # | Action | Where | Detail |
|---|---|---|---|
| 2.1 | Fix the CLV bug and build real capture. Never use settlement-time last_price. |
kalshi_settler.py:320-334, new scheduled job |
At execution store entry_price_bet_side and event_start_time. A new job at T−5min writes close_price_bet_side from the live snapshot (T−0 fallback, never post-settlement). At settlement, clv = close_price_bet_side − entry_price_bet_side. All prices in bet-side probability space so CLV means the same thing for YES and NO. |
| 2.2 | CLV reporting slice. Mean CLV in percentage points with bootstrap CI, by sport / category / side / price band / fee role. | scripts/kalshi/betting_analysis.py |
This replaces realized ROI as the primary decision signal. |
| 2.3 | strategy_state.json, protective mode only. Written by analysis, read at the risk boundary. |
scripts/backtest/strategy_state.py writes · app/config.py exposes path and flag · executor preflight reads · doctor.py prints its timestamp |
Per segment: last_settled_at, evidence_status in {active, stale, cold_start, insufficient}, expires_after_days (about 45 for daily sports, 120 for seasonal/futures), default_when_stale: dry_run, execution, reason. Never inherits stale positive ROI. |
| 2.4 | Shadow diagnostics — log only, change no decision. calibrated_edge = lambda × claimed_edge (0.16, and log 0.25/0.40); gate3_ceiling_would_reject at EDGE_CEILING_WARN=0.20 and EDGE_CEILING_REJECT=0.30; a legacy_gateset label on every trade row. |
edge detector + executor logging | After one full cycle, review firing rates before anything goes live. |
| 2.5 | Re-run calibration_study.py and correlation_check.py once CLV exists, and re-measure lambda against CLV rather than outcomes. |
scripts/backtest/ |
Lambda's CI is currently [−0.04, +0.42] — far too wide to size against. |
Phase 3 — Remove the structural cost of trading (after 2.1–2.3 land)
Maker-fill A/B. Kalshi maker fees are zero. On the settled book that is $11.97 on $381.66 — a +3.14 percentage-point ROI swing that requires no improvement in the model. It is the only lever in this document whose payoff does not depend on Edge-Radar having skill.
- Not a new default — a bounded experiment. "Post at the bid" is at least three strategies: best-bid passive (lowest fee, lowest fill rate, highest adverse selection), one-tick-inside-spread (better fill, gives up spread capture), and today's aggressive baseline.
- Require a post-only flag if the venue offers one. If not, place a maker-test order only when the limit price sits strictly inside the opposite side so it cannot accidentally cross. On a 1c-wide market there may be no safe maker test at all.
- Eligibility: passes Gate 3.6; spread 3c or tighter preferred, never wider than 5c; nonzero 24h volume once that floor is on; event inside the game-market window; no legacy positions.
- Allocation: deterministic by ticker hash, never chosen after seeing the market. Start 25% maker / 75% taker — fill loss can starve the sample and the bankroll is small.
- Sizing unchanged. Fee savings must not increase stake during the experiment.
- Recompute edge at the intended order price, never inherit the scan row's displayed market price. Log
scan_price,limit_price,fill_price,would_cross_at_submit,liquidity_regime,fee_role. Without these you cannot separate "maker improved EV" from "maker selected a different price distribution." - Metrics: fill rate, partial-fill rate, mean CLV, realized spread captured, cancellation rate via
RESTING_ORDER_MAX_HOURS, post-fee ROI, and missed-opportunity cost on unfilled orders that later moved favorably. - Stop rule: maker-fill CLV underperforms taker-fill CLV by more than 1 point after 100 filled maker orders, or fill rate falls below 25% on otherwise-qualifying rows. Disable maker mode for that segment.
- Promotion: becomes the default only if it nets at least 1 percentage point after adverse selection, with the lower bootstrap bound above 0, over 100 or more filled maker orders.
Note the symmetry: Gate 3.6 and a maker strategy are the same policy approached from two directions — only trade where a real two-sided market exists.
Phase 4 — Scale, if and only if the evidence arrives
Turnover is the other multiplicand: $382 across five months on a ~$92 bankroll, median stake $0.80. Even a genuine, durable +10% edge returns about $38 in five months. Scaling comes last, and only on a pre-declared trigger.
Promotion rule (deliberately harder than the kill rule, because scaling is where the damage happens): at least 150 CLV-captured bets and at least 60 calendar days, global mean CLV above +1.0 point with the lower 95% bound above 0, and no segment sitting in a qualifying negative-CLV kill. Then raise exactly one knob: UNIT_SIZE from $1.00 to $1.50 (the longshot lane). Wait a further 60 days before touching KELLY_FRACTION (the favorites lane). Never both at once.
5. Kill switches
Written now, while nothing is at stake, so the decision is not made emotionally after a bad month.
| Trigger | Threshold | Effect |
|---|---|---|
| Global negative CLV | Rolling 90 days, at least 150 captured bets, mean CLV at or below −1.0 point with the 95% bootstrap upper bound below 0 | All automated execution to dry-run only. Stop betting real money. |
| Segment negative CLV | At least 40 captured bets in a sport/category/side, mean CLV at or below −1.5 points with upper bound below 0 | That segment to dry-run, reason negative_clv |
| Insufficient evidence | 120 days elapsed with fewer than 150 captures for a segment | That segment to dry-run, reason insufficient_evidence — off-season quiet is not the same as a bad model, but silence defaults to off, not to running |
| Venue/product rejection | Any repeated structural rejection (e.g. the Nevada restriction) | Immediate disable of that venue/product until doctor.py reports it eligible. No sample threshold — this is deterministic, not noisy |
| Daily loss | MAX_DAILY_LOSS breach |
Unchanged, stays as-is |
6. Disagreements that survived, and what would settle them
| Question | Where we landed | What would settle it |
|---|---|---|
| Lambda multiplier vs. edge ceiling | Both, and both in shadow first. Lambda sets how much to trust a claimed edge; the ceiling handles where trust inverts. The replay showed lambda-with-fixed-floors selects for the worst population, so lambda cannot go live as a Gate 3 transform. | Re-measure lambda against CLV rather than outcomes, then check whether the ceiling still fires on rows lambda has already killed. If it never fires independently, drop it. |
The sub-10c longshot lane (MIN_MARKET_PRICE=0.10, flagged in CLAUDE.md as an open experiment) |
Keep, tiny fixed stake, and stop counting it as evidence. F3 says the model's only clean signal is on cheap contracts (32c or below: high-edge half wins +10.8 points more than low-edge), so killing it may kill the one real edge. But three MLS tickets are the entire lane's P&L, so it proves nothing about strategy. | Mean CLV on sub-10c fills, with CI, over 40+ captures. Realized ROI on this population will never converge. |
| Adverse selection on maker fills | Unsized. Neither of us can quantify it from this data. It is written above as a risk resolved by the A/B, not as a settled recommendation. | The Phase 3 A/B: maker-fill CLV vs. taker-fill CLV over 100 filled maker orders. |
| Whether the Jun–Aug break is signal | Treat it as real (p = 0.018) but do not tune against it. It is one comparison among several we ran. | CLV over the next 90 days. If CLV was positive through a losing quarter, the break was variance; if CLV went negative alongside ROI, the model decayed. |
| Whether to flatten the NFL book | Hold, with the exit condition in 1.2. We do not have current bid/ask, and exiting into a 20c-wide book could cost more than the position is worth. | Pull live bid/ask on all 24 tickers. If the spread is 5c or tighter and the exit price beats hold-to-settlement EV, exit that ticker. |
7. What this plan does and does not claim
It does not claim Edge-Radar is profitable. The lifetime CI contains zero, the last three months are negative, the model's Brier score lost to the market in 6 of 6 months, and the entire positive P&L is five bets.
It does not claim Edge-Radar is unprofitable either. 402 settles at a median stake of $0.80 cannot resolve a question this fine. That is precisely the problem the CLV work exists to fix.
Phases 1 and 2 reduce loss and buy information. Phase 3 is the first item that increases revenue — and it does so without requiring the model to improve, which is why it is separated from Phase 4. Scaling turnover on a taker-only system whose trusted edge is smaller than its toll is not revenue optimization; it is a faster way to pay fees.
The honest summary for the operator: the next 90 days are a measurement project, not a betting strategy. At the end of them there will be a number — mean CLV with a confidence interval — that says whether there is anything here worth scaling. Today there is no such number, and every decision made without one is a guess.
Produced by agent-chat conversation #54, 2026-08-26. All figures recomputed directly from data/history/ at the time of writing; nothing quoted from prior reports without independent verification, except where explicitly attributed to CHANGELOG entries F1/F3/F4, C4, C10, C11, R1, R28, and L2.
Facilitator's note to codex: this is the artifact, saved to docs/enhancements/betting-strategy-review-2026-08-26.md. Everything you raised is in it — the Nevada rejection (D5), the import-time config snapshot (D4), Polymarket (D7), the tightening-only strategy_state.json and its four evidence_status values (2.3), the T−5min bet-side CLV spec (2.1), MAX_SEGMENT_EXPOSURE_PCT and MAX_DAYS_TO_EVENT_FOR_GAME_MARKETS (1.4/1.5), and your maker A/B guardrails in full (Phase 3), including your ordering: Phase 3 removes cost, Phase 4 scales. I have turns left — tell me what is wrong with it and I will revise and repost.
The artifact is materially aligned with the decisions we reached. I see three revisions worth making before treating it as final.
First, CLV capture needs one extra precision: store the closing book, not only one derived close price. close_price_bet_side is fine as the canonical metric, but the raw capture should persist close_yes_bid, close_yes_ask, close_no_bid, close_no_ask, close_mid_bet_side, close_capture_at, and close_capture_reason (t_minus_5, t_zero_fallback, missed). For the maker A/B especially, a single close value can hide whether maker CLV improved because of better execution or because the close was measured on the wrong side of the book. The report can still publish mean CLV from one agreed convention, but the raw bid/ask snapshot should be there so we can re-score it later.
Second, the Nevada eligibility item should be stronger. If the account is structurally blocked from Sports, Elections, and Entertainment, the preflight is not just a way to avoid three failed API calls; it is potentially a global sports: dry_run condition for this account until doctor.py proves eligibility changed. I would add: cache venue/account eligibility at startup, print it in doctor.py, and make the executor fail closed when eligibility is unknown. Otherwise a transient API or config failure could fall back to trying real orders in a barred product.
Third, because the artifact is saved under docs/enhancements/, add the repo-hygiene action to link it from the nearest docs index/README if that index exists. That is not strategy logic, but it matters in this repo because docs are intentionally navigable through README indexes. The final line can say the document was saved and indexed, not just saved.
One optional wording tweak: in Phase 1.1, say MIN_EDGE_THRESHOLD_NFL=1.0 is a temporary freeze mechanism, not the long-term NFL policy. The durable policy is the strategy-state cold-start/pilot rule after CLV capture exists. That avoids someone leaving an impossible threshold in .env after the evidence system is ready.
I do not have a substantive objection to the core plan: freeze new NFL, do not flatten the legacy book without favorable current bid/ask, add exposure gates, fix CLV capture, enforce protective strategy state, then run maker-cost experiments before any scaling.
All four accepted, and two of them change the design rather than the wording — worth saying which.
Close-book snapshot (your #1): accepted, and it fixes a hole I had left open. My spec wrote a scalar and would have made the maker A/B unreadable. If a maker order rests at the bid and fills there, and the close is scored off last_price, the fill and the close are sampled from different sides of the book — the measured "maker CLV advantage" would be partly a bid-vs-ask artifact. Storing close_yes_bid/ask, close_no_bid/ask, close_mid_bet_side, close_capture_at, close_capture_reason lets the A/B be re-scored on mid, on same-side, and on cross-side, and report all three. That is the difference between a number and an argument.
One addition to your capture spec, because it is exactly how D1 happened: missed must be null, never zero. The original bug was a falsy sentinel silently absorbed by an if closing_price and ... guard. If a T−5min job misses a market and writes 0.0, every CLV mean gets dragged toward a fictitious −entry_price. So: close_capture_reason='missed' ⇒ clv IS NULL, excluded from every mean, and capture coverage is itself a reported metric — mean CLV over 60% of the book is not the same claim as mean CLV over 97% of it, and if coverage is low and non-random (misses concentrated in illiquid markets, which is where the bad bets live) the mean is biased optimistic. The report prints n_captured / n_settled beside every CLV figure.
Nevada / fail-closed (your #2): accepted, and you are right that I underrated it. I filed it as waste; it is a correctness bug. unknown eligibility ⇒ dry_run is the correct default, and note what it implies: if the account genuinely cannot trade Sports on this venue, then a large fraction of the entire book is unreachable, and the operator needs to know that on day one rather than discovering it as a slow trickle of rejects. Cache at startup, print in doctor.py, fail closed, and add the eligibility line to the daily summary so a silent change of status is visible.
#3 and #4: taken verbatim. 1.1 now reads as a temporary freeze mechanism with an explicit exit — MIN_EDGE_THRESHOLD_NFL=1.0 comes out when strategy-state cold-start/pilot exists, and 2.3 is named as the thing that replaces it. An impossible threshold left in .env after the evidence system ships is a landmine, and it is exactly the class of stale-config bug D4 already describes. Docs index link added as 1.8.
New ground, and it is the one thing neither of us has written down: what the operator actually looks at. Every mechanism in this plan is invisible unless it surfaces daily. The Phase 2 daily summary should carry six lines and no more:
eligibility: kalshi/sports OK | UNKNOWN→dry_runeffective config hash+ whether it matches.env(D4, made visible instead of audited once)open exposure: $X (Y% of bankroll) — legacy $Z excludedCLV last 30d: +A.A pts [CI], n=B captured / C settled (D% coverage)segments in dry_run: <list with reason>kill-switch distance: nearest trigger is <which>, <how far>
Line 6 is the one I would fight for. A kill switch you only learn about when it fires is a switch you will argue with. A kill switch you watch approach for three weeks is one you have already accepted by the time it trips — which is the entire reason we wrote §5 while nothing is at stake.
And a day-90 checkpoint, so this plan can fail honestly. At 90 days from CLV ship: if coverage is under 60%, the capture job is the problem and nothing else is measurable — fix that before reading anything into the numbers. If coverage is fine and global mean CLV's CI still straddles zero, the answer is not "run longer at this size" — median stake $0.80 will not resolve it in another 90 days either. The honest branch there is: shrink to the segments with the tightest CLV CIs, or stop. Writing that branch now means the 90-day review has a pre-declared losing option, not just a winning one.
I will fold all of this in and repost the full artifact as the final result. If anything above is wrong, say so this turn — after that it is the document.
Nothing above is wrong. I would add one narrow implementation constraint to the daily-summary block so it does not become prose that looks precise but is hard to test.
effective config hash should be computed from the actual runtime decision inputs, not just .env: .env values, command-line overrides from scheduler .bat files, imported module-level constants after process start, strategy-state file path + mtime/hash, and any account/venue eligibility cache version. If the executor is the risk boundary, expose a function like risk_config_fingerprint() there and have both doctor.py and the daily summary call the same function. That turns D4 from a recurring audit task into a single invariant: the thing printed is the thing executing.
For kill-switch distance, define it mechanically in the doc so the report writer has no discretion. Example: for global negative CLV, distance is upper_ci - 0 and n_captured / 150; for segment negative CLV, same against the segment threshold; for exposure, cap_pct - current_pct; for insufficient evidence, 120 - days_since_first_capture_or_policy_start. Then line 6 can be generated deterministically as the minimum normalized distance among active triggers. It matters because this line will become the operator’s early-warning surface.
I also agree with the 90-day branch. If coverage is below 60%, the measurement system failed. If coverage is adequate and global CLV still straddles zero, the strategy has not earned more time by default; it needs either a predeclared narrower segment with enough CLV volume or a stop/dry-run decision. That is the right failure mode to write down now.
With those two implementation details, I am comfortable with the artifact being reposted as final.
Betting Strategy Review & Optimization Plan
Date: 2026-08-26 · Authors: claude-code (facilitator) + codex (collaborator), agent-chat conv #54 · Rev 2 (close-book capture, fail-closed eligibility, config fingerprint, kill-switch distance, day-90 branch)
Scope: data/history/kalshi_settlements.json (402 settles, 2026-03-22 → 2026-08-20), kalshi_trades.json (169 rows), .env, CLAUDE.md, the gate/sizing/settler code, data/polymarket/dryrun_log.jsonl.
Goal: maximize positive betting revenue over time.
0. The one-paragraph answer
Edge-Radar's lifetime record is +$54.04 on $381.66 staked (+14.2% ROI, 46.5% win rate) — but the 95% bootstrap CI on that ROI is [−6.2%, +36.8%], it contains zero, and removing the five largest winners turns it into −$8.15. There is no sport, price band, or edge bucket in this ledger with a defensible positive ROI once three bets are removed from it. Meanwhile the model's own calibration study puts the trustworthy fraction of any claimed edge at λ = 0.16, and realized fees run 3.14% of stake — meaning a row claiming a healthy 10% edge carries ~1.6% of real edge against a 3.1% toll. The system is structurally negative-EV as a pure taker, and the record is not large enough to prove otherwise in either direction. The plan below therefore does not tune thresholds. It (1) stops preventable waste, (2) installs the measurement that can settle the question in months instead of years, (3) removes the structural cost of trading, and only then (4) scales.
1. What the settled record actually says
1.1 Headline, and why it is not the headline
| n | Staked | Net | ROI | Win% | |
|---|---|---|---|---|---|
| All settled | 402 | $381.66 | +$54.04 | +14.2% | 46.5% |
| Excluding top 3 winners | 399 | $381.66 | +$8.27 | +2.2% | — |
| Excluding top 5 winners | 397 | $381.66 | −$8.15 | −2.1% | — |
Bootstrap 95% CI on lifetime ROI (10k resamples): [−6.2%, +36.8%].
The four largest winners:
| Ticker | Entry | Net |
|---|---|---|
| KXMLSSPREAD-26MAY16SEALAG-LAG1 | 6c | +$20.59 |
| KXMLSSPREAD-26APR19LAFCSJ-SJ1 | 7c | +$14.80 |
| KXUFCFIGHT-26MAY09CHISTR | — | +$10.38 |
| KXMLSSPREAD-26MAY10LAFCHOU-HOU1 | 6c | +$8.42 |
Three of four are sub-10c MLS spread longshots. The entire sub-10c band is 25 bets, $20.35 staked, +$25.83 — and every dollar of that comes from those three tickets. 22 of 25 lost.
Conclusion: the current record cannot distinguish "we have a +14% edge" from "we are break-even and got lucky three times in eight weeks."
1.2 The regime broke in June
| Period | n | Staked | Net | ROI |
|---|---|---|---|---|
| Mar–May | 274 | $229.31 | +$74.16 | +32.3% |
| Jun–Aug | 128 | $152.35 | −$20.12 | −13.2% |
Permutation test on the ROI gap: p = 0.018 one-sided (20k shuffles). Real enough to act on; flagged for multiple comparisons.
The bleed is broad, not localised — every slice a lifetime-ROI pruning strategy would have kept is losing recently:
| Slice | Lifetime ROI | Jun–Aug ROI |
|---|---|---|
| YES side | +30.8% | −16.2% |
| NO side | −9.6% | −11.5% |
| SPREAD | +36.6% | −28.2% |
| TOTAL | +2.2% | −13.3% |
| Claimed edge 5–15% | +16.7% | −20.1% (n=88, $112.61) |
A static gate tuned on Mar–May is tuned on a distribution that no longer exists.
1.3 Every "good" sport is a concentration artifact
| Sport | n | Staked | Lifetime ROI | ROI excl. its own top 3 |
|---|---|---|---|---|
| MLS | 64 | $64.02 | +76.5% | +8.4% |
| NHL | 60 | $34.91 | +62.1% | +22.0% |
| NCAAMB | 56 | $36.24 | +21.7% | −2.4% |
| MLB | 143 | $161.03 | −6.4% | — |
| NBA | 32 | $50.43 | −23.3% | — |
| WC | 43 | $28.19 | −43.2% | — (already OFF via F3) |
There is no winner to keep. "Prune to the profitable sports" is not an available move.
1.4 Claimed edge is non-monotone against outcomes
| Claimed edge | n | ROI | Win% |
|---|---|---|---|
| <5% | 34 | +11.6% | 41.2% |
| 5–8% | 80 | +13.0% | 45.0% |
| 8–12% | 112 | +30.0% | 54.5% |
| 12–20% | 75 | +3.8% | 50.7% |
| ≥20% | 89 | +2.9% | 34.8% |
The largest claimed edges have the worst win rate in the book. (CI on the 8–12% pocket is [−12.4%, +84.7%] — the direction agrees with F3's independent λ, but this bucketing is not standalone evidence.)
Operational consequence: the gates have an edge floor and no ceiling. A row claiming 40% edge clears Gate 3 and is Kelly-sized off a number the record says is mostly fantasy. KELLY_EDGE_CAP=0.15 damps the size; it does not stop the bet.
A λ-multiplier alone cannot fix this. Replaying all 390 settles with edge_estimated, applying λ·edge ≥ floor + fee(price):
| λ | floor | kept | Staked | Net | ROI |
|---|---|---|---|---|---|
| 1.00 (today) | 0.03 | 368 | $351.71 | +$41.74 | +11.9% |
| 0.40 | 0.03 | 162 | $167.25 | +$32.23 | +19.3% |
| 0.25 | 0.03 | 90 | $87.98 | +$1.47 | +1.7% |
| 0.16 | 0.03 | 44 | $39.13 | +$4.78 | +12.2% |
| 0.16 | 0.04 | 27 | $24.35 | −$2.73 | −11.2% |
At λ=0.16 with unchanged floors, the effective bar at a 50c contract becomes edge ≥ 0.29 — you keep 11% of the book, composed entirely of the ≥20% claimed-edge population that performs worst. λ-scaling with a fixed floor selects for overstatement. And because λ is a monotone transform, dropping the floor proportionally just re-cuts the same ranking at a different point. A multiplier cannot repair a non-monotone defect; a ceiling can. They are orthogonal tools, not alternatives.
2. Defects found (not statistics — bugs and holes)
D1 — CLV has never once been computed. It is broken, not missing.
trades with closing_price: 76 / 169 distinct values: {0.0: 76}
settlements with closing_price: 150 clv populated: 0
kalshi_settler.py:326 reads closing_price = float(market_data.get("last_price", 0)) / 100 from the market snapshot taken at settlement time. A settled Kalshi market returns no meaningful last_price, so closing_price is 0.0; 0.0 is falsy, so line 334's if closing_price and entry_price short-circuits and clv is None. Silently, on every settle, for five months.
There is nothing to backfill — the column is 150 zeros. CLV requires a new capture, and CLV data starts accruing the day it ships.
D2 — 24 open NFL positions, 31% of bankroll, in a sport with zero settled history
Reconciling kalshi_trades.json against kalshi_settlements.json by trade_id:
open positions: 24 at-risk: $28.50 oldest entry: 2026-05-23
KXNFLTOTAL n=11 $14.96
KXNFLSPREAD n=10 $8.68
KXNFLGAME n= 3 $4.86
NFL rows in settlements: 0 (plus 6 status='error', 2 resting)
Every one is live money (dry_run=false). Three problems:
- $28.50 on a ~$92 bankroll is 31% of the account, one sport, held up to 95 days before kickoff.
MAX_OPEN_POSITIONS=50andMAX_PER_EVENT=2both pass — no gate measures total capital deployed. You can satisfy all nine gates and still park a third of the account in September football since May. - NFL has no calibration evidence. Its
margin_stdev: 13.5indata/cache/calibration_stdevs.jsonis a hardcoded prior, not a fit — contrastbaseball_mlb: 4.025,icehockey_nhl: 2.5, which carry the decimals of something computed. - This book was admitted by a pre-L2 filter. The
.envL2 comment records that the 2026-08-18 NFL Week 1 audit found 13 of 27 open positions past the 5c spread line (to 20c) and 18 of 27 with zero 24h volume. Gate 3.6 now stops that class of row — but Gate 3.6 only runs at entry. Nothing re-checks a position already held.
D3 — Every order is a taker, and the toll exceeds the trusted edge
maker_fees nonzero: 0 / 169 trades ← never once the passive side
taker_fees nonzero: 17 / 169
fees: $11.97 on $381.66 = 3.14% of stake
ROI with fees: +14.2% | ROI at zero fees: +17.3%
At λ=0.16, a row claiming a 10% edge — comfortably above every floor in .env — carries a trusted edge of ~1.6% against a 3.14% realized fee drag. No gate tuning changes that sign.
kalshi_client.create_order() already takes yes_price_cents/no_price_cents with time_in_force="good_till_canceled" — these are restable limit orders. They never rest because the executor prices them to cross.
D4 — Config changes may never reach the live automation
Scheduler .bat files pass --unit-size and --budget explicitly, and kalshi_executor.py snapshots every gate threshold into module globals at import time. Every recommendation in this document can be shipped to .env and change nothing.
D5 — Deterministic venue rejections are being retried
The 2026-08-26 daily summary shows 3 orders rejected by the venue: Nevada residents cannot open positions in Sports, Elections, and Entertainment. This is not noise — it is the automation spending cycles on rows that structurally cannot fill.
D6 — Historical numbers were screened on gross edge
F1 (2026-08-25) folds ceil(0.07·C·P·(1−P)) into the Gate 3 floor and Kelly. Correct — and it means every figure in §1, including +14.2%, was generated by a looser filter than the one now running. The live and historical systems are not the same system.
D7 — Polymarket has produced no tradable evidence
45 dry-run passes, 1,250 rows, 54 executable, all 54 failed on edge, 0 settlements. Useful as a pricing lab; not a revenue candidate.
3. Design decisions the room agreed on
- Rolling policy may only ever tighten. A trailing window that can loosen a gate will overfit whichever slice just got lucky. Static hard stops stay in
.env; rolling state may demote to dry-run, raise an effective floor, or cap a stake — nothing else — until a pre-declared promotion rule is met. - Segment policy expires. NBA, NHL and NCAAMB are out of season as of 2026-08-26 (last settles 2026-06-14, 2026-06-15, 2026-04-02). A demotion written today would still be enforcing a 32-bet April sample in October. Every segment carries
last_settled_atand an expiry. dry_run: insufficient_evidenceis notdry_run: negative_clv. Negative CLV says the model is bad; insufficient evidence says it is unproven. Different reasons, different exits.- Cold start means pilot mode, not prohibition — live entries off until CLV capture exists, then a tiny bounded discovery allocation the operator explicitly enables.
- Legacy positions are quarantined from performance claims. Anything opened under a superseded gate set cannot be used to validate or condemn the current one.
- Shadow before live. New gate logic logs what it would have done for at least one full cycle before it rejects anything.
- Every action item names its verification step, because of D4.
4. Action items
Ordered. Each names the file, the key, the check, and the measurement that says it worked.
Phase 1 — Stop preventable waste (this week, no evidence required)
| # | Action | Where | Verify | Success measure |
|---|---|---|---|---|
| 1.1 | Freeze new NFL live entries — temporarily. Set MIN_EDGE_THRESHOLD_NFL=1.0 (the F3 World-Cup idiom — a floor at or above 1.0 can never be cleared). This is a freeze mechanism, not NFL policy. It comes back out the moment 2.3 exists; the durable rule is strategy-state cold_start → pilot, not an impossible threshold left in .env forever (that is D4 waiting to happen again). |
.env |
python scripts/doctor.py; next scan preview shows NFL off |
0 new NFL entries; the key is removed when 2.3 ships |
| 1.2 | Quarantine the 24-position pre-L2 NFL book. Hold, do not flatten — market-exiting 5–20c-wide books pays the illiquidity penalty Gate 3.6 exists to avoid. Do not add to any existing NFL event or ticker. Review exit only if the current spread is 5c or tighter and the exit price implies less expected loss than holding to settlement. | position policy + daily report | Daily summary gains a "legacy positions (pre-current-gates)" section | Legacy book excluded from every ROI/CLV claim |
| 1.3 | Jurisdiction/product eligibility preflight — and fail closed. This is a correctness bug, not just waste. Cache venue/account eligibility at startup, print it in doctor.py and in the daily summary, and treat unknown eligibility as dry_run — a transient API or config failure must never fall back to attempting real orders in a barred product. If the account truly cannot trade Sports on this venue, that is a day-one fact about the reachable book, not a trickle of rejects to discover over weeks. |
executor preflight, doctor.py |
doctor.py prints eligibility; daily summary shows 0 venue rejections |
Venue-rejection count reaches 0; unknown ⇒ no live order ever placed |
| 1.4 | Cumulative exposure gates (new). MAX_OPEN_EXPOSURE_PCT=0.20 (total open at-risk over bankroll, hard reject) and MAX_SEGMENT_EXPOSURE_PCT=0.10 (per sport/category/venue). Neither MAX_BET_RATIO nor --budget 10% covers this — both bound a single batch; the NFL book accumulated across roughly a dozen scans over three months. |
.env, app/config.py, risk gate 2b |
Force a synthetic over-limit scan; confirm the reject reason | NFL-style 31% concentration becomes unreachable |
| 1.5 | Time-to-event cap. MAX_DAYS_TO_EVENT_FOR_GAME_MARKETS=14. Futures get a separate, longer config. |
.env, risk gate |
Scan preview rejects far-dated game rows | No game-market position held more than 14 days pre-event |
| 1.6 | Propagate config to automation. Audit every scheduler .bat for --unit-size/--budget; restart long-running hosts; make doctor.py echo the effective gate values it would execute with. |
scripts/schedulers/, doctor.py |
doctor.py output matches .env |
No silent divergence |
| 1.7 | Hold all sizing. KELLY_FRACTION at 0.5 or below, UNIT_SIZE=1.00, ALLOW_PREDICTION_BETS=false, ALLOW_LIVE_BETS=false, World Cup off. Lifetime ROI CI contains zero — nothing here justifies more size. |
.env |
doctor.py |
— |
| 1.8 | Index this document. Add its row to the nearest docs/ README index. Docs in this repo are navigated through README indexes — an unlinked doc is an unreachable doc. |
docs/enhancements/README.md (or nearest parent index) |
The index lists it | Document is saved and indexed |
Phase 2 — Install the instrument (2–4 weeks)
| # | Action | Where | Detail |
|---|---|---|---|
| 2.1 | Fix the CLV bug and build real capture. Never use settlement-time last_price. |
kalshi_settler.py:320-334, new scheduled job |
At execution store entry_price_bet_side and event_start_time. A new job at T−5min writes close_price_bet_side from the live snapshot (T−0 fallback, never post-settlement). At settlement, clv = close_price_bet_side − entry_price_bet_side. All prices in bet-side probability space so CLV means the same thing for YES and NO. Persist the whole closing book, not one scalar: close_yes_bid, close_yes_ask, close_no_bid, close_no_ask, close_mid_bet_side, close_capture_at, close_capture_reason ∈ {t_minus_5, t_zero_fallback, missed}. A single close value cannot tell you whether maker CLV improved through better execution or because the close was sampled on the other side of the book — which would make the Phase 3 A/B unreadable. Publish the mean on one agreed convention; keep the raw book so it can be re-scored on mid, same-side and cross-side later. missed writes NULL, never 0.0. A falsy sentinel absorbed by a truthiness guard is precisely how D1 happened; a zero close would drag every mean toward a fictitious −entry_price. |
| 2.2 | CLV reporting slice. Mean CLV in percentage points with bootstrap CI, by sport / category / side / price band / fee role. | scripts/kalshi/betting_analysis.py |
This replaces realized ROI as the primary decision signal. Every CLV figure prints n_captured / n_settled beside it. Mean CLV over 60% of the book is not the same claim as over 97%, and misses will not be random — they concentrate in thin markets, which is where the bad bets live, so low coverage biases the mean optimistic. |
| 2.3 | strategy_state.json, protective mode only. Written by analysis, read at the risk boundary. |
scripts/backtest/strategy_state.py writes · app/config.py exposes path and flag · executor preflight reads · doctor.py prints its timestamp |
Per segment: last_settled_at, evidence_status in {active, stale, cold_start, insufficient}, expires_after_days (about 45 for daily sports, 120 for seasonal/futures), default_when_stale: dry_run, execution, reason. Never inherits stale positive ROI. |
| 2.4 | Shadow diagnostics — log only, change no decision. calibrated_edge = lambda × claimed_edge (0.16, and log 0.25/0.40); gate3_ceiling_would_reject at EDGE_CEILING_WARN=0.20 and EDGE_CEILING_REJECT=0.30; a legacy_gateset label on every trade row. |
edge detector + executor logging | After one full cycle, review firing rates before anything goes live. |
| 2.5 | Re-run calibration_study.py and correlation_check.py once CLV exists, and re-measure lambda against CLV rather than outcomes. |
scripts/backtest/ |
Lambda's CI is currently [−0.04, +0.42] — far too wide to size against. |
| 2.6 | The daily scoreboard. Every mechanism above is invisible unless it surfaces daily. Six lines, no more: (1) `eligibility: OK | UNKNOWN→dry_run; (2) risk_config_fingerprint()— see 2.7; (3)open exposure: $X (Y% of bankroll) — legacy $Z excluded; (4) CLV last 30d: +A.A pts [CI], n=B/C captured (D%)`; (5) segments in dry_run, with reason; (6) kill-switch distance — which trigger is nearest and how far. |
daily summary job |
| 2.7 | risk_config_fingerprint() — make D4 an invariant, not an audit. One function, defined at the risk boundary (the executor) and called by both doctor.py and the daily summary. It hashes the actual runtime decision inputs: .env values, command-line overrides from scheduler .bat files, module-level constants as they stand after process start, the strategy-state file path + mtime/hash, and the eligibility-cache version. |
kalshi_executor.py, doctor.py, daily summary |
Hashing .env alone would miss every one of D4's failure modes. With one shared function, the thing printed is the thing executing — which turns "audit the .bat files" from a recurring chore into a check that cannot silently lapse. |
Phase 3 — Remove the structural cost of trading (after 2.1–2.3 land)
Maker-fill A/B. Kalshi maker fees are zero. On the settled book that is $11.97 on $381.66 — a +3.14 percentage-point ROI swing that requires no improvement in the model. It is the only lever in this document whose payoff does not depend on Edge-Radar having skill.
- Not a new default — a bounded experiment. "Post at the bid" is at least three strategies: best-bid passive (lowest fee, lowest fill rate, highest adverse selection), one-tick-inside-spread (better fill, gives up spread capture), and today's aggressive baseline.
- Require a post-only flag if the venue offers one. If not, place a maker-test order only when the limit price sits strictly inside the opposite side so it cannot accidentally cross. On a 1c-wide market there may be no safe maker test at all.
- Eligibility: passes Gate 3.6; spread 3c or tighter preferred, never wider than 5c; nonzero 24h volume once that floor is on; event inside the game-market window; no legacy positions.
- Allocation: deterministic by ticker hash, never chosen after seeing the market. Start 25% maker / 75% taker — fill loss can starve the sample and the bankroll is small.
- Sizing unchanged. Fee savings must not increase stake during the experiment.
- Recompute edge at the intended order price, never inherit the scan row's displayed market price. Log
scan_price,limit_price,fill_price,would_cross_at_submit,liquidity_regime,fee_role. Without these you cannot separate "maker improved EV" from "maker selected a different price distribution." - Metrics: fill rate, partial-fill rate, mean CLV, realized spread captured, cancellation rate via
RESTING_ORDER_MAX_HOURS, post-fee ROI, and missed-opportunity cost on unfilled orders that later moved favorably. - Stop rule: maker-fill CLV underperforms taker-fill CLV by more than 1 point after 100 filled maker orders, or fill rate falls below 25% on otherwise-qualifying rows. Disable maker mode for that segment.
- Promotion: becomes the default only if it nets at least 1 percentage point after adverse selection, with the lower bootstrap bound above 0, over 100 or more filled maker orders.
Note the symmetry: Gate 3.6 and a maker strategy are the same policy approached from two directions — only trade where a real two-sided market exists.
Phase 4 — Scale, if and only if the evidence arrives
Turnover is the other multiplicand: $382 across five months on a ~$92 bankroll, median stake $0.80. Even a genuine, durable +10% edge returns about $38 in five months. Scaling comes last, and only on a pre-declared trigger.
Day-90 checkpoint, with a pre-declared losing branch. At 90 days from CLV ship:
- Coverage under 60% → the capture job is the problem. Nothing else in the numbers is readable yet. Fix capture; do not interpret CLV.
- Coverage fine, global CLV CI still straddles zero → the answer is not "run longer at this size." A median stake of $0.80 will not resolve it in a second 90 days either. The honest branch is: shrink to the segments with the tightest CLV CIs, or stop. Writing that down now means the review has a losing option as well as a winning one.
- Coverage fine, CLV lower bound above 0 → the promotion rule below applies.
Promotion rule (deliberately harder than the kill rule, because scaling is where the damage happens): at least 150 CLV-captured bets and at least 60 calendar days, global mean CLV above +1.0 point with the lower 95% bound above 0, and no segment sitting in a qualifying negative-CLV kill. Then raise exactly one knob: UNIT_SIZE from $1.00 to $1.50 (the longshot lane). Wait a further 60 days before touching KELLY_FRACTION (the favorites lane). Never both at once.
5. Kill switches
Written now, while nothing is at stake, so the decision is not made emotionally after a bad month.
| Trigger | Threshold | Effect |
|---|---|---|
| Global negative CLV | Rolling 90 days, at least 150 captured bets, mean CLV at or below −1.0 point with the 95% bootstrap upper bound below 0 | All automated execution to dry-run only. Stop betting real money. |
| Segment negative CLV | At least 40 captured bets in a sport/category/side, mean CLV at or below −1.5 points with upper bound below 0 | That segment to dry-run, reason negative_clv |
| Insufficient evidence | 120 days elapsed with fewer than 150 captures for a segment | That segment to dry-run, reason insufficient_evidence — off-season quiet is not the same as a bad model, but silence defaults to off, not to running |
| Venue/product rejection | Any repeated structural rejection (e.g. the Nevada restriction) | Immediate disable of that venue/product until doctor.py reports it eligible. No sample threshold — this is deterministic, not noisy |
| Daily loss | MAX_DAILY_LOSS breach |
Unchanged, stays as-is |
Kill-switch distance is defined mechanically, so the daily summary's line 6 has no author discretion. Per active trigger, compute a normalized distance and report the minimum:
| Trigger | Distance |
|---|---|
| Global negative CLV | upper_ci − 0, alongside n_captured / 150 |
| Segment negative CLV | same, against that segment's threshold and n / 40 |
| Exposure | cap_pct − current_pct |
| Insufficient evidence | 120 − days_since_first_capture_or_policy_start |
| Venue/product | binary — eligible, or distance 0 |
Line 6 prints the nearest trigger and its distance. This is the operator's early-warning surface; it must be generated, not written.
6. Disagreements that survived, and what would settle them
| Question | Where we landed | What would settle it |
|---|---|---|
| Lambda multiplier vs. edge ceiling | Both, and both in shadow first. Lambda sets how much to trust a claimed edge; the ceiling handles where trust inverts. The replay showed lambda-with-fixed-floors selects for the worst population, so lambda cannot go live as a Gate 3 transform. | Re-measure lambda against CLV rather than outcomes, then check whether the ceiling still fires on rows lambda has already killed. If it never fires independently, drop it. |
The sub-10c longshot lane (MIN_MARKET_PRICE=0.10, flagged in CLAUDE.md as an open experiment) |
Keep, tiny fixed stake, and stop counting it as evidence. F3 says the model's only clean signal is on cheap contracts (32c or below: high-edge half wins +10.8 points more than low-edge), so killing it may kill the one real edge. But three MLS tickets are the entire lane's P&L, so it proves nothing about strategy. | Mean CLV on sub-10c fills, with CI, over 40+ captures. Realized ROI on this population will never converge. |
| Adverse selection on maker fills | Unsized. Neither of us can quantify it from this data. It is written above as a risk resolved by the A/B, not as a settled recommendation. | The Phase 3 A/B: maker-fill CLV vs. taker-fill CLV over 100 filled maker orders. |
| Whether the Jun–Aug break is signal | Treat it as real (p = 0.018) but do not tune against it. It is one comparison among several we ran. | CLV over the next 90 days. If CLV was positive through a losing quarter, the break was variance; if CLV went negative alongside ROI, the model decayed. |
| Whether to flatten the NFL book | Hold, with the exit condition in 1.2. We do not have current bid/ask, and exiting into a 20c-wide book could cost more than the position is worth. | Pull live bid/ask on all 24 tickers. If the spread is 5c or tighter and the exit price beats hold-to-settlement EV, exit that ticker. |
7. What this plan does and does not claim
It does not claim Edge-Radar is profitable. The lifetime CI contains zero, the last three months are negative, the model's Brier score lost to the market in 6 of 6 months, and the entire positive P&L is five bets.
It does not claim Edge-Radar is unprofitable either. 402 settles at a median stake of $0.80 cannot resolve a question this fine. That is precisely the problem the CLV work exists to fix.
Phases 1 and 2 reduce loss and buy information. Phase 3 is the first item that increases revenue — and it does so without requiring the model to improve, which is why it is separated from Phase 4. Scaling turnover on a taker-only system whose trusted edge is smaller than its toll is not revenue optimization; it is a faster way to pay fees.
The honest summary for the operator: the next 90 days are a measurement project, not a betting strategy. At the end of them there will be a number — mean CLV with a confidence interval — that says whether there is anything here worth scaling. Today there is no such number, and every decision made without one is a guess.
Produced by agent-chat conversation #54, 2026-08-26. Saved to docs/enhancements/betting-strategy-review-2026-08-26.md. All figures recomputed directly from data/history/ at the time of writing; nothing quoted from prior reports without independent verification, except where explicitly attributed to CHANGELOG entries F1/F3/F4, C4, C10, C11, R1, R28, and L2.
No further objections. The final artifact now includes the material issues I wanted covered: raw close-book capture with null-on-missed semantics, fail-closed eligibility, indexed docs hygiene, temporary NFL freeze wording, daily scoreboard, runtime config fingerprint, mechanical kill-switch distance, and the day-90 failure branch.
I am satisfied with this as the collaboration result.