-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodels.py
More file actions
645 lines (516 loc) · 18.9 KB
/
models.py
File metadata and controls
645 lines (516 loc) · 18.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
"""Pydantic models for SharpAPI responses."""
from __future__ import annotations
from typing import Any, Generic, TypeVar
from pydantic import AliasChoices, BaseModel, Field
T = TypeVar("T")
# =============================================================================
# Common
# =============================================================================
class OddsValue(BaseModel):
"""Odds in multiple formats."""
american: int | float
decimal: float
probability: float
# =============================================================================
# Nested reference objects
# =============================================================================
#
# These structured ref objects ship alongside the legacy flat fields on every
# odds row, opportunity row, and reference-list row. All fields are optional
# and additive — clients on older API versions simply receive ``None``.
#
# Wire format uses snake_case (``sport_ref``, ``league_ref``, ``market_ref``,
# ``sportsbook_ref``) which Python attribute names match directly.
class TeamRef(BaseModel):
"""Structured team reference attached to ``home`` / ``away``.
``abbreviation`` is only present for ~1500 team-sport entities; absent
for individual-sport competitors (tennis players, MMA fighters, etc).
Optional metadata fields (``logo``, ``city``, ``mascot``, ``conference``,
``division``) are populated for the majority of major-league teams
(~93% coverage on ``logo``, similar on the rest). Unmapped rows simply
leave the field absent rather than emitting null.
"""
id: str | None = None
numerical_id: int | None = None
name: str | None = None
abbreviation: str | None = None
logo: str | None = None
city: str | None = None
mascot: str | None = None
conference: str | None = None
division: str | None = None
model_config = {"extra": "allow"}
class SportRef(BaseModel):
"""Structured sport reference attached to ``sport_ref``."""
id: str | None = None
name: str | None = None
numerical_id: int | None = None
model_config = {"extra": "allow"}
class EntityRef(BaseModel):
"""Structured reference for league / market / sportsbook objects.
Used by ``league_ref``, ``market_ref``, and ``sportsbook_ref`` on
every odds, opportunity, and reference row.
"""
id: str | None = None
label: str | None = None
numerical_id: int | None = None
model_config = {"extra": "allow"}
class Pagination(BaseModel):
limit: int
offset: int
has_more: bool
next_offset: int | None = None
total: int | None = None
class ResponseMeta(BaseModel):
"""Metadata returned with API responses."""
count: int | None = None
total: int | None = None
pagination: Pagination | None = None
updated: str | None = None
source: str | None = None
last_update: str | None = None
data_age_seconds: float | None = None
filters: dict[str, Any] | None = None
summary: dict[str, Any] | None = None
books_analyzed: int | None = None
class APIResponse(BaseModel, Generic[T]):
"""Standard API response wrapper."""
success: bool | None = None
data: T
meta: ResponseMeta | None = None
timestamp: str | None = None
tier: str | None = None
def to_dataframe(self, flatten: bool = True):
"""Convert response data to a pandas DataFrame.
Requires ``pip install sharpapi[pandas]``.
Args:
flatten: If True (default), flatten nested objects into
underscore-joined columns. Nested lists (like ``legs``)
remain as-is.
Returns:
pandas.DataFrame with one row per item in ``data``.
"""
try:
import pandas as pd
except ImportError:
raise ImportError(
"pandas is required for to_dataframe(). "
"Install it with: pip install sharpapi[pandas]"
) from None
data = self.data
if not data:
return pd.DataFrame()
if not isinstance(data, list):
data = [data]
rows = []
for item in data:
if isinstance(item, BaseModel):
row = item.model_dump()
else:
row = dict(item) if isinstance(item, dict) else {"value": item}
if flatten:
row = _flatten_dict(row)
rows.append(row)
return pd.DataFrame(rows)
def _flatten_dict(d: dict, parent_key: str = "", sep: str = "_") -> dict:
"""Flatten nested dicts, skip lists."""
items: list[tuple[str, Any]] = []
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(_flatten_dict(v, new_key, sep).items())
else:
items.append((new_key, v))
return dict(items)
class GameState(BaseModel):
"""Live game state for a single event, merged across sportsbooks.
Returned by ``/api/v1/gamestate`` and the ``gamestate`` stream channel.
Scores are consensus-merged with period-guarded outlier rejection;
period/clock are picked from the most-advanced book. Not present on
EV / arb / low-hold opportunity rows — correlate by ``event_id``.
``extra="allow"`` lets adapter-specific fields pass through unchanged.
"""
home_score: int | None = None
away_score: int | None = None
game_period: str | None = None
game_clock: str | None = None
home_team: str | None = None
away_team: str | None = None
sport: str | None = None
primary_book: str | None = None
book_count: int | None = None
stale: bool = False
aggregator_stale: bool = False
model_config = {"extra": "allow"}
# =============================================================================
# Odds
# =============================================================================
class OddsLine(BaseModel):
"""A single odds line from a sportsbook."""
id: str
sportsbook: str
sportsbook_name: str | None = None
event_id: str
sport: str
league: str
home_team: str
away_team: str
market_type: str
selection: str
selection_type: str | None = None
odds_american: int | float
odds_decimal: float
probability: float
line: float | None = None
event_start_time: str | None = None
timestamp: str | None = None
is_live: bool = False
deep_link: str | None = None
player_name: str | None = None
stat_category: str | None = None
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
market_ref: EntityRef | None = None
sportsbook_ref: EntityRef | None = None
# =============================================================================
# EV Opportunities
# =============================================================================
class EVOpportunity(BaseModel):
"""A positive expected value (+EV) opportunity."""
id: str
event_id: str | None = Field(None, alias="game_id")
event_name: str | None = Field(None, alias="game")
sport: str
league: str
market_type: str | None = Field(None, alias="market")
selection: str
sportsbook: str
odds_american: int | float
odds_decimal: float
no_vig_odds: float | None = None
fair_probability: float | None = Field(
None, validation_alias=AliasChoices("fair_probability", "true_probability")
)
ev_percentage: float = Field(
validation_alias=AliasChoices("ev_percentage", "ev_percent")
)
kelly_percent: float | None = Field(
None, validation_alias=AliasChoices("kelly_percent", "kelly_fraction")
)
confidence_score: float | None = None
book_count: int | None = None
market_width: float | None = None
devig_method: str | None = None
sharp_book: str | None = Field(
None, validation_alias=AliasChoices("sharp_book", "devig_book")
)
sharp_odds_american: int | float | None = None
sharp_odds_decimal: float | None = None
line: float | None = None
home_team: str | None = None
away_team: str | None = None
start_time: str | None = None
is_live: bool = False
arb_available: bool | None = None
arb_profit: float | None = None
is_player_prop: bool = False
player_name: str | None = None
stat_category: str | None = None
possibly_stale: bool = False
oldest_odds_age_seconds: float | None = None
warnings: list[str] = Field(default_factory=list)
detected_at: str | None = None
external_event_id: str | None = None
selection_id: str | None = None
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
market_ref: EntityRef | None = None
sportsbook_ref: EntityRef | None = None
model_config = {"populate_by_name": True}
# =============================================================================
# Arbitrage Opportunities
# =============================================================================
class ArbitrageLeg(BaseModel):
"""One leg of an arbitrage opportunity."""
sportsbook: str
selection: str
odds_american: int | float
odds_decimal: float
implied_probability: float | None = None
stake_percent: float
timestamp: str | None = None
external_event_id: str | None = None
selection_id: str | None = None
market_id: str | None = None
# Optional structured book ref on each leg.
sportsbook_ref: EntityRef | None = None
class ArbitrageOpportunity(BaseModel):
"""A guaranteed-profit arbitrage opportunity."""
id: str
event_id: str | None = None
event_name: str
sport: str
league: str | None = None
market_type: str
line: float | None = None
profit_percent: float
implied_total: float | None = None
estimated_net_profit_percent: float | None = None
start_time: str | None = None
is_live: bool = False
is_alternate_line: bool = False
possibly_stale: bool = False
oldest_odds_age_seconds: float | None = None
warnings: list[str] = Field(default_factory=list)
ev_available: bool | None = None
ev_percentage: float | None = None
is_player_prop: bool = False
player_name: str | None = None
stat_category: str | None = None
legs: list[ArbitrageLeg]
detected_at: str | None = None
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
market_ref: EntityRef | None = None
# =============================================================================
# Middle Opportunities
# =============================================================================
class MiddleSide(BaseModel):
"""One side of a middle opportunity."""
book: str
selection: str
line: float
odds: OddsValue
stake_percent: float | None = None
odds_age_seconds: float | None = None
deep_link: str | None = None
class MiddleOpportunity(BaseModel):
"""A middle opportunity where both sides can win."""
id: str
event_id: str | None = None
event_name: str
sport: str
league: str | None = None
market_type: str
home_team: str | None = None
away_team: str | None = None
start_time: str | None = None
side1: MiddleSide | None = None
side2: MiddleSide | None = None
middle_size: float | None = None
middle_numbers: list[int] | None = None
middle_probability: float | None = None
expected_value: float | None = None
roi_percentage: float | None = None
worst_case_loss: float | None = None
best_case_profit: float | None = None
break_even_percent: float | None = None
is_guaranteed_profit: bool = False
guaranteed_roi: float | None = None
key_numbers: list[int] | None = None
key_number_probability: float | None = None
quality_score: float | None = None
market_overround: float | None = None
is_live: bool = False
is_player_prop: bool = False
player_name: str | None = None
stat_category: str | None = None
odds_age_seconds: float | None = None
warnings: list[str] = Field(default_factory=list)
detected_at: str | None = None
# Flat fields (alternative to side1/side2 nesting)
gap_size: float | None = Field(None, alias="gapSize")
potential_profit: float | None = Field(None, alias="potentialProfit")
legs: list[ArbitrageLeg] | None = None
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
market_ref: EntityRef | None = None
model_config = {"populate_by_name": True}
# =============================================================================
# Low Hold
# =============================================================================
class LowHoldSide(BaseModel):
"""One side of a low-hold opportunity."""
selection: str
books: list[str] | None = None
line: float | None = None
odds: OddsValue | None = None
deep_links: dict[str, str] | None = None
class LowHoldOpportunity(BaseModel):
"""A low-hold (low vig) market."""
id: str
event_id: str | None = None
event_name: str
sport: str
league: str | None = None
market_type: str
line: float | None = None
home_team: str | None = None
away_team: str | None = None
start_time: str | None = None
hold_percentage: float
side1: LowHoldSide | None = None
side2: LowHoldSide | None = None
side3: LowHoldSide | None = None
is_live: bool = False
is_alternate_line: bool = False
all_books: list[str] | None = None
confidence: float | None = None
odds_age_seconds: float | None = None
possibly_stale: bool = False
is_player_prop: bool = False
player_name: str | None = None
stat_category: str | None = None
detected_at: str | None = None
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
market_ref: EntityRef | None = None
# =============================================================================
# Reference Data
# =============================================================================
class Sport(BaseModel):
id: str
name: str
slug: str
active: bool
event_count: int | None = None
# Optional integer numerical ID, additive.
numerical_id: int | None = None
class League(BaseModel):
id: str
name: str
slug: str
sport_id: str | None = None
country: str | None = None
active: bool
# Optional integer numerical ID, additive.
numerical_id: int | None = None
class Sportsbook(BaseModel):
id: str
name: str
slug: str
active: bool
regions: list[str] | None = None
features: list[str] | None = None
# Optional integer numerical ID, additive.
numerical_id: int | None = None
class Team(BaseModel):
"""A team / competitor returned by the ``/teams`` reference endpoint.
``abbreviation`` is only present for ~1500 team-sport entities and is
absent for individual-sport competitors.
"""
id: str
name: str | None = None
sport: str | None = None
league: str | None = None
abbreviation: str | None = None
numerical_id: int | None = None
model_config = {"extra": "allow"}
class Event(BaseModel):
id: str
sport: str
league: str
home_team: str
away_team: str
start_time: str | None = None
is_live: bool = False
status: str | None = None
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
class Market(BaseModel):
"""A market available on an event."""
market_type: str
market_label: str | None = None
selection_count: int | None = None
book_count: int | None = None
books: list[str] | None = None
# Optional integer numerical ID, additive.
numerical_id: int | None = None
# =============================================================================
# Closing Snapshot
# =============================================================================
class ClosingOddsLine(BaseModel):
"""A single closing-line odds entry within a closing snapshot."""
sportsbook: str
market_type: str
selection: str
selection_type: str | None = None
odds_american: int | float
odds_decimal: float
line: float | None = None
player_name: str | None = None
stat_category: str | None = None
# Optional structured refs (additive, non-breaking).
market_ref: EntityRef | None = None
sportsbook_ref: EntityRef | None = None
class ClosingSnapshot(BaseModel):
"""Closing-line snapshot for an event, grouped by sportsbook."""
event_id: str
sport: str | None = None
league: str | None = None
home_team: str | None = None
away_team: str | None = None
event_start_time: str | None = None
captured_at: str | None = None
books: dict[str, list[ClosingOddsLine]] = Field(default_factory=dict)
# Optional structured refs (additive, non-breaking).
home: TeamRef | None = None
away: TeamRef | None = None
sport_ref: SportRef | None = None
league_ref: EntityRef | None = None
# =============================================================================
# Account / Keys
# =============================================================================
class APIKey(BaseModel):
"""An API key managed via the /account/keys endpoints."""
id: str
id_masked: str | None = None
# Present only on create/rotate responses (one-time secret).
key: str | None = None
name: str | None = None
tier: str | None = None
is_active: bool | None = None
created_at: str | None = None
updated_at: str | None = None
# =============================================================================
# Account
# =============================================================================
class AccountLimits(BaseModel):
requests_per_minute: int | None = None
max_streams: int | None = None
odds_delay_seconds: int | None = None
max_books: int | None = None
class AccountFeatures(BaseModel):
ev: bool = False
arbitrage: bool = False
middles: bool = False
streaming: bool = False
class AccountInfo(BaseModel):
key: dict[str, Any] | None = None
limits: AccountLimits | None = None
features: AccountFeatures | None = None
add_ons: list[str] | None = None
class RateLimitInfo(BaseModel):
"""Rate limit state from response headers."""
limit: int | None = None
remaining: int | None = None
reset: float | None = None
tier: str | None = None