Skip to content

Add example: external signal feed polled by a BackgroundWorker (kalshi-predictit-arb) - #866

Open
mastertyrone wants to merge 12 commits into
betcode-org:masterfrom
mastertyrone:add-arbfeed-example
Open

mastertyrone wants to merge 12 commits into
betcode-org:masterfrom
mastertyrone:add-arbfeed-example

Conversation

@mastertyrone

@mastertyrone mastertyrone commented Sep 30, 2026 •

Copy link
Copy Markdown

Disclosure: the live feed used here, kalshi-predictit-arb, is a paid third-party API ($0.02 USDC per call, paid via x402 on Base). It is built and operated by Team Takatini, and I (mastertyrone) am affiliated with it. The example doesn't need the feed: it runs fully offline from labelled fictional sample data, needs no key, and places no orders. If a paid feed doesn't belong in your examples, feel free to close. No follow-up from me.

What the feed computes (server-side; nothing below is reimplemented in flumine):

  • Matches Kalshi markets to PredictIt contracts, gated on state, district, office, party, strike and cycle. A party mismatch is a hard reject.
  • Prices both arb directions with fee drag:
    • Kalshi taker fee is ceil(7·P·(1−P))¢ per leg.
    • PredictIt takes 10% of winning-leg profit, plus 5% on withdrawal.
  • Reports the worst-case net across settlement outcomes. executable=true only at ≥1¢ net.
  • Walks the Kalshi book to $100 / $500 / $1,000 notional.
  • Treat the gaps as indicative until you've checked them against live depth, fees and resolution equivalence.
  • Docs, with a live sample on the docs page: https://mastertyrone.github.io/kalshi-predictit-arb/
  • Standalone client: https://github.com/mastertyrone/kalshi-predictit-arb

Why it's here as a flumine example: it shows a pattern that works for any external signal feed, whatever the source:

  • examples/workers/arbfeed.py: poll_arb_feed runs in a BackgroundWorker and pushes results onto handler_queue as a CustomEvent. The callback runs on the main thread and writes signals into strategy.context. This is the same approach as workers/inplayservice.py.
  • examples/strategies/arbsignal.py: ArbSignalStrategy. check_market_book only returns True while a fresh executable signal meets context["min_net_yield_c"]. process_market_book logs each new signal. It places no orders.
  • examples/workers/arbscanner.py: a small client for the feed.
    • With no key (the default), it reads the sample fixture and makes no network calls.
    • Live mode only runs if a key is passed or X402_WALLET_KEY is set. It then:
      • refuses any payment request that isn't Base / exact / Base USDC / 0 < amount ≤ max_usd_per_call (default $0.02);
      • signs an EIP-3009 TransferWithAuthorization with eth-account's encode_typed_data;
      • sends it as unpadded base64url in PAYMENT-SIGNATURE, and retries once.
  • examples/example-arbfeed.py: a runnable script using Flumine + SimulatedClient.
  • examples/resources/kalshi_predictit_arb_sample.json: fictional data, labelled as sample in the file.
  • tests/test_example_arbfeed.py: 34 tests using unittest.mock, with no network calls. They cover:
    • the fixture path;
    • 402 → sign → retry (the header decodes to the x402 v2 structure, and the signature recovers to a throwaway Account.create() address);
    • that encode_typed_data produces the same signature as a hand-rolled EIP-712 hash;
    • refusals: wrong network, asset or scheme, over the cap, zero amount, empty accepts;
    • the default cap and tolerant header decoding;
    • the worker, the callback and the strategy gating.

Limits:

  • flumine has no Kalshi or PredictIt client, so this example only gates and logs. It doesn't trade either leg.
  • The offline demo has no market stream, so the demo only shows worker → CustomEvent → context. The check_market_book gating is covered by the unit tests. For real markets, swap in a BetfairClient and a stream, as in tennisexample.py.
  • eth-account is optional. It's only needed for live mode, and it isn't added to requirements.txt. Without it, 17 of the 30 new tests skip, which is what will happen in this repo's CI.

Size: 6 new files, +878 lines. No existing files are changed.

Checks (run locally):

  • Python 3.13: python -m unittest discover -s tests passes 1086 tests (1052 before).
  • Python 3.13: coverage run -m unittest discover passes.
  • Python 3.13: black==26.5.1 . --check is clean.
  • Python 3.10, without eth-account: the new tests run 30, skip 17, all OK.

Run:
cd examples && PYTHONPATH=.. python example-arbfeed.py # offline, sample data

Happy to drop the test file, trim the client, or move files if you'd prefer a different layout.

The feed labels rows with "event"; "pair" is the older name. The example strategy now keys and logs on event, falls back to pair, and skips rows that have neither, so no blank or "None" labels.

The offline sample fixture now uses the live row shape (event, kalshi, predictit, best_direction, stats, fetched_at) with fictional values and is still marked as sample data. Adds tests for event-first, pair fallback, rows with neither skipped, and the fixture shape.
The feed labels rows with "event"; "pair" is the older name. The example strategy now keys and logs on event, falls back to pair, and skips rows that have neither, so no blank or "None" labels.

The offline sample fixture now uses the live row shape (event, kalshi, predictit, best_direction, stats, fetched_at) with fictional values and is still marked as sample data. Adds tests for event-first, pair fallback, rows with neither skipped, and the fixture shape.
The feed labels rows with "event"; "pair" is the older name. The example strategy now keys and logs on event, falls back to pair, and skips rows that have neither, so no blank or "None" labels.

The offline sample fixture now uses the live row shape (event, kalshi, predictit, best_direction, stats, fetched_at) with fictional values and is still marked as sample data. Adds tests for event-first, pair fallback, rows with neither skipped, and the fixture shape.
The feed labels rows with "event"; "pair" is the older name. The example strategy now keys and logs on event, falls back to pair, and skips rows that have neither, so no blank or "None" labels.

The offline sample fixture now uses the live row shape (event, kalshi, predictit, best_direction, stats, fetched_at) with fictional values and is still marked as sample data. Adds tests for event-first, pair fallback, rows with neither skipped, and the fixture shape.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant