Python SDK
Das offizielle sharpapi Python-Paket bietet typisierten Zugriff auf alle SharpAPI-Endpunkte mit Pydantic-Modellen, SSE-Streaming und vollständiger IDE-Autovervollständigung.
Installation
pip install sharpapiMit optionaler pandas-Unterstützung:
pip install sharpapi[pandas]Erfordert Python 3.9+.
Schnellstart
from sharpapi import SharpAPI
client = SharpAPI("sk_live_xxx")
# Arbitrage-Gelegenheiten
arbs = client.arbitrage.get(min_profit=1.0, league="nba")
for arb in arbs.data:
print(f"{arb.profit_percent:.2f}% profit — {arb.event_name}")
for leg in arb.legs:
print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} ({leg.stake_percent:.1f}%)")
# +EV-Gelegenheiten
evs = client.ev.get(min_ev=3.0, sport="basketball")
for opp in evs.data:
print(f"+{opp.ev_percentage:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
if opp.kelly_percent:
print(f" Kelly: {opp.kelly_percent:.2f}%, Confidence: {opp.confidence_score}")
# Beste Quoten über alle Sportsbooks
odds = client.odds.best(league="nba", market="moneyline")
for line in odds.data:
print(f"{line.home_team} vs {line.away_team}: {line.selection} {line.odds_american}")Ressourcen
Odds
# Vollständiger Snapshot mit Filtern
client.odds.get(sport="basketball", league="nba", limit=100)
# Beste Quoten pro Auswahl über alle Sportsbooks
client.odds.best(league="nfl", market="moneyline")
# Direkter Vergleich für ein Event
client.odds.comparison(event_id="evt_abc123")
# Batch-Abfrage
client.odds.batch(event_ids=["evt_abc123", "evt_def456"])+EV-Gelegenheiten (Pro+)
evs = client.ev.get(
min_ev=2.0,
sportsbook="draftkings",
league="nba",
sort="-ev", # Highest EV first
max_odds_age=60, # Only fresh odds
limit=50,
)
for opp in evs.data:
print(f"+{opp.ev_percentage:.1f}% on {opp.selection} @ {opp.sportsbook}")
print(f" Fair probability: {opp.fair_probability}")
print(f" Devig: {opp.devig_method} via {opp.sharp_book}")
print(f" Kelly: {opp.kelly_percent:.2f}%")
print(f" Confidence: {opp.confidence_score}/100")Arbitrage (Hobby+)
arbs = client.arbitrage.get(
min_profit=0.5,
sport="basketball",
group="best", # One per event+market
max_odds_age=30, # Only fresh odds
)
for arb in arbs.data:
if arb.possibly_stale:
continue # Skip stale opportunities
print(f"{arb.profit_percent:.2f}% — {arb.event_name}")
if arb.game_state:
gs = arb.game_state
print(f" Live: {gs.period} {gs.clock} ({gs.score_home}-{gs.score_away})")
for leg in arb.legs:
print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} → stake {leg.stake_percent:.1f}%")Middles (Pro+)
middles = client.middles.get(sport="football", min_size=3.0, sort="quality")
for mid in middles.data:
print(f"{mid.event_name} — gap: {mid.middle_size} pts")
if mid.side1 and mid.side2:
print(f" {mid.side1.book}: {mid.side1.selection} {mid.side1.line} @ {mid.side1.odds.american}")
print(f" {mid.side2.book}: {mid.side2.selection} +{mid.side2.line} @ {mid.side2.odds.american}")
print(f" Hit probability: {mid.middle_probability:.1%}")
print(f" Expected value: ${mid.expected_value:.2f}")
print(f" Key numbers: {mid.key_numbers}")Low Hold
low_holds = client.low_hold.get(max_hold=2.0, sport="basketball")
for lh in low_holds.data:
print(f"{lh.event_name}: {lh.hold_percentage:.2f}% hold")Referenzdaten
client.sports.list()
client.leagues.list(sport="basketball")
client.sportsbooks.list()
client.events.list(league="nba", live=True)
client.events.search("Lakers")Konto
info = client.account.me()
print(f"Tier: {info.key['tier']}")
print(f"Rate limit: {info.limits.requests_per_minute} req/min")
print(f"Features: EV={info.features.ev}, Arb={info.features.arbitrage}")SSE-Streaming
Echtzeit-Streaming mit Handler-basierten oder Iterator-basierten Mustern.
Wichtig: odds:update-Ereignisse sind Deltas — sie enthalten nur Quoten, die sich geändert haben. Ihr Client muss den lokalen Zustand verwalten und Aktualisierungen in diesen einfügen. Wenn jedes Ereignis als vollständiger Snapshot behandelt wird, entstehen fehlerhafte Daten.
Handler-basiert (Decorator-Muster)
stream = client.stream.opportunities(league="nba", min_ev=3.0)
@stream.on("ev:detected")
def on_ev(data):
# data is {"opportunities": [...], "count": N, "type": "ev"}. An item is new,
# or an updated version of one already sent (same id)
for opp in data["opportunities"]:
if not opp.get("possibly_stale"):
print(f"+EV: {opp['selection']} {opp['ev_percentage']}% @ {opp['sportsbook']}")
@stream.on("arb:detected")
def on_arb(data):
for arb in data["opportunities"]:
print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}")
@stream.on("snapshot:complete")
def on_ready(data):
print(f"Ready: snapshot complete at {data['timestamp']}")
stream.connect() # Blocks, processing eventsIterator-basiert
stream = client.stream.all(sport="basketball")
for event_type, data in stream.iter_events():
if event_type == "ev:detected":
for opp in data["opportunities"]:
print(f"+EV: {opp['ev_percentage']}%")
elif event_type == "arb:detected":
for arb in data["opportunities"]:
print(f"Arb: {arb['profit_percent']}%")
elif event_type == "snapshot:complete":
print("Stream ready")Vollständige Zustandsverwaltung
Der all-Kanal überträgt dieselben Odds-Frames wie der odds-Kanal: snapshot-Chunks und odds:update-Deltas halten ihre Zeilen jeweils in einem odds-Array, nie nach Sportsbook strukturiert: Jede snapshot-Zeile und jede vollständig gesendete odds:update-Zeile enthält ihr sportsbook; eine kompakte odds:update-Zeile enthält es nicht, daher lesen Sie das Sportsbook aus dem book-Feld des Envelopes. Auf all erhalten Sie zusätzlich snapshot-Chunks mit Gelegenheiten — unter ev, arbitrage, middles, low_hold — sowie, falls Ihr Plan Game-State einschließt, gamestate:snapshot, was dieses Beispiel ignoriert. Alle Frames sind in der Stream-Endpoint-Referenz aufgeführt.
odds_map = {} # Keyed by odds line ID
resuming = False # True zwischen einem fortgesetzten Connect und dem Frame, der ihn klärt
stream = client.stream.all(league="nba")
@stream.on("connected")
def on_connected(data):
# "resumed": True (nur odds-Kanal) heißt, dass das Replay BEGONNEN hat, nicht
# dass es beendet ist — der Server kann noch auf einen vollständigen Snapshot
# zurückfallen, also hier nicht löschen. Sonst folgt ein vollständiger
# Snapshot: jetzt löschen.
global resuming # eine Modulvariable im Handler neu zu binden braucht `global`
resuming = data.get("resumed") is True
if not resuming:
odds_map.clear()
@stream.on("snapshot")
def on_snapshot(data):
global resuming
if resuming:
# Der Server hat die Fortsetzung nach den wiederholten Frames abgebrochen:
# dieser Snapshot ist die neue Baseline (snapshot:complete meldet "full_resync").
odds_map.clear()
resuming = False
# Odds-Zeilen kommen unter "odds", hier und in odds:update; ein Chunk mit
# Gelegenheiten enthält stattdessen "ev" / "arbitrage" / "middles" / "low_hold"
# und wird übersprungen.
for odds in data.get("odds", []):
odds_map[odds["id"]] = odds
@stream.on("snapshot:complete")
def on_snapshot_complete(data):
# Ein full_resync ohne passende Zeilen sendet gar keine Chunks — also auch hier löschen.
global resuming
if data.get("mode") == "full_resync" and resuming:
odds_map.clear()
resuming = False
@stream.on("odds:update")
def on_update(data):
# DELTA — jede Zeile in die gehaltene einpflegen. Ein Delta enthält nur die
# Felder, die sich ändern können; die gespeicherte Zeile zu ersetzen, verliert den Rest.
for delta in data.get("odds", []):
row = odds_map.get(delta["id"])
if row is not None:
row.update(delta)
elif "sportsbook" in delta:
# Eine Linie, die nach Ihrem Snapshot eröffnet wurde, kommt als vollständige Zeile
odds_map[delta["id"]] = delta
@stream.on("odds:removed")
def on_removed(data):
for odds_id in data.get("ids", []):
odds_map.pop(odds_id, None)
stream.connect()Stream-Kanäle
# Nur Quoten
client.stream.odds(league="nba")
# Nur Gelegenheiten (EV + Arb + Middles)
client.stream.opportunities(min_ev=3.0)
# Alles
client.stream.all(sport="basketball")
# Einzelnes Event
client.stream.event("evt_abc123")Datenqualität
Jede Gelegenheit enthält Metadaten zur Veraltung:
arbs = client.arbitrage.get()
for arb in arbs.data:
# Veraltete oder verdächtige Gelegenheiten überspringen
if arb.possibly_stale:
print(f"Stale ({arb.oldest_odds_age_seconds}s old)")
continue
if "LIVE_HIGH_PROFIT_SUSPICIOUS" in arb.warnings:
print("Phantom arb — skipping")
continue
print(f"Actionable: {arb.profit_percent}%")Rate Limits
Informationen zu Rate Limits sind nach jeder Anfrage verfügbar:
response = client.odds.get()
rl = client.rate_limit
print(f"{rl.remaining}/{rl.limit} requests remaining (tier: {rl.tier})")Fehlerbehandlung
from sharpapi import (
SharpAPI,
AuthenticationError,
TierRestrictedError,
RateLimitedError,
)
client = SharpAPI("sk_live_xxx")
try:
evs = client.ev.get()
except AuthenticationError:
print("Invalid API key")
except TierRestrictedError as e:
print(f"Upgrade to {e.required_tier} for this feature")
except RateLimitedError as e:
print(f"Rate limited — retry after {e.retry_after}s")Hilfsfunktionen zur Quotenumrechnung
from sharpapi import american_to_decimal, american_to_probability, decimal_to_american
american_to_decimal(-110) # 1.909
american_to_decimal(150) # 2.5
american_to_probability(-110) # 0.524
decimal_to_american(2.5) # 150Context Manager
with SharpAPI("sk_live_xxx") as client:
arbs = client.arbitrage.get()
# HTTP-Client wird beim Beenden automatisch geschlossenTypsicherheit
Alle Antworten verwenden Pydantic-Modelle mit vollständigen Typhinweisen:
from sharpapi import EVOpportunity, ArbitrageOpportunity
# IDE-Autovervollständigung funktioniert für alle Felder
arbs = client.arbitrage.get()
arb: ArbitrageOpportunity = arbs.data[0]
arb.profit_percent # float
arb.legs[0].sportsbook # str
arb.game_state.period # str | None