Initial commit: Arbitrage
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph external [External]
|
||||
API[The Odds API]
|
||||
end
|
||||
|
||||
subgraph repo [Repository]
|
||||
Fetch[odds/scripts/fetch_odds.py]
|
||||
Samples[(odds/data/samples/*.json)]
|
||||
Arb[Arbitrage engine - planned]
|
||||
end
|
||||
|
||||
API --> Fetch --> Samples
|
||||
Samples --> Arb
|
||||
```
|
||||
|
||||
## Odds pipeline (current and planned)
|
||||
|
||||
### Current
|
||||
|
||||
1. Operator sets `ODDS_API_KEY` in `.env`
|
||||
2. `fetch_odds.py` calls `GET /v4/sports/{sport_key}/odds`
|
||||
3. Response JSON is stored under `odds/data/samples/`
|
||||
|
||||
### Planned
|
||||
|
||||
| Stage | Responsibility |
|
||||
|-------|----------------|
|
||||
| Loader | Read JSON snapshots or live API |
|
||||
| Normalizer | One row per (event, bookmaker, market, outcome) |
|
||||
| Arb scanner | Compare implied probabilities across books |
|
||||
| Reporter | CLI or export of opportunities above margin threshold |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Used by | Description |
|
||||
|----------|---------|-------------|
|
||||
| `ODDS_API_KEY` | `fetch_odds.py` | The Odds API authentication |
|
||||
| `ODDS_REGIONS` | (optional future) | Default regions for fetch |
|
||||
| `ODDS_MARKETS` | (optional future) | Default markets for fetch |
|
||||
|
||||
## Dependencies
|
||||
|
||||
See root `requirements.txt`. Odds fetch uses `requests`.
|
||||
|
||||
## Out of scope for git
|
||||
|
||||
- `arb-venv/`, `.venv/` — local virtualenvs
|
||||
- `.env` — secrets
|
||||
@@ -0,0 +1,102 @@
|
||||
# Odds data schema
|
||||
|
||||
Sample file: `odds/data/samples/boxing_odds.json`
|
||||
|
||||
Source: [The Odds API](https://the-odds-api.com/) v4 — `GET /sports/{sport}/odds`
|
||||
|
||||
## Top level
|
||||
|
||||
The response is a **JSON array** of event objects.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "7605c958b8ffbe29c0dcb81e4d2c8a10",
|
||||
"sport_key": "boxing_boxing",
|
||||
"sport_title": "Boxing",
|
||||
"commence_time": "2025-05-10T16:00:00Z",
|
||||
"home_team": "Charlie Senior",
|
||||
"away_team": "Cesar Ignacio Paredes",
|
||||
"bookmakers": [ ... ]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Event fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string | Unique event id |
|
||||
| `sport_key` | string | API sport identifier |
|
||||
| `sport_title` | string | Human-readable sport name |
|
||||
| `commence_time` | ISO 8601 UTC | Scheduled start |
|
||||
| `home_team` | string | Home / first fighter name |
|
||||
| `away_team` | string | Away / second fighter name |
|
||||
| `bookmakers` | array | List of books offering lines on this event |
|
||||
|
||||
## Bookmaker object
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `key` | string | Bookmaker slug (e.g. `unibet_uk`) |
|
||||
| `title` | string | Display name |
|
||||
| `last_update` | ISO 8601 | When this book’s lines were updated |
|
||||
| `link` | string \| null | Deep link to event on book site |
|
||||
| `sid` | string \| null | Book-specific event id |
|
||||
| `markets` | array | Markets offered for this event |
|
||||
|
||||
## Market object
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `key` | string | Market type: `h2h` (moneyline), `h2h_lay` (exchange lay), etc. |
|
||||
| `last_update` | ISO 8601 | Market update time |
|
||||
| `outcomes` | array | Selections and prices |
|
||||
|
||||
## Outcome object
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Fighter name or `Draw` |
|
||||
| `price` | number | **Decimal** odds (European format) |
|
||||
| `link` | string \| null | Selection deep link |
|
||||
| `sid` | string \| null | Selection id at book |
|
||||
| `bet_limit` | number \| null | Max stake if provided |
|
||||
|
||||
## Implied probability (for arbitrage)
|
||||
|
||||
For decimal odds `d`:
|
||||
|
||||
```
|
||||
implied_probability = 1 / d
|
||||
```
|
||||
|
||||
For a **single** book’s full market (all mutually exclusive outcomes), sum of implied probabilities > 1 indicates book margin (overround).
|
||||
|
||||
**Cross-book arbitrage** (simplified): for each outcome, take the **best** (highest) decimal odds across books, then:
|
||||
|
||||
```
|
||||
arb_sum = sum(1 / best_odds_i for each outcome i)
|
||||
```
|
||||
|
||||
If `arb_sum < 1`, a risk-free profit is theoretically possible before fees and limits.
|
||||
|
||||
## Normalized row (recommended for future code)
|
||||
|
||||
| Column | Example |
|
||||
|--------|---------|
|
||||
| `event_id` | `7605c958...` |
|
||||
| `commence_time` | `2025-05-10T16:00:00Z` |
|
||||
| `home_team` | `Charlie Senior` |
|
||||
| `away_team` | `Cesar Ignacio Paredes` |
|
||||
| `bookmaker_key` | `unibet_uk` |
|
||||
| `market_key` | `h2h` |
|
||||
| `outcome_name` | `Charlie Senior` |
|
||||
| `price` | `1.03` |
|
||||
| `implied_prob` | `0.9709` |
|
||||
|
||||
## API usage notes
|
||||
|
||||
- Responses count against your API quota; check response headers `x-requests-remaining`.
|
||||
- Region and market query params filter payload size; sample uses `uk,eu` and `h2h`.
|
||||
- Prices are decimal in this project (`oddsFormat=decimal` in fetch script).
|
||||
Reference in New Issue
Block a user