Reproduce a Paper Strategy Failure With a Small Fixture
A paper strategy died on tick 14,203 with a KeyError, or it sized a position to nonsense, or it threw on a market that had no ask. You want to fix it. What you do not want is to redeploy, wait for live data to drift back into the same shape, and hope the bug fires again. That is slow, non-deterministic, and it burns a hosted run for every guess.
The faster path is the one every test suite already uses: capture the exact input that broke, pin the exact code that processed it, and replay both offline until the failure is deterministic. In testing terms you are building a fixture. According to the pytest documentation, a fixture provides a defined, reliable and consistent context for the tests.
This post is about doing that for a prediction-market strategy callback. No credentials, no new hosted runs, no orders. Just a frozen input, a frozen version, and a local repro.
Why a fixture, and not another live run
A live rerun changes three things at once: the market data, the fill model, and your code if you edited it. When all three move, you cannot attribute the failure to any one of them. A fixture freezes the first two so you can isolate the third.
This is the retest-then-regression discipline. The reproduction test is expected to fail on the old code and pass on the fixed code, a fail-before, pass-after check. Once it passes, it joins your regression suite and keeps running on later changes so the same bug cannot silently return. The verification is only complete when the bug report names the fix commit and links the runs.
Concretely, a good repro gives you: deterministic failure on demand, a diff-able unit you can attach to an issue, and a guard that survives refactors.
Pin the exact version first
A fixture is worthless if it runs against a moving target. Pin the strategy SDK before anything else. The package on PyPI is bangertrades, and you should install an exact release rather than whatever is latest:
pip install bangertrades==0.1.7Record that version string in the issue and in the test file itself. If your repro passes on 0.1.7 but the deployment ran a different build, you have reproduced a different program. Reverify the pinned version against the package page before you trust a result, because the contract can change between releases.
Pin your strategy code the same way: the exact file, at the exact commit, that produced the failure. Reproducing against your current working copy, with uncommitted edits, is a common way to chase a ghost.
Capture a sanitized input
The strategy entry point you are reproducing is the per-tick callback. In 0.1.7 the signature is on_market_tick(self, market, tick, ctx), with an optional on_start(ctx) for setup. Your fixture needs to recreate the arguments that version of the callback saw when it failed.
The tick is the payload to capture carefully, because its field types are where calculation bugs hide:
- Prices are Decimal, not float. A repro that feeds float prices can hide or invent rounding and comparison bugs.
- bid and ask can be absent. Missing quotes are a real state, not an error to paper over. If your bug was a crash on a one-sided book, the fixture must preserve the missing side exactly.
- timestamp is a datetime. Keep it as the real value if any time-based logic depends on it.
Sanitize, do not mutate. Strip anything account-identifying or secret from the captured record, but do not round prices, fill in a missing ask with a guess, or normalize a type. The whole point is that the bug lived in the exact shape of the input. If you change the shape, you are testing a different input.
Capture the exact market ticker too, and write it down as a literal. Do not rebuild the fixture by taking the first result off a discovery call and assuming it is the same market. The first discovery result drifts; a hard-coded ticker does not.
Build the offline repro
Now instantiate your strategy and call the callback directly with the reconstructed arguments. This runs entirely in-process. It touches no venue, needs no keys, and creates no paper record. It is a plain function call wrapped in a test.
Structure it as a fail-before, pass-after test. Assert the behavior you actually want: that a Decimal comparison lands correctly, that a missing ask routes to your fallback branch instead of raising, that your size calculation returns what you expect. Run it against the buggy code and confirm it fails for the right reason. Then fix the code and confirm the same test passes. Keep the fixture file checked in next to the test so the repro is self-contained and shareable.
If part of your logic depends on an SDK interface whose exact contract you cannot verify, do not guess at it inside the repro. Isolate and test the pure calculation you can verify, and leave the uncertain integration boundary out of the fixture rather than inventing a method signature that was never shipped.
A passing fixture is not an execution result
When the fixture goes green, be precise about what you proved. You proved that a specific version of your callback handles one specific captured input the way you expect. That is a unit result. It is not evidence about fills, fees, queue position, or venue behavior.
Keep these three things separate in your head and in your write-up:
- Fixture pass: your code processes a frozen input correctly, offline, deterministically.
- Paper result: Banger's forward-paper runtime simulated fills against live market data under a specific data, fill, fee, and valuation model. It is evidence under that model, not proof of real execution.
- Live execution acceptance: a real order filled at a real venue. The public beta does not offer this; live execution is not available yet.
Banger's forward-paper simulator makes modeling choices you should not confuse with reality. It can fill a non-crossing resting limit at that limit and logs paper.fill.resting-assumed when it does. Its volume cap is drawn from reported volume, not full book depth or queue reconstruction. Its standard Kalshi fee helper rounds up to a whole cent, which is not identical to the finer rounding in the venue's current fee documentation. A fixture that reproduces a calculation bug says nothing about any of these. If your failure was in the simulated fill or fee math, inspect the actual paper result model instead of asserting a number your fixture never touched.
The workflow in one pass
- Pin the SDK to an exact release and record it. Pin your strategy to the exact commit that failed.
- Capture the market, tick, and context the failing callback saw. Preserve Decimal prices, absent bid/ask, and the real datetime.
- Sanitize secrets only. Do not round, fill, or retype.
- Call on_market_tick directly in a test, assert the behavior you want, confirm it fails on the old code.
- Fix, confirm pass, commit the fixture and test, and link the commit and runs in the issue.
- Report the result as a unit repro, not as a paper or live execution outcome.
The reason to do it this way is leverage. One captured tick plus one pinned version turns a flaky, run-burning bug into a five-second local test you can run on every change. That is cheaper than a redeploy, and far more honest about what it proves.