privatepanda.co

projects · flagship

Vantage near go-live

solo · Python 3.12 · outcome-space algebra · certified LP pricing

A long-running attempt to make differently named betting markets comparable without pretending that their names are their meaning. Finds and prices only; it never places a bet.

Before Vantage

Vantage is not the first version of this idea. DutchingEngine, ParlayArbitrage, and RealityEngine came before it. They accumulated useful pieces, but also several assumptions that worked only for the examples they had been written around. One adapter branch expected a packet shape the live source did not actually send and was abandoned. Some outcome-region logic survived; the old classifier later became a test oracle rather than production architecture.

The recurring mistake was trying to normalize names. Sportsbooks can describe the same payoff with different labels, and two markets with unrelated names can cover every possible result together. No amount of string cleaning makes “Clean Sheet: Yes” and “Over 0.5 Goals” look equivalent. Vantage restarted around a different representation: each selection becomes the set of outcome cells where it pays.

The model that finally held up

market amarket btogether they cover every outcome

A sport defines a finite outcome domain. A market template maps a raw selection onto a bitmask over that domain. Union, intersection, and complement then answer coverage questions without knowing which sportsbook supplied the text. Tennis uses set-by-set sequences rather than only final tallies because two 2–1 matches can disagree about who won the first set. Asian quarter lines need five settlement states, not a Boolean win/loss.

Once settlement became exact, the old equal-payout dutching formula stopped working. It can invent profit, leave money unused, and cannot price overlap. Two custom simplex implementations followed. One was exact and 100–400 times slower than HiGHS while still being wrong. The specialized float version was faster, but wrong by as much as 7.07 across randomized cases. The current solver uses SciPy’s HiGHS backend and independently checks the primal result and a dual witness.

Then the scope grew again

Pairs became general covers

The first search only handled pairs and triples. Real examples exposed covers with more legs, so it became branch-and-bound set-cover enumeration. “Middle” also had to be redefined: it is a full cover with overlap, not simply a pair of thresholds that look close.

One source became several

Adapters now feed a common source model, but matching the same real game across books became its own problem. Orientation errors can turn a losing position into a fake 29% arb. Identity matching is conservative; uncertain pairs are quarantined instead of silently merged.

Cell counts became probability mass

Writing off 30% of an outcome grid may discard only 0.5% of its modeled probability. Devigging, complement matching, cross-book consensus, and model-quality gates were added so the engine ranks the risk that matters rather than the geometry that is easiest to count.

The scanner became a live application

Offline runner and persistent-store paths were eventually removed. The current system publishes atomic live generations to a browser board, supports manual and automatic write-off analysis, and searches cross-game combinations. That consolidation also exposed scale problems that separate scripts had hidden.

The stabilization pass was not ceremonial

By late July the project had hundreds of tests and enough surface area that passing them was no longer convincing. An independent verification pass found a cross-game position advertised as +330.7% guaranteed that could lose 100%, a 29% arb created by a home/away flip, benchmark counters that could no longer fail, and scoped invalidation code that existed but was never wired into the production loop.

One test hung for hours because it monkeypatched a compatibility shim while the CLI had already bound the real server module; pytest opened a browser and entered serve_forever(). A configuration value intended as milliseconds became 30,000 seconds, so a source could poll once every 8.3 hours. These were not exotic mathematical errors. They were ordinary integration mistakes in a codebase that had grown quickly, which is why contracts, timeouts, independent enumeration, and runtime clamping now sit beside the property tests.

The solver is the clearest version of the lesson: a fast plausible answer is useless here. The dependency on HiGHS is intentional, and its output is still replayed and certified rather than trusted blindly.

Current map

Where it is now

The core contracts, pricing model, and live pipeline are substantially more disciplined than the earlier engines, but the project is not being presented as finished code yet. The terminal-style frontend is now the production panel, on top of the same server-authoritative state. A live run once stalled in the merge phase at around 1,900 boards; boards with no new data are no longer re-evaluated, results are published incrementally, and the write-off finder works on a rotating slice, but time to first opportunity at full live scale has not been measured since those fixes. The automated browser suite and the overnight live soak that the project’s own acceptance gates require are still owed. Some reversed-handicap pooling remains deliberately isolated until its normalization can be proven correct.

The intended public scope remains narrow: inspect sources, find overlapping positions, and price them. It does not hold credentials for execution and it never places a bet.

Python 3.12 · bitmask outcome spaces · SciPy HiGHS · branch-and-bound · Hypothesis · WebSocket board

back to selected projects →