Metadata-Version: 2.4
Name: overlay
Version: 0.1.0
Summary: An arbitrage and +EV betting engine that knows how wrong it might be.
License: Bookbreaker / Overlay — Software Licence
        Copyright (c) 2026 Matthew Kerr. All rights reserved.
        
        Version 1.0, 17 August 2026.
        
        
        WHAT YOU MAY DO
        
        You may download, install and run this software free of charge, on any number
        of machines you control, for as long as you like.
        
        You may use it to make money. Placing bets, sizing them, tracking them and
        keeping the proceeds are exactly what it is for, and nothing in this licence
        limits what you do with your own bankroll or your own winnings. Any profit or
        loss is yours.
        
        You may read the source, modify your own copy, and keep those modifications
        for yourself.
        
        
        WHAT YOU MAY NOT DO
        
        You may not redistribute this software, modified or unmodified, in source or
        compiled form, to anyone else.
        
        You may not sell it, rent it, sublicense it, or bundle it with something you
        sell.
        
        You may not operate it as a service for other people — hosting it, running it
        on their behalf, or reselling its output as a product or a subscription.
        
        You may not remove or alter this licence, the copyright notice, or the
        provenance labelling in the software's own output.
        
        
        THAT LAST ONE, AND WHY IT IS HERE
        
        This engine reports where each of its numbers came from: whether a figure was
        counted from data or is a stated prior, how many observations sit behind it,
        and how stale the inputs are. Those labels are not decoration. A build that
        strips them prints the same digits while destroying the only thing that makes
        them worth reading, and it would still carry this project's name.
        
        
        NO WARRANTY
        
        This software is provided "as is", without warranty of any kind, express or
        implied, including but not limited to the warranties of merchantability,
        fitness for a particular purpose and non-infringement. In no event shall the
        author be liable for any claim, damages or other liability, whether in an
        action of contract, tort or otherwise, arising from, out of or in connection
        with the software or the use or other dealings in the software.
        
        It is a calculator, not advice. It does not know whether a bet is legal where
        you are, whether a sportsbook will honour a price, or whether its own priors
        apply to your markets. Every figure it prints is conditional on inputs you
        supplied and assumptions it labels. Check them.
        
        Gambling carries risk of financial loss. 21+ where applicable. If it stops
        being fun, it is not fun — https://www.ncpgambling.org/help-treatment/
        
        
        ON THIS LICENCE CHANGING
        
        These terms are deliberately narrow, because narrow terms can be widened later
        and wide ones cannot be taken back. A future release may be issued under a more
        permissive licence; if that happens, it applies to that release forward.
        Whatever you have already downloaded stays licensed under the terms it shipped
        with, and nothing here is revoked retroactively.
        
        For any permission this licence does not grant, ask: mattkerr09@gmail.com
        
Keywords: betting,arbitrage,expected-value,devig,kelly,odds
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial
Classifier: License :: Other/Proprietary License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Overlay

**An arbitrage and +EV engine that knows how wrong it might be.**

*Overlay* is the betting term for a price better than the true probability
warrants. It is what the product looks for, so it is what the product is
called.

Status: **engine, local record, calibration loop and performance tracker —
688 tests passing, 75/75 deliberate breaks caught, 56/56 CLI commands clean.**
The site is live at [bookbreaker.bet](https://bookbreaker.bet). A live odds
feed is one class away; no UI yet. What exists is the maths every screen in this category
gets partly wrong, plus the two models none of them ship at all.

```bash
python3 backend/cli.py demo
```

```bash
python3 backend/cli.py books NJ
```

```bash
python3 backend/cli.py promo --bonus 1000 --max-hedge 2000 --state NJ
```

---

## The problem with every tool in this category

They all show you the same three screens — arbitrage, positive EV, middles —
computed the same way, and they all report a single number to two decimal
places. Three things are wrong with that number.

**It isn't one number.** Removing a book's margin to recover its true belief
is a modelling choice, not arithmetic. Four defensible methods —
multiplicative, additive, power, Shin — disagree by more than most edges are
worth. On a real Pinnacle NFL moneyline of -145 / +125, this engine reports:

```
outcome          additive  multiplicative           power            shin   consensus    spread
0                 57.37%          57.11%          57.50%          57.37%      57.34%     0.39%
```

Nearly four tenths of a point of probability, on a market where a 2% edge is
a good day. Every competing tool picks one of those columns, hard-codes it,
and prints the resulting EV as though it were measured. It is one modelling
opinion rendered as a fact. Overlay reports the spread, and `ev_low` — the
edge that survives the method that likes it least — is the number that
decides whether to bet.

**It ignores whether you can get the bet down.** The quote was seen at some
past instant. Between then and your bet landing sit the poll interval, the
network, the render and the human. Books do not hold prices for any of it, so
a screen sorted by raw EV is sorted partly by how stale its own data is — the
biggest numbers concentrate on the books that moved most recently, which are
the books most likely to have moved again. The top of a raw-EV leaderboard is
enriched for prices that no longer exist. Users experience this as "it sent me
to a line that wasn't there", and no tool attaches a number to it.

Overlay does. Fill probability is modelled per book and market class and
multiplied through, which reorders the screen:

```
DraftKings   over@220.5     +110 (2.100)
             EV +5.37%  band +5.00%..+5.96%  fill 94%  ->  realised +5.02%
```

And the age behind that number is measured from **the book's timestamp, not
ours**. The gap between them is feed latency, and during it the price may
already have moved. Measuring from receipt understates age by exactly that gap
and so overstates the chance of getting on — worst on the fastest markets,
where the gap is largest:

```
dk / h2h
  feed latency     6.3s  (measured)
  a quote 10s old is really 16s old once the feed's lag is counted
  still there: 76%   (a screen ignoring latency would say 84%)
```

A 4% edge there is worth 3.03%, not 3.36%.

**It optimises the numerator and ignores the denominator.** An account that
gets limited stops earning. Edge you cannot place is worth zero, so account
lifetime is not a footnote next to EV — it is what EV is divided by. The
standard experience with these tools is three excellent weeks followed by a
$5 maximum stake.

---

## The staking problem, concretely

Every arbitrage calculator solves the continuous optimum and hands it over:

```
exact stakes (the number every other calculator gives you):
      493.98 at +110 (2.100)
      506.02 at +105 (2.050)
  profit 37.35 on 1000.00  (3.73%)
```

`$493.98` is the single most-cited fingerprint risk desks use to identify
arbitrage. A recreational bettor stakes 50, 100, 250. Nobody types $493.98.
Telling the user to round it themselves moves the hard part onto them — and
rounding an arb naively breaks it, because the legs are not symmetric.

Overlay optimises over *round* stakes directly. It enumerates the plausible
round combinations near the optimum, scores each by its **worst** outcome, and
returns the best one that is still an arbitrage:

```
round stakes:
         490 at +110 (2.100)
         500 at +105 (2.050)
  profit 35.00 on 990.00  (3.54%)
  rounding cost 2.35 — paid to not look like a bot
```

The cost is named and reported rather than hidden. A tool that conceals its
own trade-offs is not one you can check.

---

## What "gets smarter" actually means here

Not a slogan. Four parameters start as **stated priors** and are replaced by
measurements from your own betting:

| What is learned | From what | Replaces |
|---|---|---|
| Which devig method predicts the closing line, per sport and market | your graded bet log | equal weights across four methods |
| How long a quote survives, per book and market class | your accepted/rejected bets | shipped `PRIOR_TAU` table |
| How much each book's price is worth as evidence | closing-line error by book, per book | the `BookTier` default weights |
| How correlated your simultaneous bets really are | realised joint outcomes | an asserted 0.15 |

The supervision signal is **closing line value**, not profit. Profit over a few
hundred bets says almost nothing — a 2% edge over 500 even-money bets has a
standard deviation of about 4.5% of turnover, so losing months are routine and
winning months prove nothing. Waiting for profit to confirm a model means
waiting years; adjusting on early profit means fitting noise. CLV converges far
faster, and it is a proper scoring rule, so a devig method cannot win by being
systematically timid or systematically bold.

The report refuses to claim an edge it cannot support:

- under 30 bets → `"12 bets — too few to say anything; need 30+"`
- positive but noisy → `"3.1% CLV but not significant (t=1.4); consistent with no edge"`
- clear → `"beating the close by 2.8% (t=4.2)"`

### The loop, running

```
method_weights.global: calibrated on 49 bets, verified on 21
                       out-of-sample error 0.000342 -> 0.000014
    power            0.850
    shin             0.050
```

Three things make that a calibration rather than an overfit:

**The hold-out is by time, never at random.** Consecutive bets cluster — same
event, same market, same evening, often the same line moving. A random split
puts near-duplicates on both sides, so the held-out slice is partly a copy of
the training slice and *any* candidate scores well on it.

**A candidate must beat the incumbent out of sample or it is discarded.**
Without that gate this would look identical from outside: weights would change,
numbers would move, and nothing would get better. Run twice on the same data,
the second run declines — which is a normal result, and prints as one.

**It refuses below 30 graded bets, out loud.** Weights from twelve bets are
worse than no weights, because they carry the appearance of measurement.

And the result reaches the price. Everything learned loads through one entry
point, so a market cannot be priced half-calibrated:

```
calibration: uncalibrated — devig and book weights are shipped priors
  over@220.5     50.03%

calibration: devig led by multiplicative, book weights measured; 0d old
  over@220.5     49.73%
```

Past 90 days the weights are labelled `STALE` and still used — reverting to
defaults silently would be a behaviour change nobody could see. The age shown
is the *oldest* half of the calibration, not the newest: a price built from
fresh devig weights and year-old book weights is only as current as the
year-old half.

---

## Account longevity: what this does and does not do

The heat model reads **bet shape** — stake sizes, timing, market mix,
velocity, whether the profile contains any recreational betting at all. These
are documented signals risk desks profile on, and every one of them is a
choice a bettor makes about their own betting. Heat decays with time; a book
over budget gets its stakes rounded, its bets delayed, its edge floor raised,
or the bet deferred, in that order — cheapest lever first.

Books that never limit winners score zero heat and get no shaping at all.
Spending edge on anti-limiting tactics at Pinnacle, or on a prediction market
with no bookmaker in it, buys protection from a risk desk that does not exist.
That is the most common way these tactics are applied wrongly, and there is a
test for it.

**It will not do multi-accounting, identity or KYC workarounds, or device and
location spoofing.** That is fraud rather than staking discipline, and the
line is drawn in the code, not in a policy document: `heat.py` consumes bet
attributes only and has no access to identity or network state.

Overlay is also advisory. It tells you what to bet; you place it. Automated
placement against a book's own interface is a different product with a
different risk profile, and it is not this one.

---

## Welcome offers, valued honestly

A "$1,000 bonus" is never worth $1,000, and the gap is the whole feature. A
bonus bet does not return its stake, so it is worth about half its face if bet
naively. Converting it means hedging it, and every guide says "convert on a
longshot" without mentioning what that costs at the second book:

```
    free leg    hedge at   hedge stake   guaranteed    rate
        +100        -110           524          476   47.6%
        +200        -230         1,394          606   60.6%
        +400        -450         3,273          727   72.7%  (over your hedge limit)
       +1200       -1400        11,200          800   80.0%  (over your hedge limit)
```

Converting a $1,000 bonus at +1200 needs $11,200 sitting at another book. The
hedge column is the constraint the advice always omits, and `--max-hedge` marks
what is out of reach.

The other three shapes each have their own optimisation, and two of them invert
the usual instinct:

- **Bet-and-get** — nearly free. The qualifying bet is a *fee*, so it should be
  the lowest-hold market available, not a bet you like.
- **First-bet safety net** — only pays when the qualifier *loses*, so the
  qualifier should be a longshot. Worth `(1 - q) × refund × conversion` on its
  own; the underlying bet contributes its own EV, which at fair odds is zero.
- **Deposit match** — a rollover is a price, not a condition. A matched bonus at
  10× rollover is worth exactly zero at a **5% hold**, barely above a standard
  -110/-110 market and below the hold of the restricted markets many books allow
  for playthrough. `breakeven_hold` computes that threshold, because the useful
  question is never "is this offer good" but "how cheap does my rollover need
  to be".

And it tracks what you are holding, because a bonus bet that lapses unused is
a pure loss:

```
holding 500 face, worth about 281 at 75% conversion;
2 expiring within 3 days (soonest in 1.5);
200 face already lost to expiry
```

That last figure is the one nobody else shows. Every other number there is a
forecast; that one already happened.

The catalogue stores offer *shapes*, never dollar amounts. Terms change weekly
and vary by state; a hard-coded "$1,000 bonus" in a source file is a claim about
someone else's product that starts rotting the day it is written.

---

## Which books you can actually use

An arbitrage between two books a user cannot both hold is noise with a number
attached, so jurisdiction is the first filter in the pipeline, not a settings
page. 24 venues with tier, commission, and — the field that drives the heat
model — whether each limits winners.

```
FL — 2 venues  (table read 2026-08-17, 0 days ago)
  exchange  Kalshi                [never limits]
  retail    Hard Rock Bet
  1 anchor venue(s) — +EV numbers here rest on a sharp or exchange price.
```

Three deliberate choices:

- **The table reports its own age.** A jurisdiction table shown without its date
  asserts "true now", forever. `stale_days()` exists so no screen can display it
  without being able to say how old it is.
- **Gaps are named, not silent.** The lottery-run states are listed in
  `COVERAGE_GAPS` with the reason, so an empty result is distinguishable from a
  bug.
- **"Jurisdiction unknown" is not "unavailable".** Pinnacle and Polymarket carry
  no state list because we don't know theirs. They are hidden by default and
  surfaced by `--unverified` as "check this yourself" — an unverified "legal
  here" is the one error in this table that could cost a user more than a missed
  bet.

This is a dated starting point for your own check, not advice.

---

## Architecture

```
backend/overlay_engine/
  odds.py        conversion, overround, hold          — the boundary
  devig.py       four methods and their disagreement  — the truth layer
  models.py      Book/Market/Quote/Edge, book tiers
  fair.py        log-odds blend, weighted by sharpness
  ev.py          EV as an interval, discounted by fill
  arb.py         detection + the round-stake solver
  kelly.py       fractional Kelly, correlation-aware
  staleness.py   per-book quote survival             — nobody else ships this
  heat.py        account limit risk + stake shaping   — nobody else ships this
  clv.py         closing-line grading and the learning signal
  catalog.py     24 venues, jurisdiction, who limits winners
  promo.py       welcome offers valued after every cost
  ledger.py      the local record — SQLite, and the reason anything learns
  calibrate.py   refits devig weights from the record, or refuses to
  grade.py       settles bets against recorded closes, or says why it cannot
  feed.py        the provider seam, timestamp discipline, and capture
  pricing.py     the one entry point — loads every learned parameter
  middles.py     counted margin windows, or an honestly labelled fallback
  performance.py P&L, ROI and win rate, each with its interval
backend/cli.py   demo, devig, arb, fill, kelly, books, promo,
                 ledger, capture, grade, calibrate
scripts/
  gates.sh       all three gates, with their real counts
  break_test.py  breaks 75 invariants, confirms each is caught
  smoke_cli.py   runs every CLI command, asserts on output not just exit code
docs/
  ROADMAP.md     what the build loop takes next
  DATA.md        the feed decision
  IMPROVEMENT_LOG.md
```

No runtime dependencies. Deliberate: every number here is arithmetic that can
be pointed at, and a dependency that computes odds for us is a number we did
not measure.

---

## Testing

```bash
./scripts/gates.sh   # 688 passed · 75/75 breaks caught · 56/56 CLI clean
```

A passing suite proves the tests ran, not that they can fail. `break_test.py`
deliberately breaks seventy-five load-bearing invariants — the arb margin formula,
commission netting, the fill discount, the fair-value hold-out, the CLV
sample-size floor, a bonus bet treated as returning its stake, a deposit match
quoted without its playthrough, a bet stored without the devig spread that
produced it, calibration fitted on the very slice it is verified against, a bet
graded against a close at a different line — and confirms a named test catches
each one. It baselines
every gate as green first, so a test that was already failing cannot be scored
as a catch, and it verifies each restore by re-reading the file rather than
trusting a write nobody watched.

It has already earned its place twice.

The CLV sample-size gate passed against a build with the floor deleted, because
the test used ten identical CLV values — zero variance, undefined t-statistic,
a pass no matter what the code did. The test now carries a spread that makes
t ≈ 3.8, so only the sample-size floor can hold the verdict back.

Then the harness itself started lying. A previously-caught case flipped to
MISSED after an unrelated edit: `common &= set(s)` → `common |= set(s)` is a
*same-size* change, and landing in the same mtime second as the original, it
defeated Python's `(mtime, size)` bytecode invalidation — the stale `.pyc`
loaded, the broken code never ran, and a perfectly good gate was reported as
bad. Every run now purges `__pycache__` and compiles from source, and the
harness refuses to start unless at least one case is a same-size edit, so the
guard can never be deleted without something noticing. A harness that reports a
false result is worse than no harness, because its whole purpose is to be what
you trust when a green suite is not enough.

---

## Two bugs this design already caught

**A book was pricing against a fair value it helped set.** Even at 6% weight, a
soft book pulls the consensus toward its own price, shrinking exactly the edge
being detected — hardest on the markets where that book is the lone outlier and
the edge is largest. Every book is now held out of the blend before being
priced against it.

**Uncertainty was being set by the softest book in the market.** Book
disagreement widens the confidence band, which is right; but including a
6%-weight retail book made the band as wide as that book's error, which killed
every genuine soft-book overlay whenever a second soft book was priced worse.
Only books carrying at least 10% of the weight widen the band now. A book can
be evidence about the price without being evidence about the error.

---

## The competition, as of 2026-08-17

Prices change and these are dated for that reason. Each is what the vendor's
own page or a linked review stated on that date.

| | Price | Notable |
|---|---|---|
| [OddsJam](https://oddsjam.com/) | Gold ~$199.99/mo; Global ~$399.99/mo <sup>[1]</sup> | 150+ books, "industry's fastest data" — no latency figure published |
| [AVO](https://www.avo.bet/) | Free (3% profit cap); Pro $88/mo; day pass $22 | 89+ books; free tier's odds screen runs on a stated 10-second delay |
| [Betstamp PRO](https://www.betstamp.com/) | ~$347/mo, demo-gated <sup>[2]</sup> | pro-grade odds screen |
| [Crazy Ninja Odds](https://crazyninjaodds.com/) | from $5/mo | free devigger and arb tools, the enthusiast standard |
| [Pikkit](https://pikkit.com/) | free; Pro tier | bet tracker — auto-syncs from 30+ books by linking accounts, CLV against 100+ books |

Pikkit is the closest thing to a tracker comparison. Its CLV reporting is the
feature Overlay calibrates *on* rather than merely displays; its account-linking
sync is a thing Overlay will not build, because it means handling sportsbook
credentials. CSV import reaches the same place without them.

None of the four publishes a fill-rate, a rejection rate, or any measure of
how often a surfaced line is still available. That absence is the gap this
engine is built into.

<sup>[1]</sup> [XCLSV review, 2026](https://xclsvmedia.com/oddsjam-review-2026-is-this-199-month-betting-tool-worth-it/) ·
<sup>[2]</sup> [oddsplays review, July 2026](https://oddsplays.com/reviews/betstamp-pro/)

---

## The record

Everything above learns from outcomes your own betting produces, so the ledger
is the precondition for all of it. Plain SQLite at `~/.overlay/overlay.db`
(`$OVERLAY_HOME` relocates it), on your machine, no account, no server.

```
ledger: ~/.overlay/overlay.db
  bets              34   (34 graded, 0 awaiting a close)
  attempts          34   (7 rejected)
  Enough graded bets to calibrate.
  CLV: beating the close by 4.71% (t=29.5)
  fill model — measured:
    dk           moneyline    half life    44s  (34 attempts)
```

One rule shapes the whole schema: **a bet is recorded with the model state that
produced it, or not at all** — and that means the whole devig spread, every
method's answer at bet time, not the fair probability alone. Calibration asks
which method predicted the closing line best. Six weeks later that is
unanswerable from prices nobody kept: the market has moved, the book's line is
gone, and there is no way back to what `power` would have said.

So `AtBet.validate()` refuses what `NOT NULL` cannot see — an empty devig dict,
a band that does not contain its own midpoint, a probability of exactly 0 or 1.
All three store cleanly, grade to nothing, and look completely normal in a table
listing months later.

Two more refusals, both in the same spirit: an ungraded bet is excluded rather
than counted as zero, which would drag every average toward nothing and make a
small sample look more certain than it is; and a devig method missing on any bet
is dropped rather than filled, because a method scored on a subset is not
comparable with one scored on the whole set.

### Grading feeds itself

`grade` settles bets against recorded closing lines, so nothing in the chain
needs a human with a calculator:

```
graded 40 of 43; 3 line moved

not graded:
     3  line moved
```

Almost all of that module is refusals, because every shortcut available here
produces a closing number that looks fine and is undetectably wrong:

- **Never mix books.** One book's over and another's under is the arbitrage
  between them laundered into a probability — a price no book ever offered. The
  error correlates with exactly the markets where books disagreed, which are the
  markets a +EV screen sent you to, so the bias would land hardest on the bets
  the model was most confident about.
- **Never cross a line.** A bet on over 220.5 cannot be graded against a close
  on 221.5. Line movement is the normal case on totals, so this fires often —
  and an ungraded bet is a known unknown, while a bet graded at the wrong line
  is a wrong number wearing a right one's clothes.
- **Never grade off a soft book alone.** A retail close is the last price that
  book would offer recreational money, not truth. Graded against it, a bet that
  beat a soft book scores as beating "the market".

---

## The tracker: what the record can and cannot support

Every tracker reports ROI as a fact. It is not. A 2% edge over 500 even-money
bets has a standard deviation of about 4.5% of turnover, so a bettor with a
genuine edge routinely shows anywhere from -7% to +11% over a season. On a real
220-bet record:

```
  settled            220   (112W 99L 9P)
  profit        +2,100.00
  win rate         53.1%   of 211 decided

  money ROI       +7.66%   what actually happened
  flat-bet ROI    +5.91%   betting the same amount every time
  staking         +1.76%   sizing added this much

  +5.91% flat-bet ROI, but the interval spans zero (-7.0% to +18.9%)
  — indistinguishable from break-even
```

Every other tracker prints `+7.66%` and stops.

**Pushes are not losses and voids are not bets.** Counting a push as a loss
deflates win rate; leaving a voided stake in turnover deflates ROI. Both errors
are silent and neither flatters anyone.

**Money ROI and flat-bet ROI are separated, and the gap is the point.** One is
what happened, the other is what would have happened staking level. The
difference is the only direct read on whether the Kelly sizing is earning its
keep — no other tracker surfaces it.

**And the bar rises with how many slices you check.** Tag your bets and the
report corrects for it. On 600 pure coin-flip bets across ten tags:

```
  steam                   60    +16.67%     -8.5% to +41.8%
  divisional              60    -23.33%     -48.1% to +1.5%

  10 tags testable; at a single-test bar 0 look significant, 0 survive
  the correction for testing 10 at once; with 10 slices there is a 40%
  chance one clears the ordinary bar by luck alone
```

Every tracker in this category paints `steam` green and `divisional` red. Both
are noise. With twelve slices the chance at least one looks significant by luck
is about 46% — a user who invents labels until one looks profitable is running
a search, not a test.

**Leaks are gated on sample size.** In the record above, NBA runs at -14.86%
over 74 bets and is deliberately *not* flagged: its interval is -37.1% to
+7.3%, still spanning zero. With enough slices something always looks bad, and
a screen that flags "you lose on Tuesday night hockey" from nine bets is
manufacturing a pattern rather than finding one.

---

## Middles, counted rather than assumed

Results are not smoothly distributed. Football margins pile up on 3 and 7
because of how the sport scores, so a normal curve gets the shape wrong in
exactly the place the whole bet lives. Same prices, one point apart:

```
over 2.5 / under 3.5     break-even 4.76%   EV +10.53%
over 4.5 / under 5.5     break-even 4.76%   EV  -0.53%
```

A smooth curve prices those identically.

**Nothing here ships a margin distribution.** No published table this build
could verify covers every margin with its sample size, and inventing one would
be the number-you-did-not-measure the rest of this repo refuses. The model
counts recorded finals and declines below 200 games — a margin that has not
happened in 40 games is not a margin with probability zero. Below the floor the
normal approximation is used and labelled `approximated`, because a middle
priced off a smooth curve and one priced off two thousand real games are
different claims.

`breakeven_p` is the honest anchor: arithmetic on the two prices, depending on
no distribution at all. A middle whose break-even is 4.8% and whose counted
window is 11% is a bet; one where those are reversed is a story about key
numbers.

---

## Not built yet

Stated plainly rather than implied by omission:

- **No odds feed.** The engine takes `Quote` objects; nothing fetches them.
  This is the next sprint and the biggest single decision in the project —
  see `docs/DATA.md`.
- **No UI.** CLI only.
- **No live odds provider.** The seam, the timestamp discipline, the capture
  pass and a replay provider are all built and tested; what is missing is one
  class that polls a paid feed. Pick it on the latency this engine measures
  rather than on the vendor's claims — two of the four candidates in
  `docs/DATA.md` publish no latency figure at all.
- **No live offer terms.** `promo.py` computes what any offer is worth; nothing
  fetches current amounts or per-book rollover restrictions.
- **No score source.** `middles` counts real margins once 200 games exist for a
  sport, but every score is entered by hand today.
- **Nothing is calibrated.** Every learned parameter is currently its shipped
  prior, and the CLI says so on every run rather than letting a prior pass for
  a measurement.
