Initial commit: Arbitrage
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Odds module
|
||||
|
||||
Handles **ingestion and storage** of sports odds from [The Odds API](https://the-odds-api.com/). Arbitrage logic will consume normalized odds from this layer once implemented.
|
||||
|
||||
## Contents
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `data/samples/boxing_odds.json` | Snapshot of boxing `h2h` odds (~1 MB) |
|
||||
| `scripts/fetch_odds.py` | CLI to pull live odds and save JSON |
|
||||
|
||||
Legacy path `api-pull/Odds/boxing_odds.json` may still exist locally; prefer `data/samples/` for new work.
|
||||
|
||||
## Fetching odds
|
||||
|
||||
```bash
|
||||
# From repository root, with ODDS_API_KEY set
|
||||
python odds/scripts/fetch_odds.py \
|
||||
--sport boxing_boxing \
|
||||
--regions uk,eu \
|
||||
--markets h2h \
|
||||
--out odds/data/samples/boxing_odds.json
|
||||
```
|
||||
|
||||
### Script functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `get_api_key()` | Reads `ODDS_API_KEY` from the environment |
|
||||
| `fetch_odds(sport, regions, markets, api_key)` | HTTP GET to `/v4/sports/{sport}/odds` |
|
||||
| `save_odds(data, output_path)` | Writes pretty-printed JSON |
|
||||
| `main()` | Parses CLI args and runs fetch + save |
|
||||
|
||||
## Sample data
|
||||
|
||||
`boxing_odds.json` is an array of **events**. Each event has `home_team`, `away_team`, `commence_time`, and a `bookmakers` list. Each bookmaker exposes `markets` (e.g. `h2h` head-to-head) with `outcomes` and decimal `price` values.
|
||||
|
||||
See [docs/DATA_SCHEMA.md](../docs/DATA_SCHEMA.md) for the full field reference.
|
||||
|
||||
## Next steps (arbitrage)
|
||||
|
||||
Planned pipeline:
|
||||
|
||||
1. **Normalize** — flatten bookmaker/outcome rows per event
|
||||
2. **Implied probability** — `1 / decimal_odds` per outcome
|
||||
3. **Cross-book compare** — find outcomes where sum of best inverse odds < 1 (arb margin)
|
||||
4. **Stake split** — optional Kelly / equal-profit calculators
|
||||
|
||||
None of steps 2–4 exist in code yet; this folder is ingestion-only today.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
# Legacy path
|
||||
|
||||
Sample odds were originally stored here. Use the current path instead:
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `api-pull/Odds/boxing_odds.json` | `odds/data/samples/boxing_odds.json` |
|
||||
|
||||
You may delete `api-pull/` locally after confirming the copy exists under `odds/data/samples/`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Fetch sports odds from The Odds API and save JSON snapshots.
|
||||
|
||||
Requires ODDS_API_KEY in environment or .env (see project .env.example).
|
||||
|
||||
Usage:
|
||||
python odds/scripts/fetch_odds.py --sport boxing_boxing --out odds/data/samples/boxing_odds.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Install dependencies: pip install -r requirements.txt", file=sys.stderr)
|
||||
raise
|
||||
|
||||
API_BASE = "https://api.the-odds-api.com/v4"
|
||||
DEFAULT_SPORT = "boxing_boxing"
|
||||
DEFAULT_REGIONS = "uk,eu"
|
||||
DEFAULT_MARKETS = "h2h"
|
||||
|
||||
|
||||
def get_api_key() -> str:
|
||||
"""Read API key from ODDS_API_KEY environment variable."""
|
||||
key = os.environ.get("ODDS_API_KEY", "").strip()
|
||||
if not key:
|
||||
raise SystemExit(
|
||||
"Missing ODDS_API_KEY. Copy .env.example to .env and set your key."
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
def fetch_odds(
|
||||
sport: str,
|
||||
regions: str,
|
||||
markets: str,
|
||||
api_key: str,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
GET odds for a sport from The Odds API.
|
||||
|
||||
Args:
|
||||
sport: Sport key (e.g. boxing_boxing).
|
||||
regions: Comma-separated region codes.
|
||||
markets: Comma-separated market keys.
|
||||
api_key: The Odds API key.
|
||||
|
||||
Returns:
|
||||
Parsed JSON list of events.
|
||||
|
||||
Raises:
|
||||
requests.HTTPError: On non-2xx response.
|
||||
"""
|
||||
url = f"{API_BASE}/sports/{sport}/odds"
|
||||
params = {
|
||||
"apiKey": api_key,
|
||||
"regions": regions,
|
||||
"markets": markets,
|
||||
"oddsFormat": "decimal",
|
||||
}
|
||||
response = requests.get(url, params=params, timeout=60)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def save_odds(data: list[dict], output_path: Path) -> None:
|
||||
"""Write odds JSON to disk with stable formatting."""
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
print(f"Saved {len(data)} events to {output_path}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Fetch odds from The Odds API")
|
||||
parser.add_argument("--sport", default=DEFAULT_SPORT, help="Sport key")
|
||||
parser.add_argument("--regions", default=DEFAULT_REGIONS, help="Region codes")
|
||||
parser.add_argument("--markets", default=DEFAULT_MARKETS, help="Market keys")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
default=Path("odds/data/samples/boxing_odds.json"),
|
||||
help="Output JSON path",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
api_key = get_api_key()
|
||||
data = fetch_odds(args.sport, args.regions, args.markets, api_key)
|
||||
save_odds(data, args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user