Kalshi FIFA Soccer Trader
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.
When to Use This Skill
Use when the user wants to:
- Trade Kalshi soccer match winner, total goals, or goal spread markets
- Use EA FC OVR ratings to find pricing edges against the market
- Automate position management across multiple upcoming soccer matches
- Backtest or dry-run the Poisson goal model against live Kalshi prices
Strategy
EA FC OVR disparity → bivariate Poisson → probability grid → de-vig vs Kalshi prices → EV-positive trades.
Three market types handled:
- Match winner — P(home win) / P(draw) / P(away win) vs the YES/NO price
- Total goals — P(total goals > line) vs the over market price
- Goal spread — P(home wins by ≥ N goals) vs the spread market price
Model in brief:
- Look up both teams' EA FC OVR in
ratings.json(top ~200 clubs + all national teams) - Convert OVR disparity → expected goals per team (λ_home, λ_away) via log-linear model with home advantage
- Build a 10×10 joint Poisson goal grid
- Sum grid cells to get probabilities for each market type
- De-vig the Kalshi YES/NO prices to implied probability
- Edge = model probability − implied probability
- Trade when edge > threshold and safeguards pass
Setup
-
Install the Simmer SDK
pip install simmer-sdk -
Set your Simmer API key
export SIMMER_API_KEY=... # simmer.markets/dashboard → SDK tab -
Set your Solana private key (live trading only)
export SOLANA_PRIVATE_KEY=<base58-encoded-secret-key> -
Complete KYC (required for buys on Kalshi)
- Verify at dflow.net/proof
- Check status:
curl "https://api.simmer.markets/api/proof/status?wallet=YOUR_SOLANA_ADDRESS"
-
Fund wallet
- SOL on Solana mainnet for gas (~0.05 SOL)
- USDC on Solana mainnet for capital
-
Import soccer markets (first run)
python soccer_trader.py --import-markets -
Dry run first — always
python soccer_trader.py
Configuration
| Setting | Env var | Default | Description |
|---------|---------|---------|-------------|
| Entry edge | SIMMER_SOCCER_ENTRY_EDGE | 0.06 | Min EV edge to trade (6%) |
| Exit threshold | SIMMER_SOCCER_EXIT_THRESHOLD | 0.90 | Sell when implied prob reaches this |
| Max position | SIMMER_SOCCER_MAX_POSITION_USD | 5.00 | Max USDC per trade |
| Max trades/run | SIMMER_SOCCER_MAX_TRADES_PER_RUN | 5 | Cap per scan cycle |
| Market types | SIMMER_SOCCER_MARKET_TYPES | winner,totals,spread | Which market types to trade (comma-sep) |
| Min liquidity | SIMMER_SOCCER_MIN_LIQUIDITY | 500 | Skip markets with liquidity below this USD |
| Slippage max | SIMMER_SOCCER_SLIPPAGE_MAX | 0.10 | Skip if estimated slippage > 10% |
| Smart sizing % | SIMMER_SOCCER_SIZING_PCT | 0.04 | % of balance per trade (with --smart-sizing) |
| Home advantage | SIMMER_SOCCER_HOME_BOOST | 0.12 | Log-scale home goal boost (0 = neutral venue) |
Quick Commands
# Status + open positions
python scripts/status.py
# Positions only
python scripts/status.py --positions
# Full P&L
python scripts/status.py --pnl
Running the Skill
# 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
How It Works
Each cycle:
- Fetch active soccer markets from Simmer API (filtered by
import_source=kalshi+ soccer keywords) - Parse team names and market type from each market's title/question
- Look up EA FC OVR for each team from
ratings.json(fuzzy match on team aliases) - Compute λ_home and λ_away from OVR disparity
- Build 10×10 Poisson goal grid
- Derive model probability for the market's YES outcome
- De-vig Kalshi YES/NO prices to implied fair probability
- Edge = model_prob − implied_prob
- Safeguards: check time-to-resolution, slippage, flip-flop, liquidity
- Buy YES if edge > entry threshold (or NO if negative edge is large enough)
- Tag every trade with
signal_data(OVRs, λs, model prob, edge, market type) for backtesting
Ratings
Team OVR ratings live in ratings.json. The skill uses a three-layer hierarchy to compute each team's effective OVR:
- Formation-aware (
lineup_intel.py) — picks the best XI for a given formation (default 4-3-3) by position group, then adjusts for confirmed absences fromlineup_cache.json top11_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:
| Field | Description |
|-------|-------------|
| ovr | Composite team OVR (bench included) |
| att / mid / def | Positional ratings |
| top11_avg_ovr | Average OVR of the best 11 players |
| players | List of {name, ovr, position} — used by lineup intel |
| aliases | Fuzzy-match list for Kalshi market title parsing |
Lineup Intelligence
lineup_intel.py adds formation-aware OVR and injury signal detection on top of the static ratings.
# 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). Populateslineup_cache.jsonwith 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).- Builds a formation XI (default 4-3-3) from
ratings.jsonplayer data by position group - Flags top-5 players not seen in the official squad as possible injury/exclusion
- Warns when lineup data is stale (> 90 days since last match)
- OVR is transparently adjusted in
lineup_intel.py— no changes needed in trading logic
Injury flag example:
❓ 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):
# 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:
{
"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.
Safeguards
- Time decay: Skip markets resolving in < 3 hours (spreads/winner) or < 1 hour (totals — goals can come late)
- Slippage: Skip if estimated market slippage > 10%
- Flip-flop: Skip if you've reversed direction on this market recently
- Liquidity: Skip if market liquidity < min threshold
- Both teams unknown: Skip if either team not found in ratings
Disable with --no-safeguards (not recommended).
Troubleshooting
"Team not found in ratings: [name]"
- The market title's team name didn't fuzzy-match any entry in
ratings.json - Add the team or alias: edit
ratings.jsonand add the name to itsaliaseslist
"No soccer markets found"
- Run
python soccer_trader.py --import-marketsto discover and import Kalshi soccer markets - Check that Kalshi has active soccer markets (tournament may be between matches)
"KYC verification required"
- Complete at dflow.net/proof
"SOLANA_PRIVATE_KEY not set"
- Only needed for
--live. Dry-run works without it.
"Insufficient SOL for transaction fees"
- Fund Solana wallet with at least 0.05 SOL
Kalshi maintenance window
- Kalshi's clearinghouse has weekly maintenance Thursdays 3:00–5:00 AM ET — trades during this window will fail
微信扫一扫