← Plugin catalog
Data & Analytics

StrikerAPI

StrikerAPI v1.0.0

Soccer data and stats

Create a free account to save this plugin.

Competitor profile

Developer declarations; not independently tested. Listing last checked Nov 4, 2026. Package observed Nov 3, 2026.

Declared capabilities

  • Look up clubs, players and matches across many leagues
  • Read league tables, schedules and results
  • Read lineups, match events and shot maps

Cost & access

StrikerAPI does not sell anything through ChatGPT. The plugin is read-only: it has no checkout, purchase, subscription or credit top-up flow.

Declared limitations · 3
  • Place a wager: wagering is not supported
  • Out of coverage: another sport
  • Account and billing change: not supported
Onboarding example
Show the 2025-26 Premier League table.

View package source →

Description

From the marketplace listing

StrikerAPI connects ChatGPT and Codex to soccer data from StrikerAPI. It covers the Premier League, MLS, La Liga, Serie A, Bundesliga, Ligue 1, the UEFA competitions and other leagues, with lineups, match events, shot maps and expected goals. Ask for league tables, schedules and results, lineups, match events, player stats and leaders, team ratings, head-to-head history, shot maps with expected goals, sportsbook odds and player prop lines. The plugin looks the answer up with read-only tools and reports what the data says, including the season, date and source of each number. The first time you use it, you sign in with your StrikerAPI account. Limits: StrikerAPI is read-only. It cannot place wagers, accept or track bets, or edit your account. Odds and prop lines are shown as information about the market, not as advice. Data comes from public sources and the StrikerAPI pipeline and is not guaranteed to be error-free or current to the minute, so do not rely on it as the sole basis for financial decisions. Ratings shown are StrikerAPI's own Elo-style ratings, not official rankings. Not affiliated with any league or club. Bundled skills teach the model how to choose the right tool for a question and how to explain results.

Language: English · Automatically detected from descriptions.

Changes

No changes recorded yet. Follow this plugin to keep track of future changes.

First seen Oct 30, 2026. Last seen Nov 4, 2026.

Keywords

Files & skills

File archives

Plugin package19 files · 91.1 KBBrowse files →
Skill instructions
dispatch6.23 KB

View saved version →

---
name: dispatch
description: "Routes a soccer data or analytics request to the right 2-3 skills. Load this FIRST when a request comes in and you are unsure which of the 16 skills to activate. Prevents loading every skill when only two or three are needed. Do not use when the request already names one skill or one tool."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Skill Dispatch Guide

Match the request to a pattern below, load those skills, and stop. Always load `strikerapi-tool` alongside any skill that needs data; it owns tool names, credit costs and the `{"data": ...}` envelope.

## Persona detection

| Signal | Persona | Start with |
|--------|---------|-----------|
| "What is on this weekend?" / exploring | Explorer | `match-lookup`, `odds-explorer` |
| "Build me a model" / "predict scores" | Builder | `poisson-score-models`, `walk-forward-validation` |
| "Who is scoring?" / "scout this player" | Scout | `player-scouting`, `xg-analysis` |
| "Is my model any good?" | Validator | `walk-forward-validation`, `probability-calibration` |
| "I am new to soccer analytics" | Learner | `xg-analysis`, `elo-ratings` |

## Request patterns

### "What matches are on?" / "Results from last weekend" / "Show me the lineup"
`match-lookup` (list_games, get_schedule, get_game, get_lineups)

### "How is Arsenal doing?" / "Table" / "Home and away form"
`team-analysis` (get_standings, get_team_stats, get_head_to_head)

### "Tell me about Saka" / "Top scorers" / "Compare two forwards"
`player-scouting` (search_players, get_player, get_leaders)

### "What are the odds?" / "Best price on the draw" / "How did the line move?"
`odds-explorer`. If the question turns to true probabilities or margin, add `odds-analysis`.

### "Remove the vig" / "Implied probability" / "Shin" / "Fair odds for the draw"
`odds-analysis`

### "Preview Liverpool vs City"
`match-preview` (pulls form, xG, Elo, head to head and the line). Add `xg-analysis` for shot detail.

### "Shot map" / "xG table" / "Who is overperforming their xG?"
`xg-analysis`

### "Rate the teams" / "Elo" / "Who is strongest?"
`elo-ratings`

### "Predict the score" / "Poisson" / "Dixon-Coles" / "Over 2.5 probability"
`poisson-score-models`. Add `elo-ratings` if ratings are the input, `probability-calibration` before trusting the output.

### "Will Haaland score?" / "Anytime goalscorer" / "Price a scorer"
`goalscorer-prop-modeling`, with `player-scouting` for the player and `xg-analysis` for shot volume. Only seven competitions have prices.

### "Is my model calibrated?" / "Brier" / "Ranked probability score"
`probability-calibration`

### "Is cross-validation okay?" / "Did I leak the future?"
`walk-forward-validation`

### "Backtest this" / "Closing line value" / "Would this have made money?"

### "Which tool do I call?" / "What does this cost?" / "REST or MCP?"
`strikerapi-tool` only.

## Credit warning gates

Ask the user before any plan that costs more than about 200 credits. Common big ones: a season of `get_odds` (2 credits per match, 760 for a 380-match league), `get_game` in a loop (5 each), `get_events` in a loop (10 each), and `get_prop_history` for many players (10 each).

## When NOT to use this guide

Single-skill requests do not need routing. "Devig +290 / +250 / -118" goes straight to `odds-analysis`.

## What every answer should do

1. Say which competition and season the numbers cover. The default is the Premier League and the latest season.
2. State the credits spent.
3. Treat any edge as an estimate. Warn about variance, small samples and closing-line value, and never promise returns.

## The 16 skills at a glance

| Skill | Use it for |
|-------|-----------|
| `strikerapi-tool` | Tool names, credits, REST and MCP access, the `{"data": ...}` envelope |
| `match-lookup` | Fixtures, results, box scores, events, lineups |
| `team-analysis` | Table, form, xG for and against, head to head |
| `player-scouting` | Player ids, per-90 rates, leaders, match logs |
| `odds-explorer` | Three-way prices, best book, margin, line movement |
| `match-preview` | One fixture end to end |
| `xg-analysis` | Shot maps, xG tables, expected points, finishing |
| `elo-ratings` | Reading and building Elo ratings |
| `poisson-score-models` | Poisson and Dixon-Coles score models |
| `odds-analysis` | Odds formats and three-way devigging |
| `walk-forward-validation` | Leak-free evaluation |
| `probability-calibration` | Reliability, RPS, scaling |
| `goalscorer-prop-modeling` | Anytime goalscorer probabilities and prices |
| `dispatch` | This guide |

## Common recipes

| Goal | Skills in order | Rough credits |
|------|-----------------|---------------|
| Preview one match | `match-preview` (loads `odds-explorer` and `elo-ratings` pieces) | 20 |
| Is this price fair? | `odds-explorer`, `odds-analysis` | 5 |
| Scout a striker | `player-scouting`, `xg-analysis` | 20 to 30 |
| Price an anytime scorer | `goalscorer-prop-modeling`, `player-scouting` | 30 to 40 |
| Build an xG table | `xg-analysis` | 5 |

## Disambiguation

| The user says | They probably want | Not |
|---------------|-------------------|-----|
| "odds" | `odds-explorer` for prices, `odds-analysis` for probabilities | Both at once |
| "probability" of a result | `elo-ratings` or `poisson-score-models` for a model, `odds-analysis` for the market | A guess |
| "form" | `team-analysis` (the `form` letters, xG trend) | `player-scouting` |
| "rating" | `elo-ratings` | A player rating, which is not provided |
| "expected goals" | `xg-analysis` | `poisson-score-models` unless they want predictions |
| "prop" | `goalscorer-prop-modeling` (the only prop) | Shots, cards, assists (not available) |
| "predict" | `poisson-score-models` | `match-preview` (no forecast) |
| "model is bad" | `probability-calibration` first, then `walk-forward-validation` | New features |

## Out of scope

StrikerAPI has no injury, transfer, in-play, live-score, or non-goalscorer player-prop data. If the request needs them, say so and offer the nearest alternative (for example `get_lineups` for confirmed starters, `get_line_movement` for pre-kickoff price changes).

## Credit budget

The typical monthly allowance is 500 credits. A single-match preview is about 20 credits and a season xG table is 5, so most exploration fits. A full season of closing prices (760 credits) does not; plan around one club.
elo-ratings8.18 KB

View saved version →

---
name: elo-ratings
description: "Reads and builds soccer Elo ratings: pulls StrikerAPI's club ratings, converts a rating gap and home bonus into expected score and win/draw/loss probabilities, and builds a custom Elo from match results with tuned K, home advantage, goal-difference scaling and season carryover. Use when the user asks who is strongest, wants ratings as of a date, wants win probabilities from ratings, or wants to build or tune an Elo system. Do not use for score-distribution models, see poisson-score-models. Do not use for league tables, see team-analysis."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Elo Ratings

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `get_ratings` (2 credits) returns the ratings; `list_games` (2 credits per call of up to 200 matches) supplies results for a custom build.

Elo summarizes a club's strength as one number updated after every result. It is a good baseline: stable, cheap, and hard to beat by much with simple models. It does not know about lineups, injuries, rest, or shot quality.

## When to Use

- "Who is the strongest team in the league?"
- "What did the ratings look like on 1 January?"
- "Turn this rating gap into win probabilities"
- Building or tuning a custom Elo for a league or for a model feature

## When NOT to Use

- Full scoreline distribution, totals, handicaps: `poisson-score-models`
- Current table and form: `team-analysis`
- Calibration of whatever you build: `probability-calibration`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_ratings` | Elo ratings, strongest first. `date` for as-of ratings, `limit` for count (default 50, max 200), `league` | 2 |
| `list_games` | Results for a custom build. `season`, `from`, `to`, `status`, `limit` up to 200, `offset` | 2 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_elo`, `get_team_rating` | `get_ratings` and pick the club's row |
| `get_rating_history` | Call `get_ratings` with different `date` values, or rebuild from `list_games` |
| `get_power_rankings` | `get_ratings` |
| Cross-league ratings | Not available. A rating moves only on its own competition's matches |

## The StrikerAPI Rating

The `get_ratings` response states its method: Elo from every final match in the competition, K of 20, a home advantage of 60 points (none at neutral venues), scaled by goal difference, and a third of the way back toward 1500 between seasons. A 400-point gap means 10-to-1 odds, and a 100-point gap is about a 64% expected score for the stronger side at a neutral venue.

```
get_ratings {"limit": 4}
```

```
as_of 2026-10-10
1 MNC Manchester City  1670.4   last game 2026-09-20
2 ARS Arsenal          1663.0   last game 2026-10-10
3 BHA Brighton         1586.5   last game 2026-10-10
4 LIV Liverpool        1578.0   last game 2026-09-20
```

Pass `league` for another competition. In MLS the same call, `get_ratings {"league": "mls", "limit": 2}`, returned Nashville SC (1651.7) and Inter Miami (1609.5) as of 2026-10-11. Those numbers are not comparable to the Premier League's: each competition has its own pool and its own 1500 anchor.

## Expected Score and Probabilities

Elo's expected score for the home side:

```
E_home = 1 / (1 + 10 ** (-(R_home - R_away + H) / 400))        H = 60 at a home venue, 0 if neutral
E_away = 1 - E_home
```

Expected score counts a draw as half a win. That is not a win probability. To split it into home, draw and away you need a draw model:

```
# Davidson model on the Elo scale
x = ln(10) * (R_home - R_away + H) / 400
p_home = exp( x / 2) / (exp(x / 2) + exp(-x / 2) + nu)
p_away = exp(-x / 2) / (exp(x / 2) + exp(-x / 2) + nu)
p_draw = nu            / (exp(x / 2) + exp(-x / 2) + nu)
```

`nu` is one number that sets how often level teams draw. Fit it by maximum likelihood on past results for your league, then check calibration with `probability-calibration`. Draw rates differ by league and era, so refit per competition. The model keeps the ordering Elo implies and draws peak when the teams are level.

Worked example, using the `get_ratings` snapshot above for a hypothetical Manchester City visit to Liverpool: the gap is `1670.4 - 1578.0 = 92.4` in City's favor, minus Liverpool's 60-point home bonus leaves 32.4. `E_city = 1 / (1 + 10 ** (-32.4 / 400)) = 0.546`.

## How It Works: Building Your Own

Use this when you need ratings as a feature with a known time stamp, a different league mix, or a tuned K.

**Step 1: Collect results (cheap).**

```python
games = []
for season in range(2015, 2026):
    for offset in (0, 200):
        page = call("list_games", season=season, status="final", limit=200, offset=offset)
        games += page["games"]
```

Each season is about two calls (4 credits) for 380 matches. Sort by `starts_at`.

**Step 2: Update loop.**

```python
def update(ratings, home, away, hg, ag, k=20, home_adv=60):
    rh, ra = ratings.get(home, 1500), ratings.get(away, 1500)
    exp_h = 1 / (1 + 10 ** (-(rh - ra + home_adv) / 400))
    actual_h = 1.0 if hg > ag else 0.5 if hg == ag else 0.0
    gd = abs(hg - ag)
    mult = 1.0 if gd <= 1 else 1.5 if gd == 2 else (11 + gd) / 8     # goal-difference multiplier
    delta = k * mult * (actual_h - exp_h)
    ratings[home], ratings[away] = rh + delta, ra - delta
```

**Step 3: Season break.** Pull each rating a fraction of the way back to 1500 (or the league mean) before the next season, for example `r = 1500 + (1 - c) * (r - 1500)` with `c` around 0.33. Promoted clubs start below the mean (try 1400 to 1450) or inherit their promotion-playoff level; new clubs are the biggest source of error in the first dozen matches.

**Step 4: Tune.** Grid over `k` (10-40), `home_adv` (40-90), `c` (0.2-0.5) and the multiplier. Score each setting with the ranked probability score on a walk-forward basis (see `walk-forward-validation`), never on the data the ratings were fit to. Pick the simplest setting within noise of the best.

**Step 5: Point-in-time rule.** The rating used for a match must be the one before it. In a backtest, set the feature to the rating before the update, then update. `get_ratings` with `date` helps, but confirm whether a rating dated the match day includes that match by checking `last_game`; use the day before kickoff to be safe.

## Pitfalls

1. **Not comparable across competitions.** Ratings in the Premier League and the Championship are separate pools. Do not subtract them. Champions League ratings are their own pool again.
2. **Draws are not wins.** Expected score is not a probability of winning. Always say which one you are quoting.
3. **Neutral venues.** Cup finals and some Champions League ties have no home advantage; the API applies none, and so should you.
4. **Early season.** Ratings regressed toward the mean still carry last season's squad. Summer transfers do not show up until results do.
5. **Lag.** A rating moves only on results. A club that lost its best player yesterday still has yesterday's rating.
6. **Do not over-tune K.** Differences between K of 18 and 24 are within noise on one league's history.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "A 100-point gap means 64% to win" | 64% is expected score, with draws as half | Split with a draw model |
| "Subtract an MLS and a Premier League rating" | Separate pools | Compare within one competition |
| "Fit K on all the data, then report accuracy" | Leaks the test set | Walk-forward tuning |
| "Ratings as of the match date are safe" | May include that match | Use the day before |
| "Elo explains the market" | The market knows lineups and news | Treat Elo as a baseline to beat |

## Output Format

```
[Competition] Elo as of [date]
Rank  Club  Rating  Last game
Gap [club A] vs [club B]: [x] points, home bonus [60] -> expected score [x.xx]
Win / draw / loss (draw model fit on [n] matches): [p_h] / [p_d] / [p_a]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Need a scoreline or totals | Rating as an input to a goals model | `poisson-score-models` |
| Probabilities in hand | Check them against history | `probability-calibration` |
| Want to compare with the market | Remove the margin first | `odds-analysis` |
goalscorer-prop-modeling11.5 KB

View saved version →

---
name: goalscorer-prop-modeling
description: "Models anytime goalscorer probabilities for soccer players and compares them with sportsbook prices: expected minutes and start probability, non-penalty xG per 90 with shrinkage, penalty duty, team goal expectation from the match market, one-sided price handling, voids, and backtests with get_prop_history. Use when the user asks who will score, whether a goalscorer price is fair, or how to build a scorer model. Available for the Premier League, MLS, La Liga, Serie A, Bundesliga, Ligue 1 and Champions League only. Do not use for team totals, see poisson-score-models."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Goalscorer Prop Modeling

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `get_player_props` (10 credits, or 0 with `estimate_only`) for the prices. `get_prop_history` (10 credits for up to 200 games) for one player's prices against results. `get_shot_map` (10) with `player` for shot quality. `get_player` (5) and `get_player_games` (5) for minutes and rates. `get_lineups` (5) for starters.

The anytime goalscorer market asks one question: will this player score at least once? It is the only player prop StrikerAPI carries. Prices are a Yes price per player, with no line. The structure of the market (one-sided, margin-heavy, voided when a player does not play) drives most of the method.

## Coverage

Anytime goalscorer prices exist for seven competitions: the Premier League (`soccer`), MLS (`mls`), La Liga (`laliga`), Serie A (`seriea`), the Bundesliga (`bundesliga`), Ligue 1 (`ligue1`) and the Champions League (`ucl`). Per the StrikerAPI README, the Premier League and MLS start in April 2024, Ligue 1 in May 2024, La Liga, Serie A and the Bundesliga in August 2024, and the Champions League in March 2025. DraftKings and FanDuel cover most matches, with Pinnacle and William Hill on some matches in 2026. There are no prices for the other ten competitions; other player markets (shots, assists, cards) do not exist.

## When to Use

- "What is the chance Haaland scores this weekend?"
- "Is +250 fair for this winger?"
- "Rank the likeliest scorers for a match"
- "How often did he score when priced under even money?"

## When NOT to Use

- Match results, totals, handicaps: `poisson-score-models`
- Player profiles without prices: `player-scouting`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_player_props` | Anytime goalscorer prices. `game_id` for every player in a game; or `player_id` with `from_date` (and `to_date`) for one player across games. `bookmaker`, `snapshot_type` (`closing` default or `opening`), `market`, `estimate_only` | 10 (game); 10 per game returned for a player |
| `get_prop_history` | One row per game: closing price (median across books, or one `bookmaker`), opening line, whether he scored, `result`, over rate. `from_date`, `to_date`, `limit` up to 200, `next_from_date` to continue | 10 for up to 200 games |
| `get_shot_map` | The player's shots with `xg`, `penalty`, `zone`, `body_part` | 10 |
| `get_player` | Per-season minutes, goals, xG, per-90 rates | 5 |
| `get_player_games` | Match log with minutes on and off | 5 |
| `get_lineups` | Starting XI and bench for a match | 5 |
| `search_players` | Player id | 1 |

For one player over many games, `get_prop_history` (10 credits for up to 200 games) is far cheaper than `get_player_props` with `player_id` (10 credits per game).

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_player_props` for shots, assists, cards, first scorer | Not available; only `player_goal_scorer_anytime` exists |
| `get_goalscorer_odds` | `get_player_props` |
| `get_penalty_takers` | Count `penalty: true` shots in `get_shot_map` with `player` |
| `get_expected_lineup`, `get_injuries` | Not available. `get_lineups` once teams are named |

## What a Price Row Looks Like

From `get_player_props {"game_id": "740968"}` (closing, Premier League, 34 rows for 17 players across two books):

```
player             book        over_odds  under_odds
Gabriel Martinelli draftkings  250        null
Gabriel Martinelli fanduel     250        null
Daniel Munoz       draftkings  550        null
Daniel Munoz       fanduel     650        null
Christian Norgaard draftkings  900        null
```

`over_odds` is the Yes price, an American price. `under_odds` is the No price where a book posts it; in this match it was `null` for every row. `line` is `null` because the market has no line. `source` was `the_odds_api`.

`get_prop_history` rows add `actual` (goals), `result` and `books`. For Haaland (`player_id` 253989) from April 2024, five rows:

```
game_date   over_odds  actual  result   books
2024-04-28    +300       1     over       1
2024-05-04    -260       4     over       2
2024-05-11    -195       0     under      2
2024-05-14    -185       2     over       2
2024-05-19    -290       0     under      2
summary: games 5, graded 5, overs 3, unders 2, over_rate 0.6
```

`over` means he scored, `under` means he played and did not, and a `null` result is a void: he was not in the squad or did not play. Leave voids out of hit rates, and treat the book's own rules on voids as the source of truth for settlement.

## The Model

**Step 1: Scoring rate.** Goals follow a Poisson process well enough that:

```
P(scores at least once) = 1 - exp(-lambda)
lambda = expected goals the player scores in the match (open play plus penalties)
```

**Step 2: Build lambda.**

```
lambda = lambda_open_play + lambda_penalty
lambda_open_play = npxg90_shrunk * (expected_minutes / 90) * team_factor
lambda_penalty   = P(team awarded a penalty) * P(he takes it) * penalty_conversion
```

- `npxg90_shrunk`: non-penalty xG per 90. Take shots from `get_shot_map` with `player`, drop `penalty: true`, sum `xg`, divide by his minutes. Shrink toward his position's average with a prior of about 900 minutes: `(xg + prior_rate * 10) / (minutes / 90 + 10)`. A forward on 400 minutes should be mostly the prior.
- `expected_minutes`: `start_prob * minutes_if_start + (1 - start_prob) * minutes_if_sub`, from `get_player_games` (`minutes on and off`). Use `get_lineups` for `start_prob` once teams are named; otherwise estimate from his share of starts.
- `team_factor`: the club's expected goals in this match divided by its average. Get the club's expected goals from `poisson-score-models`, or back them out of the devigged three-way price and total from `odds-analysis` and `get_odds`. A club with a team total of 2.1 and a season average of 1.5 gets 1.4.
- The penalty term: count his penalties and attempts across seasons from `get_shot_map` (`penalty: true`) and `get_events`; take the league's penalties per match and the conversion rate from the same data, rather than a figure from memory.

**Step 3: Compare with the market.** Convert `over_odds` to an implied probability. Because the market is one-sided, you cannot devig it the usual way.

## One-Sided Prices

The Yes price has a margin built in and, with no No price, there is nothing to normalize against. Options, best first:

1. **Two-sided book.** If `under_odds` is present (where a book posts a No price), devig the pair with the two-way method in `odds-analysis`.
2. **Empirical scale.** Over many matches, compare mean implied probability with the actual scoring rate for similar players. `get_prop_history` gives both in one call. `c = hit_rate / mean_implied`, then `p_market = c * implied`. Five games is not enough; use a hundred or more across players.
3. **Sum check.** The fair probabilities for all players in a team should add up to roughly the expected number of distinct scorers (a bit below the team's expected goals). The match above listed 17 players, and the raw implied probabilities summed to 2.21 at DraftKings and 2.53 at FanDuel, for a match whose total was about 2.75 goals. The lists do not cover every player, so the sums are not a margin estimate. Use this check only when a book prices the whole squad.

## Worked Example

Haaland, Premier League, from `get_player {"player_id": "253989"}`:

```
season 2025: 35 appearances, 2959 minutes, 27 goals, 23.12 xG, xg_per90 0.70
```

A rough baseline for a full 90: `lambda = 0.70`, so `P = 1 - exp(-0.70) = 50.3%`. At his average of 84.5 minutes per appearance, `lambda = 0.70 * 84.5 / 90 = 0.657`, `P = 48.2%`. (Check whether penalties are in that xG: `get_shot_map` with `player` marks them with `penalty: true`. Remove them for the open-play term and add the penalty term back.)

Now compare with the market. In the five-row history above the median closing price implied `0.25, 0.722, 0.661, 0.649, 0.744`, an average of 60.5%, and he scored in 3 of 5 (60%). The market was usually well above 48%. Two reasons, both worth testing: those matches were against weaker opposition (`team_factor` above 1), and the mean implied probability includes the margin. The right response is not to assume the market is wrong; build `team_factor`, then check whether the model's log loss beats the market's over 100+ priced games.

Five games demonstrate the layout of the data, not the quality of a model.

## Backtest

1. For a set of regular goalscorers, `get_prop_history` with `limit` 200 (10 credits each). Cache.
2. For each game, your model probability (using only information available before kickoff) and the market's closing implied probability.
3. Drop voids. Compute log loss and calibration for both; the market is the benchmark.
5. Report coverage: games priced, books, and the range of dates.

## Pitfalls

1. **Own goals do not count** for the player. Confirm the book's rules; penalties do count.
2. **Voids.** `result: null` is a void, not a loss. Excluding voids from the model's training minutes biases rates up.
3. **Starters versus subs.** A starter's scoring chance is much higher than a substitute's; the price already knows who starts. Use `get_lineups` before comparing.
5. **Margin.** One-sided prices carry large margins. An edge you cannot see after scaling is not an edge.
6. **Small samples.** A rate built on fewer than 600 minutes is mostly the prior. A scorer's hit rate over 20 games has a standard error near 11 points.
7. **Price source.** Closing is the last price before kickoff and opening is the first captured; prices can move when lineups are released.
8. **Penalty taker changes** move a striker's price a lot. Check who took the last penalties.
9. **No promises.** Treat any edge as an estimate with a wide error band.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "xG per 90 of 0.7 means 70% to score" | `1 - exp(-lambda)` is about 50% | Convert the rate properly |
| "Normalize the prices to sum to 1" | They do not sum to 1; many players are unpriced | Use an empirical scale or a two-sided book |
| "Fit on closing price" | Not actionable | Evaluate against close, enter at open |
| "Pull each game with `get_player_props`" | 10 credits per game | `get_prop_history` is 10 for 200 games |
| "He scored 4 times, he is hot" | A 4-goal game is a tail event | Look at xG and shots |

## Output Format

```
[Player] ([team]) vs [opponent] - [competition], [date]
Model: lambda [x.xx] (open play [x.xx], penalties [x.xx]), P(score) [p]
Inputs: npxG/90 [x.xx] (shrunk from [x.xx]), expected minutes [n], team factor [x.xx]
Market: [book] [+x] implied [p], scaled [p]
Gap: [pts]   Caveat: [lineup, void rule, sample]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Model close to market | Fine; there is no edge to chase | none |
| Need the team goal expectation | Fit a score model | `poisson-score-models` |
match-lookup7.28 KB

View saved version →

---
name: match-lookup
description: "Finds soccer fixtures and results and opens one match in full: score, box scores, xG, lineups, goals, cards, substitutions and VAR events, across 17 competitions. Use when the user asks who plays, what the score was, what happened in a match, who started, when a goal was scored, or for results by date, club, stage or season. Do not use for league tables or season averages, see team-analysis. Do not use for betting lines, see odds-explorer. Do not use for shot maps across many matches, see xg-analysis."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Match Lookup

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `list_games` (2 credits) finds matches. `get_schedule` (2) lists upcoming ones with a median line. `get_game` (5) opens one match. `get_lineups` (5) and `get_events` (10) go deeper.

You find the right match, then pull only as much detail as the question needs. Most mistakes here are wrong match ids, wrong competition, and paying 10 credits for events when the 5-credit box score already answered the question.

## When to Use

- "Who plays this weekend?" / "What are the fixtures for Arsenal?"
- "What was the score?" / "Results from last Saturday"
- "Who scored, and when?" / "Who came off the bench?"
- "What formation did they play?"
- Getting a `game_id` to feed another skill

## When NOT to Use

- League table, form, home and away records: `team-analysis`
- Season-long xG, shot maps for a club or player: `xg-analysis`
- Prices, line movement, best book: `odds-explorer`
- Player season totals and per-90 rates: `player-scouting`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_games` | Fixtures and results by `season`, `stage`, `date`, `from`/`to`, `team`, `status` | 2 |
| `get_schedule` | Upcoming fixtures, next `days` (1-30, default 7), median home/draw/away line | 2 |
| `get_game` | One match: score with shootout, both box scores, every player line, key events, each book's line | 5 |
| `get_lineups` | Formation, starting XI with positions, substitutes with minutes, unused bench | 5 |
| `get_events` | Every event in order. Filter with `kind`: goals, shots, cards, substitutions, set_pieces, fouls, all | 10 |
| `list_teams` | Club ids and abbreviations, to resolve a name | 1 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_match`, `get_fixture` | `get_game` for one match, `list_games` for many |
| `get_live_score` | Not available. `get_schedule` shows `status` and `status_detail` for matches today |
| `get_team_schedule` | `list_games` with `team` |
| `search_games` | `list_games` with `team`, `date`, `from`, `to` |
| `get_referee`, `get_injuries` | Not available |

## How It Works

**Step 1: Identify the competition.** The default is the Premier League. For anything else pass `league` (`mls`, `laliga`, `seriea`, `bundesliga`, `ligue1`, `ucl`, and so on). A `game_id` is competition-scoped: passing a Champions League id without `league` set to `ucl` will not find it.

**Step 2: Find the match.**

```
list_games {"team": "ARS", "season": 2025, "status": "final", "limit": 5}
```

- `status` is `scheduled` or `final`. Results come back newest first.
- `stage: "league"` is league-table games (regular season, group stage, league phase). `stage: "knockout"` is qualifying, knockout rounds and playoffs.
- Page with `offset`, using the `next_offset` the response returns. `limit` goes up to 200.
- `date`, `from` and `to` are UTC. An evening kickoff in the Americas can land on the next UTC date.

**Step 3: Pick the lightest call that answers it.**

| Question | Call |
|----------|------|
| Score, winner | The `home.score`, `away.score` and `result` (`H`, `D`, `A`) already in `list_games` |
| Team stats, xG, who played | `get_game` |
| Who scored and assisted | `get_game` `key_events`, or `get_events` with `kind: "goals"` |
| Shots with location | `get_events` with `kind: "shots"` or `get_shot_map` with `game_id` |
| Formation and subs | `get_lineups` |

**Step 4: Read the box score honestly.** `get_game` returns `team_stats` with `goals`, `xg`, `possession_pct`, `shots`, `shots_on_target`, `corners`, `fouls`, cards, `passes`, `tackles` and `penalty_goals`. A stat ESPN did not chart is `null`, never 0. xG is StrikerAPI's own, computed from located shots, and is `null` for seasons without located shots.

## Worked Example

Arsenal at Crystal Palace, 24 May 2026, `game_id` 740968:

```
list_games {"team": "ARS", "season": 2025, "status": "final", "limit": 1}
get_game {"game_id": "740968"}
```

Trimmed to the fields that matter:

```
result: A   Crystal Palace 1, Arsenal 2   (Selhurst Park)
team_stats:
  CRY  goals 1  xg 0.86  possession 39.7  shots 8   on target 3
  ARS  goals 2  xg 2.42  possession 60.3  shots 17  on target 7
players: 40 lines   key_events: 14   odds: 8 books
```

Read: Arsenal won 2-1 and the xG (2.42 to 0.86) agrees with the scoreline. One match tells you little about quality; see `xg-analysis` for why a single xG gap is noisy.

For the goals, `get_events {"game_id": "740968", "kind": "goals"}` returns three events, each with the scorer, the assister (`player2`), the shot's `xg` (0.104, 0.035 and 0.164 in this match), `body_part`, `zone` and coordinates.

## Edge Cases

- **Old matches.** Events for older matches carry goals, cards and substitutions only. Shots, fouls and corners start with the seasons that have located shots (2012-13 for the longest-covered leagues).
- **Shootouts.** `get_game` returns the score with the shootout. Event rows carry a `shootout` flag. Exclude shootout rows from goal counts.
- **Own goals and penalties** are flagged on the event (`own_goal`, `penalty`). Filter before crediting a scorer.
- **Postponed or unplayed matches** appear as `scheduled`. Do not report a `null` score as 0-0.
- **Two-leg ties** are two separate matches. Use `stage: "knockout"` and sum the legs yourself.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "Loop `get_game` over the season for scores" | 5 credits each; `list_games` returns 200 scores for 2 | Use `list_games` and open only the matches you need |
| "Use `get_events` to get the final score" | 10 credits for data already in the list | Read `result` and the scores from `list_games` |
| "Guess the game_id from a URL" | Ids are ESPN match ids per competition | Look it up with `list_games` or `get_schedule` |
| "Call a null xG zero" | Null means no located shots, not a blank attack | Report it as not available |

## Output Format

```
[Home] [score] - [score] [Away]   [date]   [competition, stage]
Venue: [venue]   xG: [home] to [away]   Shots: [h] to [a] (on target [h] to [a])
Goals: [min] [scorer] (assist [name], xG [x.xx]) ...
Cards/subs of note: ...
```

State the competition and `game_id`. Offer the next level of detail rather than dumping all 40 player lines.

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Need the price for this match | Compare books | `odds-explorer` |
| xG gap looks interesting | Pull the shot map | `xg-analysis` |
| Want the whole table context | League position and form | `team-analysis` |
| Need a pre-match view | Form, Elo, head to head, line | `match-preview` |
| A player stood out | Season rates and log | `player-scouting` |
match-preview6.86 KB

View saved version →

---
name: match-preview
description: "Builds a pre-match preview for one soccer fixture by combining league position, form, xG for and against, Elo ratings, head to head, lineups when available and the market line, with a credit budget. Use when the user asks for a preview, a matchup breakdown, who is favored and why, or what to know before a specific fixture. Do not use for a post-match recap, see match-lookup. Do not use for a whole-slate card with many fixtures."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Match Preview

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> A full preview costs about 20 credits: `get_schedule` 2, `get_standings` 2, `get_team_stats` 5, `get_ratings` 2, `get_head_to_head` 2, plus `get_odds` 5 for each book's price. Add `get_lineups` 5 and `get_shot_map` 10 per club only when asked.

This is a compound skill. It does not introduce new data; it sequences the calls from `team-analysis`, `elo-ratings`, `odds-explorer` and `xg-analysis` in the cheapest order and assembles one readable page.

## When to Use

- "Preview Liverpool vs Manchester City"
- "What should I know before Saturday's Arsenal match?"
- "Who is favored and why?"
- Preparing a matchday post or newsletter

## When NOT to Use

- A match that already finished: `match-lookup`
- Season-long club review: `team-analysis`
- Ten fixtures at once: run this per fixture only after costing it; a weekend of 10 matches is about 200 credits

## Commands Used

| Step | Command | Credits |
|------|---------|---------|
| Find the fixture and its median line | `get_schedule` (with `team`) | 2 |
| Table position, home and away, form | `get_standings` | 2 |
| xG for and against, whole league | `get_team_stats` (no `team`) | 5 |
| Elo and its gap | `get_ratings` | 2 |
| Meetings in this competition | `get_head_to_head` | 2 |
| Each book's price | `get_odds` with `game_id` | 5 |
| Formations (only once published) | `get_lineups` | 5 |
| Shot profile | `get_shot_map` with `team` | 10 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_preview`, `get_prediction` | Assemble from the calls above. StrikerAPI provides no match forecast; any probability you state is your model's or the market's |
| `get_injuries`, `get_suspensions` | Not available. Say availability is unknown, or read `get_lineups` once lineups exist |
| `get_referee`, `get_weather` | Not available |
| `get_team_form` | The `form` field in `get_standings` |

## How It Works

**Step 1: Locate the fixture.**

```
get_schedule {"team": "LIV", "days": 7}
```

The row gives `game_id`, `starts_at`, venue, and `line` (median `home_ml`, `draw_ml`, `away_ml`, `total`, `home_handicap`, and `implied`).

**Step 2: Table and form.** One `get_standings` call; read both clubs' `rank`, `points`, `home`, `away`, `form`.

**Step 3: Underlying numbers.** One `get_team_stats` call without `team` returns all clubs. Take both clubs' `xg_for`, `xg_against`, `shots_for`, `shots_against`, and compute xG difference per match.

**Step 4: Ratings.** One `get_ratings` call (use `limit` of 50 or more so both clubs appear). The Elo difference plus the home bonus gives an expected score; see `elo-ratings` for the formula.

**Step 5: History.** `get_head_to_head` for the pair. Weight the last three or four meetings over the lifetime record, and say how many meetings that is.

**Step 6: The market.** The schedule `line.implied` values are the raw prices converted to probabilities, so they sum to more than 1. Remove the margin (see `odds-analysis`) before quoting them as the market's view. For a per-book view add `get_odds` with the `game_id`.

**Step 7: Lineups.** `get_lineups` only returns something useful once clubs have named their teams. Check the response before building anything on it. If it is empty, say lineups are not out.

## Worked Example

Premier League, 11 October 2026. The `get_schedule` example in the StrikerAPI docs shows Liverpool at home to Manchester City with 8 books and a median line of `+244 / +263 / +111` (home, draw, away), total 2.88. Converting the median prices:

```
home +244 -> 0.2907    draw +263 -> 0.2755    away +111 -> 0.4739    sum 1.0401 (4.0% margin)
no-margin (proportional): home 0.279, draw 0.265, away 0.456
```

The market makes City the favorite away from home, in a low-scoring-ish game (median total 2.88 goals).

Cross-check with ratings, from `get_ratings` on 10 October: Manchester City 1670.4, Liverpool 1578.0, a 92.4 gap. With Liverpool's 60-point home bonus the effective gap is 32.4 in City's favor, an expected score of about 0.546 for City (a draw counts as half). The market's no-margin City win (0.456) plus half the draw (0.132) is 0.588, so the market rates City a little higher than the ratings do. That is a flag to look at form and availability, not an edge.

Assembled preview:

```
Liverpool (LIV) vs Manchester City (MNC) - Anfield, 11 Oct 2026
Market (median of 8 books, no-margin): LIV 27.9%  Draw 26.5%  MNC 45.6%  Total 2.88
Elo: MNC 1670, LIV 1578; home bonus 60 -> MNC expected score 0.55
Table: [from get_standings]    Underlying: [xG for/against from get_team_stats]
History: [from get_head_to_head, last 3-4 meetings]
Unknowns: lineups, injuries (no data source here)
```

## Reading the Pieces Together

| Signal | Weight | Why |
|--------|--------|-----|
| Closing market | Highest | It already contains lineups, injuries, news |
| Elo gap with home bonus | High | Stable, but ignores current squad |
| xG difference per match | Medium, grows with matches played | Less noisy than results |
| League position and points | Low early in the season | Six matches is small |
| Head to head | Low | Squads and managers change |
| Form (last five) | Low | Results are noisy; look at the xG behind them |

If the market and your read disagree, trust the market until you can name what it is missing.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "Quote `implied` as the market's probability" | It includes the margin and sums over 100% | Devig first |
| "They won the last four meetings, so they are favored" | Small sample, changing squads | Show it, weight it little |
| "Loop `get_team_stats` per club" | 5 credits each | One call without `team` |
| "Predict a score" | No forecast is provided and one scoreline is a coin flip | Give a probability range, or use `poisson-score-models` |
| "Say who is injured" | There is no injury data | State the gap |

## Output Format

Lead with the verdict in one sentence, then the table. Always state the competition, date, credits spent, and the unknowns (lineups, injuries). End with one thing that would change the read.

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Want an independent probability | Fit a score model | `poisson-score-models` |
| Shot profiles differ | Compare where each side shoots | `xg-analysis` |
odds-analysis7.65 KB

View saved version →

---
name: odds-analysis
description: "Converts soccer odds between American, decimal and fractional formats and removes the bookmaker margin from three-way (home/draw/away) markets to get no-vig probabilities, using proportional, power and Shin methods, plus two-way devigging for totals and handicaps. Use when the user asks about devigging, implied probability, overround, fair odds, no-vig lines, the draw's true price, or the favorite-longshot bias. Do not use for browsing current odds, see odds-explorer."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Odds Analysis

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `get_odds` (5 credits for one match) returns each book's open and close with `close_prob`, the no-margin probabilities. `get_schedule` (2) returns a median line. For your own odds CSV the math below needs no credits.


## When to Use

- "What is the true probability of the draw at +250?"
- "Remove the vig from +290 / +250 / -118"
- Converting decimal 3.90 to American, or to a probability
- Comparing a soft book against Pinnacle or an exchange
- Working out how much margin a book charges

## When NOT to Use

- Browsing prices and finding the best one: `odds-explorer`
- Model calibration: `probability-calibration`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_odds` | Per-book three-way prices, handicap, total, open and close, `close_prob` with `overround` | 5 (one match) |
| `get_schedule` | Median line and raw `implied` for upcoming fixtures | 2 |
| `get_line_movement` | Pre-kickoff snapshots, for devigging the open | 10 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_fair_odds`, `get_no_vig_odds` | `close_prob` in `get_odds`, or compute below |
| `get_implied_probability` | Compute with the formulas here |
| `get_btts_odds`, `get_correct_score_odds` | Not available; only the three-way moneyline, goal handicap and total |

## Conversion

```
American +x  ->  decimal = 1 + x / 100            implied = 100 / (x + 100)
American -x  ->  decimal = 1 + 100 / x            implied = x / (x + 100)
decimal d    ->  implied = 1 / d
fractional a/b -> decimal = 1 + a / b
```

Examples: `+290` is decimal 3.90 and implied 25.6%. `-118` is decimal 1.847 and implied 54.1%. Always say which format you are in; StrikerAPI returns American.

## Three-Way Devigging

Take implied probabilities `p_h`, `p_d`, `p_a`. Their sum `B` is above 1, and `overround = B - 1`.

### 1. Proportional (multiplicative)

```
fair_i = p_i / B
```

Fast and the default. It removes the same percentage from every outcome, which understates the favorite-longshot effect.

### 2. Power

Find the exponent `k` so that `sum(p_i ** k) = 1`, then `fair_i = p_i ** k`. It shrinks longshots more than proportional.

### 3. Shin

Models the margin as protection against a share `z` of insider money. Solve for `z` so the fair probabilities sum to 1:

```python
from math import sqrt

def shin(implied):
    """Shin devig for any number of outcomes. implied: list of raw implied probabilities."""
    B = sum(implied)
    def fair(z):
        return [(sqrt(z * z + 4 * (1 - z) * x * x / B) - z) / (2 * (1 - z)) for x in implied]
    lo, hi = 0.0, 0.4
    for _ in range(100):
        z = (lo + hi) / 2
        if sum(fair(z)) > 1:
            lo = z
        else:
            hi = z
    return fair(z), z
```

Shin moves probability from longshots to favorites compared with proportional, and returns `z`, an estimate of the insider share, which behaves like a margin score.

### 4. Additive

`fair_i = p_i - (B - 1) / 3`. Can go negative on longshots. Do not use it on skewed markets.

## Worked Example

888sport's close for Crystal Palace v Arsenal, from `get_odds {"game_id": "740968"}`: home `+290`, draw `+250`, away `-118`.

```
implied:      home 0.2564   draw 0.2857   away 0.5413     sum 1.0834  (overround 8.3%)
proportional: home 0.2367   draw 0.2637   away 0.4996
Shin:         home 0.2307   draw 0.2594   away 0.5099     z = 0.042
```

The API's `close_prob` for this row was `home 0.237, draw 0.264, away 0.5, overround 0.083`, which matches the proportional method. So `close_prob` is a proportional devig; if you want Shin or power, compute them yourself from the prices.

Notice the gap. Proportional says Arsenal 50.0%, Shin 51.0%. The one-point difference is bigger than many "edges" people chase. Pick a method and keep it for the whole analysis.

Sharpest reference in the same match, Pinnacle (`+317 / +279 / -111`, overround 3.0%): `close_prob` home 0.233, draw 0.256, away 0.511. A low-margin book is the best single estimate of the market's view, because the margin you remove is small and the method barely matters.

## Choosing a Method

| Situation | Use |
|-----------|-----|
| Low-margin book (Pinnacle, exchange) | Any; they agree within a fraction of a point |
| High-margin book (6-9% overround) | Shin or power; proportional misstates the longshot |
| Heavy favorite and a longshot draw | Shin |
| Quick screening | Proportional |
| Consensus across books | Devig each book, then take the median of fair probabilities |

Exchanges can show an overround near zero (the Betfair Exchange close above was 0.4%), which is liquidity, not a missing margin. Thin early snapshots can produce prices that make no sense (a negative overround). Reject rows with overround below 0 or above about 15%.

## Two-Way Markets: Totals and Handicaps

For a total of 2.75 with over and under prices, the market has two outcomes, so devig as in two-way:

```
fair_over = p_over / (p_over + p_under)
```

Quarter lines (2.25, 2.75, and handicaps like -0.25) settle as two half-stakes on adjacent lines, with a push or half-win possible. Price each half, then average. Whole-number lines can push, which removes that probability from both sides; ignore it only knowing you are doing so.

Handicaps and totals are posted by fewer books. In the 740968 sample, only Bet 365, Pinnacle and William Hill had a total at close.

## Pitfalls

1. **Devig the triple together.** Never devig home and away alone as if the draw did not exist.
2. **Raw `implied` is not a probability.** The `implied` field in `get_schedule` is the raw price converted, summing to more than 1 (1.040 for the median Liverpool v City line). Normalize before quoting.
3. **Medians are not a book.** A median across books does not correspond to any bettable price. Use it for context.
4. **Closing versus opening.** Fair opening probabilities are less informative; the close is the benchmark.
6. **Compare methods once.** Whatever you choose, apply it to your model benchmarks and your backtest alike.
7. **Never mix formats.** American `-110` and decimal 1.91 are the same price. A mix produces nonsense.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "Divide the draw by 1.083" | Ignores the other two outcomes' margin share | Devig all three |
| "Use one book's raw prices as probability" | Overround 3-9% | Devig first |
| "Shin is always better" | The difference vanishes on low-margin books | Use it where margin is high |
| "The exchange price is the truth" | Thin books misprice | Check liquidity and overround |
| "Edge = model minus raw implied" | Overstates every edge by the margin | Subtract the devigged probability |

## Output Format

```
[Match] - [book], [open/close]
Prices: H [+x]  D [+x]  A [-x]   overround [x.x]%
Fair (proportional): H [p]  D [p]  A [p]
Fair (Shin, z = [z]): H [p]  D [p]  A [p]
Benchmark: [Pinnacle or exchange] fair H [p] D [p] A [p]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Want to browse more books | Compare prices | `odds-explorer` |
odds-explorer8.24 KB

View saved version →

---
name: odds-explorer
description: "Compares soccer three-way moneylines (home, draw, away), goal handicaps and totals across sportsbooks, finds the best price on each outcome, reads overround and line movement, and checks opening versus closing lines. Use when the user asks about the odds on a match, the best price for a draw, which book is sharpest, or how a line moved before kickoff. Do not use for removing the margin or true probabilities, see odds-analysis. Do not use for goalscorer prices, see goalscorer-prop-modeling."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Odds Explorer

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `get_schedule` (2 credits) for the median line on upcoming fixtures. `get_odds` with `game_id` (5) for every book on one match, with `date` (10) for a day, with `season` (2 per match) for a block. `get_line_movement` (10) for pre-kickoff snapshots.

Soccer is a three-way market: home win, draw, away win. A two-outcome habit from other sports, such as comparing only the favorite and underdog, throws away the draw, which is about a quarter of matches. This skill reads the whole triple.

## When to Use

- "What are the odds on Liverpool against City?"
- "Which book has the best price on the draw?"
- "How much margin is each book charging?"
- "Did the line move toward the home side?"
- "What was the closing line for this finished match?"

## When NOT to Use

- Converting prices to fair probabilities: `odds-analysis`
- Anytime goalscorer prices: `goalscorer-prop-modeling`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_schedule` | Upcoming fixtures with `line`: books count, median `home_ml`, `draw_ml`, `away_ml`, `total`, `home_handicap`, `implied` | 2 |
| `get_odds` | Per-book three-way moneyline, handicap, total, with open and close, and for finished matches the outcome | 5 (`game_id`), 10 (`date`), 2 per match (`season`) |
| `get_line_movement` | Time series of each book's `home_ml`, `draw_ml`, `away_ml`, `home_handicap`, `total` before kickoff | 10 |
| `get_game` | Includes each book's opening and closing line inside a full match (5) | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_live_odds` | `get_odds` for a match, `get_schedule` for the median line. No in-play prices |
| `get_best_odds` | Compare the `books` array from `get_odds` yourself |
| `get_btts_odds`, `get_corners_odds`, `get_card_odds` | Not available. The markets are three-way moneyline, goal handicap and goal total |
| `get_closing_line` | The `*_close` fields in `get_odds` |
| `get_public_betting` | Not available. Infer from movement only, cautiously |

## What the Prices Look Like

Moneylines are American odds. A home price of `+290` risks 100 to win 290. A price of `-118` risks 118 to win 100. The three prices for one book sum to more than 100% implied, and the excess is the overround.

`get_odds` returns, per match, a `books` array. Per book:

| Field | Meaning |
|-------|---------|
| `provider`, `source` | Book name; `the_odds_api` or `espn` |
| `home_ml_open`, `home_ml_close` (and `draw_`, `away_`) | Opening and closing moneyline. ESPN rows often have `null` opens |
| `close_prob` | No-margin home, draw and away probabilities plus `overround` |
| `home_handicap_*`, `total_*`, `over_odds`, `under_odds` | Goal handicap and total where posted. Many Odds API rows have `null` here |
| `captured_at` | When the row was captured. For ESPN rows on old matches it can be well after the match, so it is not a kickoff timestamp |

For a finished match there is also `outcome`: `result` (`H`, `D`, `A`), `median_total`, `median_home_handicap`, `over`, `home_covered`.

## How It Works

**Step 1: Get the match id.** `get_schedule` for upcoming, `list_games` (see `match-lookup`) for past.

**Step 2: Pull every book.**

```
get_odds {"game_id": "740968"}
```

**Step 3: Find the best price per outcome.** Independently for home, draw and away, take the highest American number (the largest payout). For `+330` versus `+290`, `+330` is better. For `-106` versus `-118`, `-106` is better. The best price for each side usually sits at a different book.

**Step 4: Measure each book's margin.** Convert each price to implied probability, sum the three, subtract 1.

```
implied(+x) = 100 / (x + 100)          implied(-x) = x / (x + 100)
overround   = implied_home + implied_draw + implied_away - 1
```

Betting exchanges and Pinnacle typically run far below high-street books. `close_prob.overround` gives it directly.

**Step 5: Clean the rows before ranking.** Drop or flag a book whose three prices sum to less than 100% (negative overround) or far above 115%. Exchange snapshots in thin markets produce nonsense. In one line-movement pull, an exchange row early in the series read `-323 / -1000 / -323`, which is not a real market.

**Step 6: Movement.** `get_line_movement` returns snapshots per book. ESPN lines are captured once a day, The Odds API lines hourly for matches in the next two days, so a quiet series is not necessarily a quiet market.

```
get_line_movement {"game_id": "401879267"}
```

Compare the first and last real snapshot per book, in implied probability rather than raw price, because a move from `+230` to `+200` and from `-110` to `-125` are different sizes.

## Worked Example

Crystal Palace vs Arsenal, closing lines from `get_odds {"game_id": "740968"}` (Arsenal won 2-1, `result` A):

```
book              home    draw    away    overround
888sport          +290    +250    -118     8.3%
Bet 365 (espn)    +310    +260    -118     6.3%
Betfair Exchange  +330    +290    -106     0.4%
DraftKings        +300    +270    -110     4.4%
FanDuel           +320    +260    -115     5.1%
Marathonbet       +315    +274    -110     3.2%
Pinnacle          +317    +279    -111     3.0%
William Hill      +290    +250    -118     8.3%
```

Best home price: `+330` (Betfair Exchange). Best draw: `+290` (Betfair Exchange). Best away: `-106` (Betfair Exchange), then `-110`. Lowest margin: the exchange at 0.4%, then Pinnacle at 3.0%. The same book at the worst prices (888sport, William Hill) costs about 8% overround, so the choice of book moves the expected cost of a bet by several percent. The match total: Pinnacle `2.75` with `-106` over and `-104` under; William Hill `2.5`; `outcome.median_total` was `2.75` and the total went over (`over: true`).

Opening versus closing for the home price at Pinnacle was `+649` to `+317`: the market moved heavily toward the home side's chance (Arsenal were still favorites at `-111`), which shows how much information gets priced in during the final days.

## Reading Movement

| Pattern | Reading |
|---------|---------|
| All books shorten the same side together | Information or heavy money; follow the sharpest book first |
| One soft book lags the others | A stale price, a line-shopping chance, not a signal |
| Draw price drifts while home and away hold | Often margin redistribution, not a view on the match |
| Total moves 2.5 to 2.75 | Quarter lines: the book is shading, price the over and under at the stated odds |


## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "Compare home and away only" | The draw holds about a quarter of the probability | Always show all three |
| "Best price at one book for all three" | Best prices sit at different books | Choose per outcome |
| "Pinnacle open to close is a steady line" | Compare `source`: ESPN and Odds API rows differ in timing | Report the source with each number |
| "Trust any row" | Thin exchange snapshots can be garbage | Sanity check overround |
| "Median across books is a real line" | Medians such as `2.88` or `0.13` are not postable lines | Use them for comparison, not as a bet |

## Output Format

```
[Home] vs [Away] - [date], [competition]
Best home: [price] @ [book]    Best draw: [price] @ [book]    Best away: [price] @ [book]
Lowest margin: [book] [x.x]%   Highest: [book] [x.x]%
Total: [line] ([over] / [under]) @ [book]
Movement: [book] [open] -> [close], implied [x]% -> [y]%
Credits: [n]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Need true probabilities | Remove the margin | `odds-analysis` |
| Want context for the match | Form, Elo, xG | `match-preview` |
player-scouting7.26 KB

View saved version →

---
name: player-scouting
description: "Scouts soccer players: finds ids, builds per-season and per-90 profiles for goals, assists, shots, xG and goalkeeping, reads match logs and minutes, ranks league leaders and compares players. Use when the user names a player, asks who the top scorers or creators are, wants to compare forwards, or asks whether a player is finishing above his xG. Do not use for team tables, see team-analysis. Do not use for goalscorer prices, see goalscorer-prop-modeling. Do not use for shot-by-shot locations, see xg-analysis."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Player Scouting

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `search_players` (1 credit) to find the id. `get_player` (5) for the profile and per-season rates. `get_player_games` (5) for the match log. `get_leaders` (5) for league-wide rankings.

You find the right player, then judge him on rates and minutes rather than raw totals, and you say how much evidence you have.

## When to Use

- "Tell me about Erling Haaland" / "How has Saka's season gone?"
- "Top scorers by xG per 90"
- Comparing two players across seasons
- Checking whether a striker is over- or under-finishing
- Goalkeepers: saves and clean sheets

## When NOT to Use

- Goalscorer prices or props: `goalscorer-prop-modeling`
- Where a player's shots come from: `xg-analysis` (`get_shot_map` with `player`)
- A club's table position: `team-analysis`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `search_players` | Find a player by name (accents optional), optionally within `team`. Returns id, position, club, last season, appearances | 1 |
| `get_player` | Profile, per-season appearances, starts, minutes, goals, assists, shots, our xG, cards, goalkeeping, per-90 rates, last 10 matches | 5 |
| `get_player_games` | Match log newest first: opponent, score, minutes on and off, goals, assists, shots, xG, fouls, cards | 5 |
| `get_leaders` | Season leaders. `stat`: goals, assists, goal_contributions, shots, shots_on_target, xg, yellow_cards, red_cards, saves, clean_sheets. `per90`, `min_games`, `limit` | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_player_stats` | `get_player` |
| `get_top_scorers` | `get_leaders` with `stat` set to `goals` |
| `compare_players` | Call `get_player` twice and compare per-90 rows |
| `get_player_xg` | `xg` fields in `get_player`, or `get_shot_map` with `player` |
| `get_market_value`, `get_injuries`, `get_transfers` | Not available |
| `get_assists_expected` (xA) | Not available. Assists are counted, expected assists are not provided |

## How It Works

**Step 1: Get the id.** `get_player` takes an ESPN player id, not a name.

```
search_players {"name": "Haaland"}
```

Returns `player_id` 253989, Forward, Manchester City, 137 appearances. A common surname returns several rows; narrow with `team`.

**Step 2: Pull the profile.**

```
get_player {"player_id": "253989"}
```

Each season row carries `appearances`, `starts`, `minutes`, `goals`, `assists`, `shots`, `shots_on_target`, `xg`, and the per-90 rates `goals_per90`, `assists_per90`, `shots_per90`, `xg_per90`.

**Step 3: Normalize and compare.** Totals reward minutes. Per-90 rewards role. Use both:

```
finishing gap   = goals - xg                      (positive = above expectation)
shot quality    = xg / shots
conversion      = goals / shots
minutes share   = minutes / (team matches x 90)   (role security)
```

**Step 4: Check the sample.** A season of 2,000+ minutes carries real signal on shot volume and location. Goals over xG does not: the standard error of a season goal total around its xG is roughly the square root of the xG, so a striker on 20 xG can land anywhere from about 11 to 29 goals on luck alone. Treat a one-season gap under about 4 goals as noise. Persistent over-performance across three or more seasons is more credible, and the best strikers do hold a small positive gap.

**Step 5: Rank with guardrails.** For leaders:

```
get_leaders {"stat": "xg", "per90": true, "min_games": 20, "limit": 10}
```

## Worked Example

Haaland, `get_player {"player_id": "253989"}`, Premier League, season rows trimmed:

```
season  apps  minutes  goals  xg     shots  xg_per90
2023     31    2558     27   24.75    115     0.87
2024     31    2742     22   19.93    109     0.65
2025     35    2959     27   23.12    126     0.70
2026      5     450      5    3.87     20     0.77   (five matches)
```

Read: across three full seasons he has scored 76 against 67.8 xG, about +8, with minutes above 2,500 each year. That is consistent with a small positive finishing gap but each single season (+2.3, +2.1, +3.9) is inside the noise. The 2026 line is five matches and should not be extrapolated.

## The Early-Season Per-90 Trap

`get_leaders` with `per90: true` requires a minimum of 10 appearances by default. Early in a season it returns an empty list. In October 2026, `{"stat": "xg", "per90": true}` returned `leaders: []`. Lowering `min_games` to 3 returns names, but `min_games` filters appearances, not minutes:

```
rank 1: Callum Wilson  games 4  minutes 18   xg 0.26  per90 1.28
rank 2: Zian Flemming  games 3  minutes 70   xg 0.85  per90 1.09
rank 3: Lewis Koumas   games 3  minutes 21   xg 0.21  per90 0.91
```

Eighteen minutes of substitute appearances produced the top per-90 rate. Always compute your own minutes filter (for example 450 minutes) from `get_player` before ranking, and say so.

## Position and Role

- `position` can be `null` for young or recently added players (Koumas above). Do not drop him silently.
- Goalkeepers use `saves`, `goals_conceded`, `clean_sheets`. Shot-stopping quality needs post-shot xG, which is not provided. A save rate alone mixes defence quality with keeper quality.
- Assist counts depend on scorer and passer and are far noisier than goals per 90.
- A player who moved clubs has separate season rows per club. Sum them for a career view but compare per 90.
- Stats are per competition. A player's Champions League numbers are in a separate call with `league` set to `ucl`.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "He leads per 90, so he is best" | Tiny minutes inflate rates | Set a minutes floor and show the minutes |
| "27 goals from 23 xG means elite finisher" | One season is inside luck | Look across seasons and give the standard error |
| "Compare Eredivisie and Premier League totals" | Different leagues, different defences | Compare within a league or flag the gap |
| "Search by name in `get_player`" | It needs the numeric id | `search_players` first |
| "Use goals as a proxy for threat" | Penalties and luck | Prefer xG and shots, and separate penalties via `get_shot_map` |

## Output Format

```
[Player] ([position], [club]) - [competition]
Season    Apps  Min   G   xG    G-xG   Shots  xG/90
...
Read: [finishing gap with sample size], [role and minutes], [one caveat]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Interesting shot profile | Locations, body part, penalties | `xg-analysis` |
| Player is a regular starter in a covered league | Price his goal chance | `goalscorer-prop-modeling` |
| Comparing his club's context | Team strength and xG | `team-analysis` |
poisson-score-models10.5 KB

View saved version →

---
name: poisson-score-models
description: "Builds soccer score models: independent Poisson goals from attack and defence strengths, the Dixon-Coles low-score correction with time decay, scoreline matrices, and the home/draw/away, over/under and handicap probabilities that fall out of them, validated against closing odds. Use when the user wants to predict scores, price totals or handicaps, fit team strengths, or turn xG or Elo into match probabilities. Do not use for rating-only win probabilities, see elo-ratings. Do not use for goalscorer probabilities, see goalscorer-prop-modeling."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Poisson Score Models

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `list_games` (2 credits per call, up to 200 matches) supplies scores. `get_team_stats` (5) supplies xG for and against. `get_odds` (5 per match, 2 per match by season) supplies the benchmark.

Goals in soccer are rare, roughly independent events, which is why a Poisson distribution fits them tolerably well. A score model predicts a goal rate for each side and turns the two rates into a full scoreline matrix. From that one matrix you get every market: home/draw/away, totals, handicaps and exact score.

## When to Use

- "What is the probability Arsenal win 2-1?" / "P(over 2.5)?"
- "Fit attack and defence strengths for the league"
- "Turn xG or Elo into match probabilities"
- Producing a model probability to compare with a devigged market

## When NOT to Use

- A single rating-based expected score: `elo-ratings`
- Anytime goalscorer pricing: `goalscorer-prop-modeling` (player-level, different structure)
- Evaluating a finished model: `walk-forward-validation`, `probability-calibration`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_games` | Scores by `season`, `status: "final"`, `limit` 200, `offset`; the training data | 2 |
| `get_team_stats` | `goals_for`, `goals_against`, `xg_for`, `xg_against` per match, all clubs | 5 |
| `get_ratings` | Elo input | 2 |
| `get_odds` | Closing three-way prices, handicap and total, for benchmarking | 5 or 2 per match |
| `get_schedule` | Fixtures to predict, with the median line | 2 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_prediction`, `get_forecast` | Fit the model yourself with the code below |
| `get_btts`, `get_correct_score_odds` | Not available. Both-teams-to-score and correct-score prices are not provided, so those outputs cannot be benchmarked here |
| `get_expected_lineup` | Not available |

## The Model

For home club `i` against away club `j`:

```
lambda_home = exp(attack_i + defence_j + home_advantage)      goals the home side is expected to score
mu_away     = exp(attack_j + defence_i)                        goals the away side is expected to score
P(home = x, away = y) = Poisson(x; lambda) * Poisson(y; mu) * tau(x, y)
```

`attack` is high for strong attacks. `defence` is high for leaky defences (it adds to the opponent's goals). Fix the average of each set to zero so the model is identified.

**Independent Poisson** sets `tau = 1`. It underestimates 0-0 and 1-1 and overestimates 1-0 and 0-1.

**Dixon-Coles** corrects the four low scores with a single parameter `rho`:

```
tau(0,0) = 1 - lambda * mu * rho      tau(0,1) = 1 + lambda * rho
tau(1,0) = 1 + mu * rho               tau(1,1) = 1 - rho
tau = 1 otherwise
```

`rho` is usually slightly negative: more 0-0 and 1-1 than independence implies.

**Time decay** weights each match by `exp(-xi * days_ago)`. A half-life of a year or so (`xi` near 0.0019 per day) is a common starting point; tune it.

## Fitting

Training data is cheap. A Premier League season is 380 matches: two `list_games` calls, 4 credits.

```python
rows = []
for off in (0, 200):
    rows += call("list_games", season=2025, status="final", limit=200, offset=off)["games"]
```

Then fit by weighted maximum likelihood. Tested against simulated data (recovered attack strengths with 0.95 correlation):

```python
import numpy as np
from scipy.optimize import minimize
from scipy.special import gammaln

def tau(x, y, lam, mu, rho):
    t = np.ones_like(lam)
    t = np.where((x == 0) & (y == 0), 1 - lam * mu * rho, t)
    t = np.where((x == 0) & (y == 1), 1 + lam * rho, t)
    t = np.where((x == 1) & (y == 0), 1 + mu * rho, t)
    t = np.where((x == 1) & (y == 1), 1 - rho, t)
    return t

def fit_dixon_coles(home_idx, away_idx, hg, ag, days_ago, n_teams, xi=0.0019):
    hg, ag = np.asarray(hg), np.asarray(ag)
    w = np.exp(-xi * np.asarray(days_ago))

    def unpack(p):
        att = p[:n_teams] - p[:n_teams].mean()
        dfn = p[n_teams:2 * n_teams] - p[n_teams:2 * n_teams].mean()
        return att, dfn, p[-2], p[-1]

    def nll(p):
        att, dfn, home, rho = unpack(p)
        lam = np.exp(att[home_idx] + dfn[away_idx] + home)
        mu = np.exp(att[away_idx] + dfn[home_idx])
        t = tau(hg, ag, lam, mu, rho)
        if np.any(t <= 0):
            return 1e10
        ll = (np.log(t) + hg * np.log(lam) - lam - gammaln(hg + 1)
              + ag * np.log(mu) - mu - gammaln(ag + 1))
        return -(w * ll).sum()

    p0 = np.zeros(2 * n_teams + 2)
    p0[-2] = 0.25
    res = minimize(nll, p0, method="L-BFGS-B",
                   bounds=[(None, None)] * (2 * n_teams) + [(-0.5, 1.0), (-0.3, 0.3)])
    return unpack(res.x)
```

## From Rates to Markets

```python
def score_matrix(lam, mu, rho, max_goals=10):
    g = np.arange(max_goals + 1)
    ph = np.exp(-lam + g * np.log(lam) - gammaln(g + 1))
    pa = np.exp(-mu + g * np.log(mu) - gammaln(g + 1))
    m = np.outer(ph, pa)
    m[0, 0] *= 1 - lam * mu * rho
    m[0, 1] *= 1 + lam * rho
    m[1, 0] *= 1 + mu * rho
    m[1, 1] *= 1 - rho
    return m / m.sum()

def one_x_two(m):
    return np.tril(m, -1).sum(), np.trace(m), np.triu(m, 1).sum()   # home, draw, away
```

With `m[i, j]` the probability of home `i`, away `j`:

| Market | Formula |
|--------|---------|
| Home / draw / away | `one_x_two(m)` |
| Over 2.5 | `1 - sum(m[i, j] for i + j <= 2)` |
| Exact score 2-1 | `m[2, 1]` |
| Home -1 (win by 2+) | `sum(m[i, j] for i - j >= 2)` |
| Asian handicap -0.5 | Same as the home win probability; whole and quarter lines need push and half-stake handling |
| Both teams to score | `1 - m[0, :].sum() - m[:, 0].sum() + m[0, 0]` (model only; no price here to compare) |

Quarter-total and handicap lines such as 2.75 settle as two half-stakes at 2.5 and 3.0. Price each half separately and handle the push at the whole line.

## Worked Example (back-of-envelope, from `get_team_stats`)

A quick multiplicative version, before any fitting, using the 2025-26 per-match xG from `get_team_stats {"season": 2025}`. League average xG for per club per match was 1.47. Arsenal: 1.90 for, 0.83 against. Chelsea: 1.73 for, 1.36 against.

```
lambda_ARS = 1.90 * 1.36 / 1.47 = 1.76        mu_CHE = 1.73 * 0.83 / 1.47 = 0.98
```

Passing 1.76 and 0.98 to `score_matrix` (no home bonus, because season averages mix home and away matches) with `rho` of 0 and then -0.05:

```
rho      home win  draw   away win   over 2.5   1-1    2-1    1-0
0.00      0.557    0.234   0.208      0.515    0.111  0.098  0.114
-0.05     0.552    0.246   0.203      0.515    0.117  0.098  0.109
```

A small negative `rho` moves about 1.2 points of probability into the draw and leaves the total alone, which is the expected pattern. This is a sketch, not a forecast: it ignores the home bonus, lineups and the opposition faced. A real fit (above) estimates strengths jointly.

## Using xG and Elo as Inputs

- **xG-based rates.** Replace goals with xG in the strength estimates (`get_team_stats` gives `xg_for` and `xg_against` per match). xG is less noisy than goals over a short window. Fit on the same likelihood, treating xG as the scored-goals analogue, or blend goals and xG.
- **Elo-based rates.** Map the Elo gap to a goal difference with a regression fit on history, set the total from the league mean, and solve for `lambda` and `mu`.
- **Shrinkage.** For clubs with few matches (promoted sides, early season), shrink attack and defence toward the league mean in proportion to matches played.

## Validation

1. Walk forward. Fit on matches before date `t`, predict the matches on `t`, step forward. Never random-split. See `walk-forward-validation`.
2. Score with the ranked probability score (RPS) and log loss across home/draw/away, not accuracy. See `probability-calibration`.
3. Benchmark against the devigged closing prices from `get_odds` (see `odds-analysis`). If you cannot beat the market's RPS, you have a model of what the market already knows. That is the usual outcome for a goals-only model; the useful question is where it differs and whether those differences are signal.
4. Check totals separately: calibrate P(over) by decile against the actual over rate.

## Pitfalls

1. **Leakage.** Using season-end strengths for early-season matches. Refit as you go, or use the time-decayed fit frozen at the prediction date.
2. **Home advantage differs by league and era.** Fit it per competition. Neutral games (finals, some European ties) have none.
3. **Cup and European matches.** Mixed-competition fits need strength links between leagues; avoid unless you model it.
4. **Small samples.** Fewer than about 15 matches for a club makes its strengths unreliable.
5. **Overdispersion.** Real totals have a fatter right tail than Poisson. A negative binomial or a mixture can help totals pricing.
6. **Score effects.** Teams ahead take their foot off. Pure Poisson ignores game state.
7. **Lineups.** Strengths describe the average squad, not tonight's.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "Random 80/20 split" | Uses the future to predict the past | Walk forward by date |
| "Poisson says 3-1 is the pick" | Most likely scoreline is rarely likely | Report probabilities |
| "Accuracy 52%" | Draw and close calls hide quality | RPS and log loss against the closing line |
| "Use goals_for as the attack rate" | Opponent quality is ignored | Fit attack and defence jointly |

## Output Format

```
[Home] v [Away] - model lambda [x.xx], mu [x.xx], rho [x.xxx], fit through [date]
Home / Draw / Away: [p] / [p] / [p]   (market no-margin: [p] / [p] / [p])
Over 2.5: [p]  BTTS (model only): [p]  Most likely scores: 1-1 [p], 1-0 [p], 2-1 [p]
Backtest RPS: model [x.xxx], market [x.xxx], n = [matches]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Model probabilities | Check calibration | `probability-calibration` |
| Need a proper test | Walk-forward harness | `walk-forward-validation` |
probability-calibration8.68 KB

View saved version →

---
name: probability-calibration
description: "Checks and fixes whether soccer model probabilities mean what they say: per-class reliability for home, draw and away, ranked probability score, log loss, multiclass Brier, and Platt, isotonic or temperature scaling for three-way outputs. Use when the user asks if their probabilities are calibrated, why the model is overconfident, how to fix the draw, or whether to trust a 60% number. Do not use for building the model, see poisson-score-models or elo-ratings. Do not use for the walk-forward split, see walk-forward-validation. Do not use for devigging, see odds-analysis."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Probability Calibration

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> Calibration works on your model's out-of-sample predictions plus outcomes. Outcomes come from `list_games` (2 credits per call); the market benchmark from `get_odds` (5 per match, 2 per match by season).


## When to Use

- "Are my win probabilities calibrated?"
- "The model is overconfident on favorites"
- "My draws are always too low"
- Picking between a raw model, a scaled model and the market
- Before computing any edge

## When NOT to Use

- Fitting the underlying model: `poisson-score-models`, `elo-ratings`
- Designing the out-of-sample split: `walk-forward-validation`
- Converting prices to probabilities: `odds-analysis`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_games` | Outcomes (`result` H, D or A, scores) | 2 per call |
| `get_odds` | Market close and `close_prob` as a benchmark | 5 or 2 per match |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_model_probabilities`, `get_predictions` | StrikerAPI provides none; you supply the model output |
| `get_calibration` | Compute it from the code below |

## Inputs

A table with one row per match, with out-of-sample predictions (never in-sample):

```
match_id, date, p_home, p_draw, p_away, result   (result in {H, D, A})
```

Probabilities must sum to 1. Check it. Everything below assumes the predictions came from a walk-forward run.

## Metrics

```python
import numpy as np

def log_loss(P, y):            # P: (n, 3) probabilities in order H, D, A; y: indices 0, 1, 2
    return -np.mean(np.log(np.clip(P[np.arange(len(y)), y], 1e-12, 1)))

def brier(P, y):
    O = np.eye(3)[y]
    return np.mean(((P - O) ** 2).sum(axis=1))

def rps(P, y):
    O = np.eye(3)[y]
    cum_p, cum_o = P.cumsum(axis=1)[:, :2], O.cumsum(axis=1)[:, :2]
    return np.mean(((cum_p - cum_o) ** 2).sum(axis=1) / 2)
```

| Metric | Reads as | Notes |
|--------|----------|-------|
| Log loss | Penalizes confident misses | Sensitive to extremes; clip |
| Multiclass Brier | Mean squared error on the triple | Robust |
| RPS | Ordered Brier on cumulative outcomes | Standard for soccer; lower is better |

Always report the same metric for the market (devigged close) on the same matches. The absolute number matters less than the gap.

## Reliability, Per Class

Calibration is checked for each class separately. For home win:

1. Bin matches by `p_home` (deciles, or fewer when n is small).
2. In each bin, compare the mean predicted probability to the observed home-win frequency.
3. Plot observed against predicted with a confidence interval (`sqrt(p (1 - p) / n)` per bin).

Repeat for the draw and away win.

```python
def reliability(p, hit, bins=10):
    edges = np.quantile(p, np.linspace(0, 1, bins + 1))
    idx = np.clip(np.digitize(p, edges[1:-1]), 0, bins - 1)
    rows = []
    for b in range(bins):
        m = idx == b
        if m.sum():
            rows.append((p[m].mean(), hit[m].mean(), int(m.sum())))
    return rows        # (mean predicted, observed rate, count)
```

Expected calibration error is the count-weighted mean of the absolute gap; report it with the bin counts, since 380 matches in ten bins is 38 each and each bin's rate has a standard error near 7 points.

## The Draw Problem

Draws are the hardest class. A model's draw probability tends to stay inside a narrow band, so the draw reliability curve is a short, noisy segment, and it is easy to get wrong in a systematic way. Typical failure modes:

- A Poisson model with independent goals understates draws; Dixon-Coles helps
- An Elo-based model treats a draw as half a win and has no draw model at all
- A model fit across leagues applies one draw rate everywhere

Check the average predicted draw probability against the observed draw frequency first. If they differ by more than about 2 points over several hundred matches, fix the draw rate before anything else.

## Fixing It

Fit the calibrator on one set of matches and test it on a later one. Never fit and test on the same predictions.

| Method | When | Notes |
|--------|------|-------|
| **Temperature scaling** | Probabilities uniformly too confident or too timid | One parameter `T` on the logits of the triple. Cheap, hard to overfit. Start here |
| **Multinomial logistic recalibration** | Class-specific bias (draw too low) | Logistic regression on `log(p_h / p_d)`, `log(p_a / p_d)` |
| **Platt, one-vs-rest** | Binary question like "home win" | Logistic fit on the logit of one probability; renormalize the three |
| **Isotonic** | Plenty of data (thousands of matches) | Flexible; overfits on one season of 380 matches |
| **Blend with the market** | Always worth testing | `p = w * p_model + (1 - w) * p_market` with `w` fit on a prior period. A model that cannot beat this has little to add |

```python
from scipy.optimize import minimize_scalar

def fit_temperature(P, y):
    logits = np.log(np.clip(P, 1e-12, 1))
    def nll(T):
        z = logits / T
        z -= z.max(axis=1, keepdims=True)
        Q = np.exp(z); Q /= Q.sum(axis=1, keepdims=True)
        return log_loss(Q, y)
    return minimize_scalar(nll, bounds=(0.5, 3.0), method="bounded").x
```

`T` above 1 flattens (the model was overconfident); below 1 sharpens.

## Worked Example Plan

1. Produce walk-forward predictions for a season of Premier League matches with `poisson-score-models` (the 380 outcomes come from two `list_games` calls, 4 credits).
2. Compute log loss, Brier and RPS for the model on all 380.
3. Pull the devigged closes from `get_odds` (season mode, 760 credits; consider a single club's matches with `team` to cut the cost while testing) and compute the same metrics.
4. Draw the three reliability curves. Report the mean predicted draw rate against the observed rate.
5. Fit `T` on the first half, apply to the second half, compare.
6. Report the scores with standard errors, using the per-match differences against the market.

Do not state results before running this; the numbers belong to your model and sample.

## Sample Size Guide

| Matches | What you can conclude |
|---------|----------------------|
| Under 200 | Overall draw rate and rough bias only |
| 380 (one league-season) | Coarse reliability in 4 or 5 bins; temperature scaling |
| 1,500+ | Per-class curves, multinomial recalibration |
| 5,000+ | Isotonic, league-specific effects |

## Pitfalls

1. **Calibrating on in-sample predictions.** In-sample probabilities are overconfident by construction.
2. **Binning with small n.** Ten bins on 200 matches is mostly noise; use fewer.
3. **Fixing the wrong thing.** Bad ranking cannot be fixed by calibration. Check discrimination (does the model separate H from A?) as well.
4. **Calibrating the favorite only.** Look at all three classes.
5. **Different leagues.** Calibrate per competition when draw rates differ.
6. **Time drift.** Recalibrate on a rolling window and check the scaling stays stable.
7. **Treating the market as perfectly calibrated.** It is the best public benchmark, not a law. Test it too.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "Accuracy is 55%, so it is fine" | Says nothing about the probabilities | Reliability and RPS |
| "Isotonic on one season" | Overfits | Temperature scaling |
| "Calibrated on the test set" | Optimistic | Train the calibrator earlier, test later |
| "The draw is unpredictable, skip it" | The draw is a third of the triple | Check the average draw rate at least |

## Output Format

```
[Model] - [competition] [period], n = [matches], out-of-sample
              Model      Scaled     Market (devigged close)
Log loss      [x.xxx]    [x.xxx]    [x.xxx]
Brier         [x.xxx]    [x.xxx]    [x.xxx]
RPS           [x.xxx]    [x.xxx]    [x.xxx]
Mean p(draw) [x.xxx] vs observed [x.xxx]
Verdict: [calibrated / overconfident / draw biased], T = [x.xx]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Still behind the market | Improve features and model | `poisson-score-models` |
strikerapi-tool8.11 KB

View saved version →

---
name: strikerapi-tool
description: "Default data source for the StrikerAPI skills. Routes every request for soccer results, lineups, box scores, events, shot maps, xG, Elo ratings, three-way sportsbook odds and anytime goalscorer prices to one of 19 tools across 17 competitions. Use when any other skill needs data, or when the user asks which tool to call, what a call costs, or how to hit the REST API."
metadata:
  version: 1.0.0
  author: StrikerAPI
  user-invocable: false
---

# StrikerAPI Tool

> Connection: this plugin reaches the data through its MCP server, which the user connects with a normal OAuth sign-in. Never ask the user for an API key, token or password, and never put one in a tool argument.


Every other skill in this pack gets its data here. This skill owns the 19 tools, their credit costs, the response envelope, and the rules that keep a request cheap and correct.

- Full reference with example responses: https://strikerapi.com/docs

## The response envelope

Every REST response is wrapped as `{"data": ...}`. Unwrap it before reading fields. Over MCP the tool result carries the same payload.



## The 19 tools

| Tool | What it returns | Credits | Skills that use it |
|------|-----------------|---------|--------------------|
| `list_teams` | Club ids, abbreviations, colours for a season | 1 | team-analysis, match-lookup |
| `search_players` | Player ids by name, with position, club, last season | 1 | player-scouting, goalscorer-prop-modeling |
| `list_games` | Fixtures and results by season, stage, date, range or club | 2 | match-lookup, poisson-score-models, elo-ratings |
| `get_schedule` | Upcoming fixtures (1-30 days) with the median three-way line | 2 | odds-explorer, match-preview |
| `get_standings` | League table with deductions, home/away, form | 2 | team-analysis |
| `get_ratings` | Our Elo ratings, strongest first, current or as of a date | 2 | elo-ratings, match-preview |
| `get_head_to_head` | Two clubs' record and meetings in this competition | 2 | match-preview, team-analysis |
| `get_game` | One match: score, both box scores with xG, player lines, key events, each book's line | 5 | match-lookup, match-preview |
| `get_lineups` | Formations, starting XIs, substitutions with minutes, unused bench | 5 | match-lookup, goalscorer-prop-modeling |
| `get_player` | Profile, per-season totals and per-90 rates, last 10 matches | 5 | player-scouting |
| `get_player_games` | A player's match log, newest first | 5 | player-scouting, goalscorer-prop-modeling |
| `get_leaders` | Season leaders in goals, assists, xG, shots, cards, saves, clean sheets | 5 | player-scouting, xg-analysis |
| `get_team_stats` | Per-match averages for and against, including xG | 5 | team-analysis, xg-analysis |
| `get_line_movement` | Pre-kickoff snapshots of each book's line | 10 | odds-explorer |
| `get_events` | Ordered match events, shots with location and xG | 10 | match-lookup, xg-analysis |
| `get_shot_map` | Shots and zone totals for a club, player or match | 10 | xg-analysis, goalscorer-prop-modeling |
| `get_player_props` | Anytime goalscorer prices by book, opening or closing | 10 (0 with `estimate_only`) | goalscorer-prop-modeling |

`get_odds` is priced by how you call it: pass `game_id` for one match, `date` for a day, or `season` (optionally with `team`) for priced matches, 100 per page.

## Parameters shared by most tools

| Parameter | Meaning |
|-----------|---------|
| `league` | One of `soccer` (Premier League, the default), `mls`, `laliga`, `seriea`, `bundesliga`, `ligue1`, `ucl`, `championship`, `nwsl`, `ligamx`, `uel`, `uecl`, `eredivisie`, `primeira`, `scotland`, `brazil`, `argentina` |
| `season` | The year the season starts: `2025` is 2025-26. MLS, the NWSL, Liga MX, Brazil and Argentina use the calendar year |
| club arguments | A club id, abbreviation or name. Ids and abbreviations are safest |
| dates | `YYYY-MM-DD`, in UTC |
| paging | `limit` plus `offset`; the response carries `next_offset` |

Exceptions worth remembering: `get_standings` does not accept `ligamx` or `argentina`. `get_player_props` and `get_prop_history` accept only `soccer`, `mls`, `laliga`, `seriea`, `bundesliga`, `ligue1` and `ucl`.

## Season resolution

- European leagues: August to December, the season is the current calendar year (October 2026 is season `2026`, meaning 2026-27). January to July, it is the previous year (March 2026 is season `2025`).
- MLS, NWSL, Liga MX, Brazil, Argentina: the calendar year.
- Omit `season` for "the latest". Early in a season that means very small samples, see the per-90 warning in `player-scouting`.

## Coverage (from the StrikerAPI docs and README, check https://strikerapi.com/docs for the per-season counts)

- Results, lineups and goals from 2001-02 for the longest-covered leagues; shot locations and our xG from 2012-13. Start years differ by competition.
- Sportsbook lines from 2017-18, densest from 2021-22 to 2023-24. Opening and closing lines from The Odds API since June 2020 for 16 competitions, ESPN's lines from 2024.
- Anytime goalscorer prices for seven competitions: Premier League and MLS from April 2024, Ligue 1 from May 2024, La Liga, Serie A and Bundesliga from August 2024, Champions League from March 2025. Mostly DraftKings and FanDuel. No other player market exists.
- Older matches carry goals, cards and substitutions only in `get_events`. Shots, fouls and corners come later.

## Which tool, fast

| You need | Call | Not this |
|----------|------|----------|
| Club or player id | `list_teams`, `search_players` | Guessing ids |
| What is on this week, with a price | `get_schedule` | `list_games` (no lines) |
| Results for a club or date | `list_games` | `get_game` in a loop (5 each) |
| One match in full | `get_game` | `get_events` (10, events only) |
| League table | `get_standings` | `get_team_stats` |
| xG for and against, every club | `get_team_stats` with no `team` (5 for the whole league) | One call per club |
| Where a club's shots came from | `get_shot_map` | `get_events` per match |
| Best price on a match | `get_odds` with `game_id` | `get_line_movement` |
| How a price moved | `get_line_movement` | Re-polling `get_odds` |
| Goalscorer prices | `get_player_props` | `get_odds` (no player markets) |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_xg`, `get_player_xg` | `get_shot_map`, `get_team_stats`, `get_leaders` with `stat` set to `xg` |
| `get_table` | `get_standings` |
| `get_fixtures` | `get_schedule` (upcoming) or `list_games` |
| `get_match` | `get_game` |
| `get_player_stats` | `get_player` |
| `get_injuries`, `get_transfers`, `get_live_scores` | Not in the 19 tools. Lineups appear in `get_lineups` |
| Other player props (shots, cards, assists) | Not available. Only `player_goal_scorer_anytime` exists |
| Both-teams-to-score, corners, cards markets | Not available. `get_odds` carries the three-way moneyline, goal handicap and total |

## Credit discipline

1. Estimate before any loop. A Premier League season is 380 matches: `get_odds` by season is 760 credits, `get_game` per match is 1,900.
2. Prefer the call that covers more per credit. `list_games` returns up to 200 matches for 2 credits. `get_team_stats` without `team` returns the whole league for 5. `get_shot_map` returns up to 2,000 located shots for 10.
3. `get_player_props` takes `estimate_only: true` and returns the cost without charging or running.
4. Cache everything. Finished matches never change; save responses to disk keyed by tool and arguments.
5. The `X-Credits-Remaining` response header reports your balance.

## BYOD

If the user brings a CSV of results or odds, skip the API and check columns first: `date`, `home`, `away`, `home_goals`, `away_goals` for results; `home_ml`, `draw_ml`, `away_ml` (American) or decimal equivalents for odds. Flag, do not silently drop, missing rows. No credits are used.

## What to do next

| What you retrieved | Next |
|--------------------|------|
| Fixtures and a median line | `match-preview` or `odds-explorer` |
| Per-book prices | `odds-analysis` to remove the margin |
| Shot maps or team xG | `xg-analysis` |
| Results history | `elo-ratings` or `poisson-score-models` |
| Goalscorer prices | `goalscorer-prop-modeling` |
team-analysis7.22 KB

View saved version →

---
name: team-analysis
description: "Analyzes clubs: league table, points per game, home and away splits, form, goals and xG for and against, possession and discipline, head to head, and strength relative to the league. Use when the user asks how a club is doing, wants the table, compares two clubs, or asks whether a club is over- or under-performing. Do not use for a single match, see match-lookup. Do not use for player-level stats, see player-scouting. Do not use for building ratings, see elo-ratings."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Team Analysis

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `get_standings` (2 credits) for the table. `get_team_stats` (5) for per-match averages including xG, and it returns the whole league in one call when `team` is omitted. `get_head_to_head` (2) for meetings. `get_ratings` (2) for Elo.

You turn a table and a block of averages into a view of how good a club actually is, separating what the points say from what the underlying numbers say.

## When to Use

- "How is Chelsea doing?" / "Show me the Bundesliga table"
- "Is this club lucky?" (points versus xG)
- Home and away splits, recent form
- Comparing two clubs on attack and defence
- Strength of schedule context before a preview

## When NOT to Use

- One match in detail: `match-lookup`
- Shot locations and finishing: `xg-analysis`
- A named player: `player-scouting`
- Producing win probabilities from ratings: `elo-ratings`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_teams` | Club ids and abbreviations for a season | 1 |
| `get_standings` | Rank, played, W/D/L, goals for and against, points, deductions, qualification note, home and away records, form | 2 |
| `get_team_stats` | Per-match averages for and against: goals, `xg_for`, `xg_against`, shots, shots on target, possession, pass accuracy, corners, fouls, cards, tackles, clean sheets | 5 |
| `get_head_to_head` | Meetings in this competition: wins, draws, goals, first meeting, results newest first | 2 |
| `get_ratings` | Elo ratings, strongest first | 2 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_table`, `get_league_table` | `get_standings` |
| `get_team` | `get_team_stats` with `team` set |
| `get_form` | The `form` field in `get_standings` (last results as letters), or `list_games` with `team` |
| `get_xg_table` | Build it from `get_team_stats`, see `xg-analysis` |
| `get_injuries` | Not available |

## How It Works

**Step 1: Resolve the competition and season.** Default is the Premier League, latest season. `get_standings` accepts every league except `ligamx` and `argentina`. In the Champions League it returns one table per group or the league phase.

**Step 2: Pull the table and the averages.**

```
get_standings {"season": 2025}
get_team_stats {"season": 2025}
```

Omit `team` in `get_team_stats` to get all clubs for 5 credits. One call per club would cost 5 each.

**Step 3: Compute the gaps.** From `get_team_stats`:

```
xGD per match   = xg_for - xg_against
goal diff/match = goals_for - goals_against
luck gap        = goal diff/match - xGD per match      (positive = outperforming)
```

From `get_standings`: points per game = `points / played`, and home and away records parse from strings like `"3-0-0"` (wins-draws-losses).

**Step 4: Judge sustainability.** Over a full 38-match season, goal difference tracks xG difference closely and, as a rule of thumb, a gap above about 0.3 goals per match is unusual. Over 5 or 6 matches, a gap of 0.5 is noise. Do not call a club lucky or unlucky on fewer than about 10 matches.

**Step 5: Add context.** Home and away splits (a club with 5 wins from 5 at home has played a different schedule than one with 3 and 2). `get_head_to_head` for the specific pairing, but it counts every meeting in this competition since the first, so recent meetings matter more than the lifetime record.

## Worked Example

2025-26 Premier League, `get_team_stats {"season": 2025}`, sorted by xG difference per match (38 matches each):

```
club  goals_for goals_against xg_for xg_against  xGD/match
ARS     1.87      0.71        1.90     0.83       +1.07
MNC     2.03      0.92        1.95     1.22       +0.73
MAN     1.82      1.32        1.76     1.32       +0.44
CHE     1.53      1.37        1.73     1.36       +0.37
...
BUR     1.00      1.97        0.94     2.12       -1.18
```

Read: Arsenal's goal difference per match (+1.16) sits just above its xG difference (+1.07), so the table position is earned, and the defence is the story: 0.83 xG conceded per match is the best in the league. Manchester City scored more than Arsenal but conceded more. Burnley's -1.18 xGD is the worst by 0.6, so its position is not bad luck.

The early-season table is a different read: `get_standings` with no season (2026-27) after 5 or 6 matches shows Manchester City and Arsenal level on 15 points, Brighton with the best goal difference (+13 from 6). That is six matches. Treat it as a starting point and pull `get_team_stats` before concluding anything.

## Interpreting Fields

| Field | Note |
|-------|------|
| `deductions` | Official points deductions are applied to `points`. Mention them |
| `note` | Qualification or relegation note from the table, null mid-table |
| `form` | Most recent results as letters, e.g. `WLWWW` |
| `possession_pct`, `pass_accuracy_pct` | Style descriptors, weakly related to results. Do not rank clubs by them |
| `red_cards_total` | Total red cards, not a per-match rate |
| any stat `null` | ESPN did not chart it. It is not 0 |

`get_team_stats` averages cover every final match in the season for the competition you query, "league and cup stages alike". In the Champions League that means group and knockout matches together. Other competitions are not included: a club's Premier League numbers do not contain its Champions League matches.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "They are top, so they are best" | Six matches is noise | Show xGD and the sample size together |
| "Possession leaders are better" | Possession and results correlate weakly | Lead with xG for and against |
| "Lifetime head to head decides the preview" | Squads and managers change | Weight the last few meetings, state the count |
| "Call `get_team_stats` per club" | 5 credits x 20 clubs | One call without `team` |
| "Compare Elo across leagues" | Ratings move only on one competition's matches | Compare within a competition only |

## Output Format

```
[Competition] [season] after [N] matches

Table: [rank] [club] [pts] ([W-D-L], GD [x])  form [WWDLW]
Underlying: xG for [x.xx] / against [x.xx] per match, xGD [+x.xx]
Luck gap: [goal diff/match] vs [xGD/match]  -> [outperforming / in line / underperforming]
Home: [W-D-L]   Away: [W-D-L]
Caveat: [sample size, deductions, schedule]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Big goals-versus-xG gap | Where are the shots coming from | `xg-analysis` |
| Want win probabilities from strength | Convert ratings | `elo-ratings` |
| Comparing two clubs for a fixture | Full preview | `match-preview` |
| Want to model the league | Poisson team strengths | `poisson-score-models` |
walk-forward-validation9.06 KB

View saved version →

---
name: walk-forward-validation
description: "Evaluates soccer prediction models without leaking the future: walk-forward and expanding-window splits by matchweek or season, point-in-time features, leakage checks specific to standings, season stats and ratings, and significance testing of a model against the closing line. Use when the user asks whether cross-validation is okay, how to split match data, whether their backtest leaked, or how to compare two models. Do not use for probability scaling, see probability-calibration."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# Walk-Forward Validation

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> Results history comes cheap from `list_games` (2 credits for up to 200 matches). `get_ratings` with `date` gives point-in-time Elo (2). Most StrikerAPI calls return the state as of today, so rebuilding features as of an earlier date from match logs is usually the safe path.

Soccer data is a time series with a regime change every August. A random k-fold split uses next year's results to predict last year's matches, which makes any model look better than it will be. Walk-forward validation trains on the past, predicts the next block, then steps forward.

## When to Use

- "Can I use 5-fold cross-validation on matches?" (No)
- "Is my model any good?"
- "Did my backtest leak?"
- Choosing between two models, two feature sets or two hyperparameters
- Tuning an Elo or a decay rate

## When NOT to Use

- Turning scores into calibrated probabilities: `probability-calibration`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_games` | Results by `season`, `from`/`to`, `status` for the training stream | 2 per call |
| `get_ratings` | Elo as of a `date` | 2 |
| `get_game` | Per-match xG and box score (5 each, use sparingly) | 5 |
| `get_shot_map` | A club-season of shots with `game_id` and `date`, so you can rebuild xG per match | 10 |
| `get_odds` | Closing prices for the benchmark, `season` mode | 2 per match |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_standings_as_of` | Not available. `get_standings` returns the table as it stands now (or at season end for a past season); rebuild an as-of table from `list_games` |
| `get_team_stats_as_of` | Not available. `get_team_stats` averages the whole season; rebuild as-of averages from per-match data |
| `get_form_history` | Rebuild from `list_games` |

## The Split

**Expanding window (default).** Train on everything before block `t`, predict block `t`, add block `t` to the training set, step.

**Rolling window.** Same, but drop blocks older than N seasons. Use if the league's style or your model's parameters drift.

Block choice:

| Block | When |
|-------|------|
| Matchweek (roughly 10 matches) | Models that update weekly: Elo, time-decayed Poisson |
| Month | Cheaper retrain |
| Season | Models with hard-to-refit parameters, and for the final report |

```python
def walk_forward(matches, first_test_date, step_days=7, fit=None, predict=None):
    matches = sorted(matches, key=lambda m: m["starts_at"])
    out, t = [], first_test_date
    while t <= matches[-1]["starts_at"][:10]:
        train = [m for m in matches if m["starts_at"][:10] < t]
        test = [m for m in matches if t <= m["starts_at"][:10] < add_days(t, step_days)]
        if train and test:
            model = fit(train)
            out += [(m, predict(model, m)) for m in test]
        t = add_days(t, step_days)
    return out
```

(`add_days` is a one-line date helper.) Keep the loop identical for every model you compare.

## Leakage Checklist for Soccer

| Leak | How it happens | Fix |
|------|---------------|-----|
| Table position | `get_standings` returns the table as of now, not as of the match date. Using it as a feature for a match in November uses May's table | Rebuild the as-of table from results |
| Season xG and team averages | `get_team_stats` averages include the match you predict and everything after it | Compute rolling averages from per-match xG, shifted by one match |
| Current Elo for past matches | `get_ratings` without `date` is today's rating | Use the rating before the match, or your own point-in-time Elo |
| Player season totals | `get_player` rows are whole-season | Build from `get_player_games`, shifted |
| Closing odds as a feature | If you predict before the close, the close is the future | Use opening or earlier snapshots only |
| Same-match rows | Both clubs' rows from one match split across train and test | Keep a match intact on one side |
| Promoted clubs | A club with no history gets a fabricated one | Give them a prior, flag them |
| Tuning on the test blocks | Choosing hyperparameters from the final scores | Tune inside the training window, nested walk-forward |
| Mid-season managerial or squad change | Treated as stationary | Use decay, check for breaks |

Shift rule: any feature for match `m` uses only matches dated strictly before `m`'s kickoff. If two matches share a day, treat same-day results as unavailable.

## Metrics

For three-way outcomes (home, draw, away) with probabilities `p` and the one-hot outcome `o`:

```
Log loss = - log(p[actual])                                 mean over matches
Multiclass Brier = sum_k (p_k - o_k) ** 2                   mean over matches
Ranked probability score (RPS) = sum over the 2 cumulative cut points of (cum_p - cum_o) ** 2 / 2
```

RPS respects the order home, draw, away, so predicting a draw when the home side wins is penalized less than predicting an away win. It is the standard metric for soccer. Compare each model with the market's devigged closing probabilities from `odds-analysis`; the gap in RPS is what you are after. Accuracy is a poor choice: a model that never picks the draw can still score 50%.

## Significance

1. Compute per-match loss for model A and the benchmark B on the same matches.
2. Take the difference `d_i = loss_A_i - loss_B_i`.
3. Mean and standard error: `se = std(d) / sqrt(n)`. A t-statistic above about 2 is suggestive, not proof.
4. Or block-bootstrap `d` by matchweek (to keep within-week correlation) and read the 95% interval.

How much is enough? One league-season is 380 matches, which is not many for comparing two reasonable models whose per-match scores differ by small amounts. Measure the standard deviation of `d` on your own data and report the standard error rather than quoting a rule of thumb; expect that small differences will not be separable within a single season. If you tested several models or settings, apply a multiple-comparison correction or hold out a final untouched season.

## Worked Example Plan

Evaluate a Dixon-Coles model (`poisson-score-models`) against Pinnacle's closing prices for 2023-24 to 2025-26:

1. Pull results: two `list_games` calls per season (12 credits for three seasons).
2. Pull closing prices: `get_odds` with `season` and 380 matches is 760 credits per season. Budget this first; consider one season, or restrict to a team's matches with `team`.
3. Run `walk_forward` with weekly steps starting at matchweek 11 each season (so ratings have data).
4. Devig Pinnacle's close with `odds-analysis`.
5. Report mean RPS for the model, the market, and the difference with its standard error.

A common outcome: the market's RPS is lower than a goals-only model's. The honest conclusion is to find where the model differs from the market and test those differences on their own.

## Pitfalls

1. **Random splits.** Never. Even shuffled-within-season is leakage.
2. **Few early-season matches.** Starting each season with an empty model gives poor early predictions. Carry ratings over with regression to the mean.
3. **Changing the rules mid-experiment.** Define the metric, block size and start date before you look.
4. **Selection by test score.** Picking the best of 20 settings by the test score inflates it. Hold out a final season.
5. **Ignoring cup breaks and international windows.** They are not leakage but they change the nature of the next block.
6. **Draw prediction.** Judge it separately in the calibration table, not just via accuracy.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "5-fold CV is standard" | Future leaks into the past | Expanding window |
| "I used the standings as a feature" | They were today's standings | Rebuild as-of |
| "Accuracy improved 2 points" | Within noise on 380 matches | Compare RPS with a standard error |
| "Great test score, I tuned on it" | Optimistic | Nested walk-forward and a final holdout |

## Output Format

```
Model [name] vs [benchmark], [competition], [first test date] to [last], n = [matches]
RPS: model [x.xxx], benchmark [x.xxx], difference [x.xxx] +/- [se]
Log loss: model [x.xxx], benchmark [x.xxx]
Leakage checks passed: [list]
Verdict: [better / indistinguishable / worse], with the sample caveat
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Model competitive with market | Check calibration | `probability-calibration` |
| Model clearly worse | Go back to features | `poisson-score-models`, `elo-ratings` |
xg-analysis10.7 KB

View saved version →

---
name: xg-analysis
description: "Soccer expected goals analysis: shot maps for a match, club, defence or player, zone breakdowns, xG tables with xG difference and expected points, finishing over- and under-performance, and what a shot's location and body part say about its quality. Use when the user asks about xG, shot maps, shot quality, who is lucky or unlucky, an xG table, or expected points. Do not use for building your own xG model from scratch, that needs a separate dataset pipeline. Do not use for outcome prices, see odds-explorer. Do not use for goalscorer pricing, see goalscorer-prop-modeling."
metadata:
  version: 1.0.0
  author: StrikerAPI
---

# xG Analysis

> **Default data tool:** StrikerAPI (`strikerapi-tool`).
> `get_shot_map` (10 credits) for located shots with xG. `get_team_stats` (5) for season xG for and against, all clubs in one call. `get_leaders` with `stat` set to `xg` (5) for player rankings. `get_events` with `kind: "shots"` (10) for one match in event order.

Expected goals (xG) gives each shot a probability of being a goal from where it was taken, with what body part, and in what situation. Summed over a match or a season it describes the chances a team created and allowed, which is a steadier signal than the scoreline. The xG in StrikerAPI is our own, computed from ESPN-charted located shots from 2012-13 where located shots exist, and it is `null` where they do not.

## When to Use

- "Show me Arsenal's shot map"
- "Which clubs allowed the most xG?"
- "Build an xG table" / "Who is overperforming?"
- "Is this striker finishing or just lucky?"
- "Expected points for the season"

## When NOT to Use

- Training a new xG model: you need shot features plus freeze-frame or event context that this API does not expose; use the located shots as a baseline only
- Match prices: `odds-explorer`
- Player rates and minutes: `player-scouting`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_shot_map` | Shots for a `team`, a `player`, or one `game_id`, for a `season`. `against: true` with `team` returns shots conceded. `include_shots: false` returns totals and zones only | 10 |
| `get_team_stats` | Per-match `xg_for`, `xg_against`, shots, shots on target for every club (omit `team`) | 5 |
| `get_leaders` | Season leaders by `stat: "xg"`, with `per90` and `min_games` | 5 |
| `get_events` | Match events; `kind: "shots"` gives each shot with `xg`, `zone`, `body_part`, `x`, `y` | 10 |
| `get_game` | Box score with `xg` per team (5) | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_xg`, `get_expected_goals` | `get_shot_map`, `get_team_stats` |
| `get_xg_table` | Build it from `get_team_stats` (below) |
| `get_xa`, `get_xgot` | Not available. No expected assists and no post-shot xG |
| `get_pass_map`, `get_heatmap`, `get_pressing` | Not available. Only shots carry coordinates |

## Shot Map Fields

```
get_shot_map {"team": "ARS", "season": 2025}
```

The response has `totals` (shots, located, goals, on_target, xg, xg_per_shot), `zones`, `unlocated_shots`, and a `shots` array, newest first, up to 2,000. Each shot:

| Field | Meaning |
|-------|---------|
| `game_id`, `date`, `minute`, `team`, `player_id`, `player` | Context |
| `outcome` | goal, saved, missed, blocked, woodwork |
| `body_part` | right_foot, left_foot, header |
| `penalty` | True for penalties |
| `zone` | six_yard_box, penalty_area, outside_box, penalty |
| `xg` | The shot's probability of scoring |
| `x`, `y` | Location. `x` is the distance from the goal line as a share of half the pitch (the penalty spot is 0.23). `y` runs across the pitch, 0.5 is the centre |
| `end_x`, `end_y` | Where the shot ended |
| `distance_m` | Distance to goal in metres |

Events from `get_events` also carry `metres.from_goal_line` and `metres.from_centre`, which are easier to plot on a real pitch.

Unlocated shots are counted in `shots_total` but have no xG. In the Arsenal 2025 map, 538 shots included 520 located and 18 unlocated, so xG totals understate slightly.

## How It Works

### A: Profile one club (attack and defence)

```
get_shot_map {"team": "ARS", "season": 2025, "include_shots": false}
get_shot_map {"team": "ARS", "season": 2025, "against": true, "include_shots": false}
```

Compare zone shares and xG per shot for and against. A defence that concedes mostly from outside the box is doing something different from one that concedes few shots of high quality.

### B: Build an xG table (5 credits)

```
get_team_stats {"season": 2025}
```

For each club: `xGD = xg_for - xg_against` per match, multiply by `games` for the season total. Sort by xGD, then show actual goal difference, points (from `get_standings`) and the gap.

### C: Expected points (match-level, about 200 credits per league season)

Expected points need match-by-match shot lists, not season averages, because the draw and win chances depend on the distribution of goals, not the mean. Pull every club's shots for the season (one `get_shot_map` per club, 10 each) and group by `game_id`.

```python
def goal_dist(xgs, max_goals=10):
    dist = [1.0] + [0.0] * max_goals
    for p in xgs:
        for k in range(max_goals, 0, -1):
            dist[k] = dist[k] * (1 - p) + dist[k - 1] * p
        dist[0] *= 1 - p
    return dist

def match_xpts(home_xgs, away_xgs):
    h, a = goal_dist(home_xgs), goal_dist(away_xgs)
    p_home = sum(h[i] * a[j] for i in range(len(h)) for j in range(len(a)) if i > j)
    p_draw = sum(h[i] * a[i] for i in range(len(h)))
    p_away = sum(h[i] * a[j] for i in range(len(h)) for j in range(len(a)) if i < j)
    return 3 * p_home + p_draw, 3 * p_away + p_draw
```

`goal_dist` is a Poisson-binomial: each shot is an independent Bernoulli trial with its own `xg`. Do not use total xG as a Poisson mean for this: a sum of shot-sized Bernoulli trials has less variance than a Poisson with the same mean, so the goal distribution comes out wrong. Exclude penalties and shootout kicks from open-play analysis or label them. Season expected points are the sum across matches.

### D: Finishing (player or club)

```
goals_minus_xg = goals - sum(xg)          standard error ~ sqrt(sum(xg * (1 - xg)))
```

For a club on 60 xG the standard error is about 7 goals. Report the gap with that range.

## Worked Example

One match, both clubs: `get_shot_map {"game_id": "740968"}` (Crystal Palace 1, Arsenal 2):

```
shots_total 25   located 25   goals 3   on target 10   xg 3.29   xg/shot 0.132
zone           shots  goals  xg
six_yard_box     2      0    0.59
penalty_area    21      3    2.64
outside_box      2      0    0.06
penalty          0      0    0.00
```

Splitting the `shots` array by `team`: Crystal Palace 8 shots, 0.86 xG; Arsenal 17 shots, 2.43 xG (the box score in `get_game` shows 2.42, a rounding difference). Running both lists through `match_xpts`:

```
Crystal Palace 0.43 expected points    Arsenal 2.41 expected points
P(home win) 0.09   P(draw) 0.16   P(away win) 0.75
```

Arsenal won, which was the likeliest single outcome. Note that 84% of shots came from the penalty area and none were penalties.

Season level, `get_team_stats {"season": 2025}`: Arsenal 1.90 xG for and 0.83 against per match over 38 matches (xGD +1.07 per match, about +40.7 over the season) against Manchester City's +0.73 and Burnley's -1.18. The season shot map for Arsenal: 538 shots, 67 goals, 72.14 xG. Goals minus xG is about -5, within one standard error (about 8).

Both sides of the season for Arsenal, with `include_shots: false` (totals and zones only, no shots array):

```
                 shots  located  goals  xG      xG/shot   six-yard  box    outside  penalties
for (2025)        538     520     67    72.14   0.139     26.80     35.57  6.52     3.25 (4 shots)
against (2025)    302     297     26    31.63   0.106      8.50     18.60  4.53     0.00
```

Arsenal took 1.8 times as many shots as they allowed, and each shot was worth about a third more. The four penalties carried 3.25 xG, about 0.81 each, so 4.5% of the club's xG came from the spot; strip them before judging open-play finishing. Goals against (26) sat 5.6 below xG against (31.63), which is a defence over-performing or a goalkeeper having a very good year; with this sample size, do not tell the two apart.

Season leaders, October 2026, `get_leaders {"stat": "xg", "per90": true}` returned an empty list because the default needs 10 matches. See `player-scouting` for the minutes trap.

## Pitfalls

1. **Single-match xG is noisy.** A 2.4 versus 0.9 match result says Arsenal created the better chances, not that they are 2.5 times better. Use 10+ matches for a club view.
2. **Game state.** Teams that are ahead tend to shoot less, and from worse positions. xG totals are biased toward the team that scored first. Do not read the trailing team's volume as quality.
3. **Penalties** have their own `penalty` zone in `zones` and `penalty: true` on the shot. A club with many penalties looks like an overperformer in goals versus open-play xG. Subtract them from both sides when judging finishing.
4. **Blocked and unlocated shots.** Located shots only count toward `xg`. Compare club to club only within one league and season, because charting coverage varies.
5. **Own goals** are not shots; they sit in goals but not in xG.
6. **Do not mix** the xG of this API with another provider's. Models differ and so do scales.
7. **Regress finishing gaps.** Shrink an observed goals-minus-xG gap heavily toward zero. Even for strikers, most of a one-season gap is luck.

## Anti-patterns

| Rationalization | Why It's Wrong | Do This Instead |
|----------------|---------------|-----------------|
| "xG says they deserved to win" | One match is a sample of about 12 shots | Say who created more, with the range |
| "Use total xG as a Poisson mean to get win odds" | Wrong variance for a sum of shots | Use the shot-level Poisson-binomial |
| "Call `get_shot_map` once per match" | 10 credits each | One call per club-season returns up to 2,000 shots |
| "Compare Premier League and MLS xG per shot" | Different charting and defences | Compare within a competition |
| "He outscored his xG, buy" | Mostly noise | Show the standard error |

## Output Format

```
[Club/Player] - [competition] [season], [n] shots ([located] located)
xG [x.x], goals [g], gap [+/-x.x] (SE about [s])
Zones: six-yard [n] shots / [xg] xG; box [n] / [xg]; outside [n] / [xg]; penalties [n]
Against: xG per match [x.xx] vs for [x.xx]
Verdict: [one sentence with the sample-size caveat]
```

## What to Do Next

| What You Found | Next Action | Skill |
|----------------|-------------|-------|
| Strong or weak xG difference | Turn it into a score model | `poisson-score-models` |
| Player with a high xG share | Scout the rate | `player-scouting` |
| A scorer with strong chances in a covered league | Price his goal chance | `goalscorer-prop-modeling` |

Release notes

Initial release. Read-only soccer data tools with OAuth sign-in and 14 workflow skills.

Declared in the saved package. Remote tools may change independently.

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package license
MIT
Package author
StrikerAPI
Keywords
See keywords
Commerce declaration
Does not support commerceThis does not establish whether access is free or paid.
Review scenarios
5 positive · 3 negativeScenario counts, not test results.

Declared capabilities

  • Look up clubs, players and matches across many leagues
  • Read league tables, schedules and results
  • Read lineups, match events and shot maps
  • Read expected goals and player leaders
  • Read sportsbook odds and prop lines (information only)

Package observed Nov 3, 2026.

Technical details
First seen
Oct 30, 2026 · 00:00 UTC
Last seen
Nov 4, 2026 · 00:00 UTC
Collection status
Collected

plugin_asdk_app_6ad146628be48191826f84406ff2c4c1

Download plugin data (JSON)