Trade soccer markets on Kalshi using EA FC OVR rating disparity as the signal, executed via DFlow on Solana.
🚨 Framework, not a production trading system. Read DISCLAIMER.md before connecting a wallet with real funds.
This is a template. The default signal is EA FC OVR disparity fed through a bivariate Poisson goal model. Remix it: swap in Elo ratings, add in-play context, or filter by tournament. The skill handles market discovery, model scoring, de-vigging, and trade execution. Your alpha is the input.
Defaults: dry-run mode, max $5 USDC per trade, minimum 6% EV edge required. Pass
--liveto enable real trades; setSIMMER_SOCCER_MAX_POSITION_USDandSIMMER_SOCCER_ENTRY_EDGEto adjust.
Powered by DFlow. Kalshi trades execute via DFlow's Solana-based prediction market infrastructure. KYC through Proof is required for buys.
Use when the user wants to:
EA FC OVR disparity → bivariate Poisson → probability grid → de-vig vs Kalshi prices → EV-positive trades.
Three market types handled:
Model in brief:
ratings.json (top ~200 clubs + all national teams)Install the Simmer SDK bash
pip install simmer-sdk
Set your Simmer API key bash
export SIMMER_API_KEY=... # simmer.markets/dashboard → SDK tab
Set your Solana private key (live trading only) bash
export SOLANA_PRIVATE_KEY=<base58-encoded-secret-key>
Complete KYC (required for buys on Kalshi)
curl "https://api.simmer.markets/api/proof/status?wallet=YOUR_SOLANA_ADDRESS"Fund wallet
Import soccer markets (first run) bash
python soccer_trader.py --import-markets
Dry run first — always bash
python soccer_trader.py
SettingEnv varDefaultDescriptionEntry edgeSIMMER_SOCCER_ENTRY_EDGE0.06Min EV edge to trade (6%)Exit thresholdSIMMER_SOCCER_EXIT_THRESHOLD0.90Sell when implied prob reaches thisMax positionSIMMER_SOCCER_MAX_POSITION_USD5.00Max USDC per tradeMax trades/runSIMMER_SOCCER_MAX_TRADES_PER_RUN5Cap per scan cycleMarket typesSIMMER_SOCCER_MARKET_TYPESwinner,totals,spreadWhich market types to trade (comma-sep)Min liquiditySIMMER_SOCCER_MIN_LIQUIDITY500Skip markets with liquidity below this USDSlippage maxSIMMER_SOCCER_SLIPPAGE_MAX0.10Skip if estimated slippage > 10%Smart sizing %SIMMER_SOCCER_SIZING_PCT0.04% of balance per trade (with --smart-sizing)Home advantageSIMMER_SOCCER_HOME_BOOST0.12Log-scale home goal boost (0 = neutral venue)
bash
# Status + open positions
python scripts/status.py
# Positions only
python scripts/status.py --positions
# Full P&L
python scripts/status.py --pnl
bash
# Dry run (default) — shows edge but no trades
python soccer_trader.py
# Live trading
python soccer_trader.py --live
# Smart position sizing from portfolio balance
python soccer_trader.py --live --smart-sizing
# Only trade match winner markets
python soccer_trader.py --live --market-types winner
# Only totals and spread
python soccer_trader.py --live --market-types totals,spread
# Import new Kalshi soccer markets
python soccer_trader.py --import-markets
# Show current model config and ratings loaded
python soccer_trader.py --config
# Quiet — only print on trades/errors (ideal for cron)
python soccer_trader.py --live --smart-sizing --quiet
# Manage open positions (exit anything above threshold)
python soccer_trader.py --live --manage-positions
Each cycle:
import_source=kalshi + soccer keywords)ratings.json (fuzzy match on team aliases)signal_data (OVRs, λs, model prob, edge, market type) for backtestingTeam OVR ratings live in ratings.json. The skill uses a three-layer hierarchy to compute each team's effective OVR:
lineup_intel.py) — picks the best XI for a given formation (default 4-3-3) by position group, then adjusts for confirmed absences from lineup_cache.jsontop11_avg_ovr — average OVR of the best 11 players (fallback if no player data)ovr — composite team OVR including bench (last resort)Fields per team:
FieldDescriptionovrComposite team OVR (bench included)att / mid / defPositional ratingstop11_avg_ovrAverage OVR of the best 11 playersplayersList of {name, ovr, position} — used by lineup intelaliasesFuzzy-match list for Kalshi market title parsing
lineup_intel.py adds formation-aware OVR and injury signal detection on top of the static ratings.
bash
# Step 1 (run once, or to refresh): load official WC 2026 squads from Wikipedia
python3 scripts/fetch_wc_squads.py # all 48 WC teams
python3 scripts/fetch_wc_squads.py Spain France # specific teams only
python3 scripts/fetch_wc_squads.py --dry-run Spain # preview without writing
# Step 2 (optional): augment with recent match lineup data from TheSportsDB
python3 scripts/fetch_lineups.py Sweden Tunisia
python3 scripts/fetch_lineups.py --all # all teams in ratings.json
python3 scripts/fetch_lineups.py --demo Spain "Cape Verde" # prediction demo
# Cached output: lineup_cache.json
What it does:
fetch_wc_squads.py — fetches all 48 official WC 2026 squad lists from Wikipedia (no API key, no rate limits). Populates lineup_cache.json with the full 26-man roster per team. Run this first so injury signals only fire for players genuinely excluded from the squad.fetch_lineups.py — pulls last 5 matches per team from TheSportsDB and merges confirmed match starters into the existing squad cache (Wikipedia squad data is preserved, not replaced).ratings.json player data by position grouplineup_intel.py — no changes needed in trading logicInjury flag example:
text
❓ V. Gyöker... (OVR 82) — not confirmed in last 1 lineup(s); possible injury/rest
⚠ Most recent data is 174d old (Tunisia 3-1 Uganda)
Recommended workflow: run
fetch_wc_squads.pyonce before a tournament to seed accurate squad data, thenfetch_lineups.pybefore individual matches for recent form signals. TheSportsDB free tier returns only ~3 starters per match — use it as a supplement to the Wikipedia squad, not a replacement.
Refresh from SoFIFA (Chrome must be closed due to profile lock):
bash
# 1. Quit Chrome (Cmd+Q on macOS)
# 2. Scrape EA FC 26 ratings for all 48 WC 2026 teams
node scripts/scrape_sofifa.mjs
# 3. Merge scraped data into ratings.json
python3 scripts/merge_sofifa.py
# 4. Re-open Chrome
Add a missing team manually:
json
{
"name": "Team Name",
"ovr": 83,
"att": 83, "mid": 82, "def": 81,
"top11_avg_ovr": 84,
"aliases": ["alternate name", "abbreviation", "country code"]
}
If a team can't be matched, the market is skipped with a warning. Add the team name (or an alias) to ratings.json to include it.
Disable with --no-safeguards (not recommended).
"Team not found in ratings: [name]"
ratings.jsonratings.json and add the name to its aliases list"No soccer markets found"
python soccer_trader.py --import-markets to discover and import Kalshi soccer markets"KYC verification required"
"SOLANA_PRIVATE_KEY not set"
--live. Dry-run works without it."Insufficient SOL for transaction fees"
Kalshi maintenance window