sharpapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: sharpapi
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the SharpAPI real-time sports betting odds API
5
+ Project-URL: Homepage, https://sharpapi.io
6
+ Project-URL: Documentation, https://docs.sharpapi.io/sdks/python
7
+ Project-URL: Repository, https://github.com/sharpapi/sharpapi-python
8
+ Project-URL: Changelog, https://github.com/sharpapi/sharpapi-python/releases
9
+ Author-email: SharpAPI <support@sharpapi.io>
10
+ License-Expression: MIT
11
+ Keywords: api,arbitrage,ev,odds,pinnacle,real-time,sports-betting
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx>=0.25.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Provides-Extra: pandas
27
+ Requires-Dist: pandas>=1.5.0; extra == 'pandas'
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
30
+ Requires-Dist: pytest>=8.0; extra == 'test'
31
+ Requires-Dist: respx>=0.21; extra == 'test'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # SharpAPI Python SDK
35
+
36
+ Official Python client for the [SharpAPI](https://sharpapi.io) real-time sports betting odds API.
37
+
38
+ Get pre-computed +EV opportunities, arbitrage detection, middles, and live odds from 20+ sportsbooks — with Pinnacle as the sharp reference.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install sharpapi
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ from sharpapi import SharpAPI
50
+
51
+ client = SharpAPI("sk_live_xxx")
52
+
53
+ # --- Arbitrage opportunities ---
54
+ arbs = client.arbitrage.get(min_profit=1.0, league="nba")
55
+ for arb in arbs.data:
56
+ print(f"{arb.profit_percent:.2f}% profit — {arb.event_name}")
57
+ for leg in arb.legs:
58
+ print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} ({leg.stake_percent:.1f}%)")
59
+
60
+ # --- +EV opportunities ---
61
+ evs = client.ev.get(min_ev=3.0, sport="basketball")
62
+ for opp in evs.data:
63
+ print(f"+{opp.ev_percent:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
64
+ if opp.kelly_fraction:
65
+ print(f" Kelly: {opp.kelly_fraction:.1%} of bankroll")
66
+
67
+ # --- Best odds across books ---
68
+ odds = client.odds.best(league="nba", market="moneyline")
69
+ for line in odds.data:
70
+ print(f"{line.home_team} vs {line.away_team}: {line.selection} {line.odds_american}")
71
+ ```
72
+
73
+ ## Streaming
74
+
75
+ Real-time SSE streaming for odds updates and opportunity alerts (requires WebSocket add-on):
76
+
77
+ ```python
78
+ stream = client.stream.opportunities(league="nba")
79
+
80
+ @stream.on("ev:detected")
81
+ def on_ev(data):
82
+ for opp in data:
83
+ print(f"+EV: {opp['selection']} {opp['ev_percent']}% @ {opp['sportsbook']}")
84
+
85
+ @stream.on("arb:detected")
86
+ def on_arb(data):
87
+ for arb in data:
88
+ print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}")
89
+
90
+ stream.connect() # Blocks, processing events
91
+ ```
92
+
93
+ Or iterate over events:
94
+
95
+ ```python
96
+ for event_type, data in stream.iter_events():
97
+ if event_type == "ev:detected":
98
+ print(data)
99
+ ```
100
+
101
+ ## All Resources
102
+
103
+ ```python
104
+ # Odds
105
+ client.odds.get(sport="basketball", league="nba")
106
+ client.odds.best(league="nfl", market="moneyline")
107
+ client.odds.comparison(event_id="abc123")
108
+ client.odds.batch(event_ids=["abc123", "def456"])
109
+
110
+ # Opportunities
111
+ client.ev.get(min_ev=2.0, sportsbook="draftkings")
112
+ client.arbitrage.get(min_profit=0.5, sport="football")
113
+ client.middles.get(sport="football", min_size=3.0)
114
+ client.low_hold.get(max_hold=2.0)
115
+
116
+ # Reference data
117
+ client.sports.list()
118
+ client.leagues.list(sport="basketball")
119
+ client.sportsbooks.list()
120
+ client.events.list(league="nba", live=True)
121
+ client.events.search("Lakers")
122
+
123
+ # Account
124
+ client.account.me() # Tier, limits, features
125
+ client.account.usage() # Request counts
126
+
127
+ # Streaming
128
+ client.stream.odds(league="nba")
129
+ client.stream.opportunities(min_ev=3.0)
130
+ client.stream.all(sport="basketball")
131
+ client.stream.event("event_id_here")
132
+ ```
133
+
134
+ ## Data Quality
135
+
136
+ Every opportunity response includes staleness metadata to avoid acting on stale odds:
137
+
138
+ ```python
139
+ arbs = client.arbitrage.get()
140
+ for arb in arbs.data:
141
+ if arb.possibly_stale:
142
+ print(f" Skipping — odds may be stale ({arb.oldest_odds_age_seconds}s old)")
143
+ continue
144
+ if "LIVE_HIGH_PROFIT_SUSPICIOUS" in arb.warnings:
145
+ print(f" Skipping — likely phantom arb")
146
+ continue
147
+ print(f"Actionable: {arb.profit_percent}%")
148
+ ```
149
+
150
+ ## Rate Limits
151
+
152
+ Rate limit info is available after every request:
153
+
154
+ ```python
155
+ response = client.odds.get()
156
+ print(f"Remaining: {client.rate_limit.remaining}/{client.rate_limit.limit}")
157
+ print(f"Tier: {client.rate_limit.tier}")
158
+ ```
159
+
160
+ ## Error Handling
161
+
162
+ ```python
163
+ from sharpapi import (
164
+ SharpAPI,
165
+ AuthenticationError,
166
+ TierRestrictedError,
167
+ RateLimitedError,
168
+ )
169
+
170
+ client = SharpAPI("sk_live_xxx")
171
+
172
+ try:
173
+ evs = client.ev.get()
174
+ except AuthenticationError:
175
+ print("Invalid API key")
176
+ except TierRestrictedError as e:
177
+ print(f"Upgrade to {e.required_tier} tier for this feature")
178
+ except RateLimitedError as e:
179
+ print(f"Rate limited — retry after {e.retry_after}s")
180
+ ```
181
+
182
+ ## Odds Conversion Utilities
183
+
184
+ ```python
185
+ from sharpapi import american_to_decimal, american_to_probability, decimal_to_american
186
+
187
+ american_to_decimal(-110) # 1.909
188
+ american_to_decimal(150) # 2.5
189
+ american_to_probability(-110) # 0.524
190
+ decimal_to_american(2.5) # 150
191
+ ```
192
+
193
+ ## Requirements
194
+
195
+ - Python 3.9+
196
+ - httpx
197
+ - pydantic v2
198
+
199
+ ## Links
200
+
201
+ - [API Docs](https://docs.sharpapi.io)
202
+ - [Dashboard](https://sharpapi.io/dashboard)
203
+ - [Discord](https://discord.gg/sharpapi)
@@ -0,0 +1,170 @@
1
+ # SharpAPI Python SDK
2
+
3
+ Official Python client for the [SharpAPI](https://sharpapi.io) real-time sports betting odds API.
4
+
5
+ Get pre-computed +EV opportunities, arbitrage detection, middles, and live odds from 20+ sportsbooks — with Pinnacle as the sharp reference.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install sharpapi
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from sharpapi import SharpAPI
17
+
18
+ client = SharpAPI("sk_live_xxx")
19
+
20
+ # --- Arbitrage opportunities ---
21
+ arbs = client.arbitrage.get(min_profit=1.0, league="nba")
22
+ for arb in arbs.data:
23
+ print(f"{arb.profit_percent:.2f}% profit — {arb.event_name}")
24
+ for leg in arb.legs:
25
+ print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} ({leg.stake_percent:.1f}%)")
26
+
27
+ # --- +EV opportunities ---
28
+ evs = client.ev.get(min_ev=3.0, sport="basketball")
29
+ for opp in evs.data:
30
+ print(f"+{opp.ev_percent:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
31
+ if opp.kelly_fraction:
32
+ print(f" Kelly: {opp.kelly_fraction:.1%} of bankroll")
33
+
34
+ # --- Best odds across books ---
35
+ odds = client.odds.best(league="nba", market="moneyline")
36
+ for line in odds.data:
37
+ print(f"{line.home_team} vs {line.away_team}: {line.selection} {line.odds_american}")
38
+ ```
39
+
40
+ ## Streaming
41
+
42
+ Real-time SSE streaming for odds updates and opportunity alerts (requires WebSocket add-on):
43
+
44
+ ```python
45
+ stream = client.stream.opportunities(league="nba")
46
+
47
+ @stream.on("ev:detected")
48
+ def on_ev(data):
49
+ for opp in data:
50
+ print(f"+EV: {opp['selection']} {opp['ev_percent']}% @ {opp['sportsbook']}")
51
+
52
+ @stream.on("arb:detected")
53
+ def on_arb(data):
54
+ for arb in data:
55
+ print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}")
56
+
57
+ stream.connect() # Blocks, processing events
58
+ ```
59
+
60
+ Or iterate over events:
61
+
62
+ ```python
63
+ for event_type, data in stream.iter_events():
64
+ if event_type == "ev:detected":
65
+ print(data)
66
+ ```
67
+
68
+ ## All Resources
69
+
70
+ ```python
71
+ # Odds
72
+ client.odds.get(sport="basketball", league="nba")
73
+ client.odds.best(league="nfl", market="moneyline")
74
+ client.odds.comparison(event_id="abc123")
75
+ client.odds.batch(event_ids=["abc123", "def456"])
76
+
77
+ # Opportunities
78
+ client.ev.get(min_ev=2.0, sportsbook="draftkings")
79
+ client.arbitrage.get(min_profit=0.5, sport="football")
80
+ client.middles.get(sport="football", min_size=3.0)
81
+ client.low_hold.get(max_hold=2.0)
82
+
83
+ # Reference data
84
+ client.sports.list()
85
+ client.leagues.list(sport="basketball")
86
+ client.sportsbooks.list()
87
+ client.events.list(league="nba", live=True)
88
+ client.events.search("Lakers")
89
+
90
+ # Account
91
+ client.account.me() # Tier, limits, features
92
+ client.account.usage() # Request counts
93
+
94
+ # Streaming
95
+ client.stream.odds(league="nba")
96
+ client.stream.opportunities(min_ev=3.0)
97
+ client.stream.all(sport="basketball")
98
+ client.stream.event("event_id_here")
99
+ ```
100
+
101
+ ## Data Quality
102
+
103
+ Every opportunity response includes staleness metadata to avoid acting on stale odds:
104
+
105
+ ```python
106
+ arbs = client.arbitrage.get()
107
+ for arb in arbs.data:
108
+ if arb.possibly_stale:
109
+ print(f" Skipping — odds may be stale ({arb.oldest_odds_age_seconds}s old)")
110
+ continue
111
+ if "LIVE_HIGH_PROFIT_SUSPICIOUS" in arb.warnings:
112
+ print(f" Skipping — likely phantom arb")
113
+ continue
114
+ print(f"Actionable: {arb.profit_percent}%")
115
+ ```
116
+
117
+ ## Rate Limits
118
+
119
+ Rate limit info is available after every request:
120
+
121
+ ```python
122
+ response = client.odds.get()
123
+ print(f"Remaining: {client.rate_limit.remaining}/{client.rate_limit.limit}")
124
+ print(f"Tier: {client.rate_limit.tier}")
125
+ ```
126
+
127
+ ## Error Handling
128
+
129
+ ```python
130
+ from sharpapi import (
131
+ SharpAPI,
132
+ AuthenticationError,
133
+ TierRestrictedError,
134
+ RateLimitedError,
135
+ )
136
+
137
+ client = SharpAPI("sk_live_xxx")
138
+
139
+ try:
140
+ evs = client.ev.get()
141
+ except AuthenticationError:
142
+ print("Invalid API key")
143
+ except TierRestrictedError as e:
144
+ print(f"Upgrade to {e.required_tier} tier for this feature")
145
+ except RateLimitedError as e:
146
+ print(f"Rate limited — retry after {e.retry_after}s")
147
+ ```
148
+
149
+ ## Odds Conversion Utilities
150
+
151
+ ```python
152
+ from sharpapi import american_to_decimal, american_to_probability, decimal_to_american
153
+
154
+ american_to_decimal(-110) # 1.909
155
+ american_to_decimal(150) # 2.5
156
+ american_to_probability(-110) # 0.524
157
+ decimal_to_american(2.5) # 150
158
+ ```
159
+
160
+ ## Requirements
161
+
162
+ - Python 3.9+
163
+ - httpx
164
+ - pydantic v2
165
+
166
+ ## Links
167
+
168
+ - [API Docs](https://docs.sharpapi.io)
169
+ - [Dashboard](https://sharpapi.io/dashboard)
170
+ - [Discord](https://discord.gg/sharpapi)
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sharpapi"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "SharpAPI", email = "support@sharpapi.io" }]
13
+ keywords = ["sports-betting", "odds", "arbitrage", "ev", "api", "real-time", "pinnacle"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.25.0",
29
+ "pydantic>=2.0.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ pandas = ["pandas>=1.5.0"]
34
+ test = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://sharpapi.io"
38
+ Documentation = "https://docs.sharpapi.io/sdks/python"
39
+ Repository = "https://github.com/sharpapi/sharpapi-python"
40
+ Changelog = "https://github.com/sharpapi/sharpapi-python/releases"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/sharpapi"]
44
+
45
+ [tool.ruff]
46
+ target-version = "py39"
47
+ line-length = 100
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I", "UP"]
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ testpaths = ["tests"]
55
+
56
+ [tool.pyright]
57
+ pythonVersion = "3.9"
58
+ typeCheckingMode = "standard"
@@ -0,0 +1,93 @@
1
+ """SharpAPI Python SDK — Real-time sports betting odds, +EV, and arbitrage detection.
2
+
3
+ Example::
4
+
5
+ from sharpapi import SharpAPI
6
+
7
+ client = SharpAPI("sk_live_xxx")
8
+
9
+ # Arbitrage opportunities
10
+ arbs = client.arbitrage.get(min_profit=1.0)
11
+ for arb in arbs.data:
12
+ print(f"{arb.profit_percent}% — {arb.event_name}")
13
+
14
+ # +EV opportunities
15
+ evs = client.ev.get(min_ev=3.0, league="nba")
16
+ for opp in evs.data:
17
+ print(f"+{opp.ev_percent}% on {opp.selection} @ {opp.sportsbook}")
18
+ """
19
+
20
+ from .async_client import AsyncSharpAPI
21
+ from .client import SharpAPI
22
+ from .exceptions import (
23
+ AuthenticationError,
24
+ RateLimitedError,
25
+ SharpAPIError,
26
+ StreamError,
27
+ TierRestrictedError,
28
+ ValidationError,
29
+ )
30
+ from .models import (
31
+ APIResponse,
32
+ AccountInfo,
33
+ ArbitrageLeg,
34
+ ArbitrageOpportunity,
35
+ EVOpportunity,
36
+ Event,
37
+ GameState,
38
+ League,
39
+ LowHoldOpportunity,
40
+ LowHoldSide,
41
+ MiddleOpportunity,
42
+ MiddleSide,
43
+ OddsLine,
44
+ OddsValue,
45
+ Pagination,
46
+ RateLimitInfo,
47
+ ResponseMeta,
48
+ Sport,
49
+ Sportsbook,
50
+ )
51
+ from .streaming import EventStream
52
+ from ._utils import american_to_decimal, american_to_probability, decimal_to_american
53
+
54
+ __version__ = "0.1.0"
55
+
56
+ __all__ = [
57
+ # Clients
58
+ "SharpAPI",
59
+ "AsyncSharpAPI",
60
+ # Models
61
+ "APIResponse",
62
+ "AccountInfo",
63
+ "ArbitrageLeg",
64
+ "ArbitrageOpportunity",
65
+ "EVOpportunity",
66
+ "Event",
67
+ "GameState",
68
+ "League",
69
+ "LowHoldOpportunity",
70
+ "LowHoldSide",
71
+ "MiddleOpportunity",
72
+ "MiddleSide",
73
+ "OddsLine",
74
+ "OddsValue",
75
+ "Pagination",
76
+ "RateLimitInfo",
77
+ "ResponseMeta",
78
+ "Sport",
79
+ "Sportsbook",
80
+ # Streaming
81
+ "EventStream",
82
+ # Exceptions
83
+ "AuthenticationError",
84
+ "RateLimitedError",
85
+ "SharpAPIError",
86
+ "StreamError",
87
+ "TierRestrictedError",
88
+ "ValidationError",
89
+ # Utilities
90
+ "american_to_decimal",
91
+ "american_to_probability",
92
+ "decimal_to_american",
93
+ ]
@@ -0,0 +1,121 @@
1
+ """Shared logic for sync and async clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .exceptions import (
10
+ AuthenticationError,
11
+ RateLimitedError,
12
+ SharpAPIError,
13
+ TierRestrictedError,
14
+ ValidationError,
15
+ )
16
+ from .models import APIResponse, RateLimitInfo, ResponseMeta
17
+
18
+ DEFAULT_BASE_URL = "https://api.sharpapi.io"
19
+ DEFAULT_TIMEOUT = 30.0
20
+ USER_AGENT = "sharpapi-python/0.1.0"
21
+
22
+
23
+ def parse_response(raw: dict, model_class: type) -> APIResponse:
24
+ """Parse raw API JSON into a typed APIResponse."""
25
+ data_raw = raw.get("data", [])
26
+ if isinstance(data_raw, list):
27
+ items = [model_class.model_validate(item) for item in data_raw]
28
+ else:
29
+ items = [model_class.model_validate(data_raw)]
30
+
31
+ meta = None
32
+ meta_raw = raw.get("meta")
33
+ if meta_raw:
34
+ meta = ResponseMeta.model_validate(meta_raw)
35
+
36
+ return APIResponse(
37
+ success=raw.get("success"),
38
+ data=items,
39
+ meta=meta,
40
+ timestamp=raw.get("timestamp"),
41
+ tier=raw.get("tier"),
42
+ )
43
+
44
+
45
+ def parse_rate_limit(response: httpx.Response) -> RateLimitInfo:
46
+ """Extract rate limit info from response headers."""
47
+ headers = response.headers
48
+ return RateLimitInfo(
49
+ limit=_int_or_none(headers.get("x-ratelimit-limit")),
50
+ remaining=_int_or_none(headers.get("x-ratelimit-remaining")),
51
+ reset=_float_or_none(headers.get("x-ratelimit-reset")),
52
+ tier=headers.get("x-tier"),
53
+ )
54
+
55
+
56
+ def handle_errors(response: httpx.Response) -> None:
57
+ """Raise typed exceptions for error responses."""
58
+ if response.is_success:
59
+ return
60
+
61
+ try:
62
+ body = response.json()
63
+ except Exception:
64
+ body = {}
65
+
66
+ error_obj = body.get("error", body)
67
+ if isinstance(error_obj, dict):
68
+ error_msg = error_obj.get("message", error_obj.get("error", f"HTTP {response.status_code}"))
69
+ code = error_obj.get("code", body.get("code", "unknown_error"))
70
+ else:
71
+ error_msg = str(error_obj) if error_obj else f"HTTP {response.status_code}"
72
+ code = body.get("code", "unknown_error")
73
+ status = response.status_code
74
+
75
+ if status == 401:
76
+ raise AuthenticationError(error_msg, code=code, status=status)
77
+ elif status == 403:
78
+ raise TierRestrictedError(
79
+ error_msg,
80
+ code=code,
81
+ status=status,
82
+ required_tier=body.get("required_tier"),
83
+ )
84
+ elif status == 429:
85
+ raise RateLimitedError(
86
+ error_msg,
87
+ code=code,
88
+ status=status,
89
+ retry_after=body.get("retry_after"),
90
+ )
91
+ elif status == 400:
92
+ raise ValidationError(error_msg, code=code, status=status)
93
+ else:
94
+ raise SharpAPIError(error_msg, code=code, status=status)
95
+
96
+
97
+ def make_headers(api_key: str) -> dict[str, str]:
98
+ """Build default request headers."""
99
+ return {
100
+ "X-API-Key": api_key,
101
+ "Content-Type": "application/json",
102
+ "User-Agent": USER_AGENT,
103
+ }
104
+
105
+
106
+ def _int_or_none(value: str | None) -> int | None:
107
+ if value is None:
108
+ return None
109
+ try:
110
+ return int(value)
111
+ except (ValueError, TypeError):
112
+ return None
113
+
114
+
115
+ def _float_or_none(value: str | None) -> float | None:
116
+ if value is None:
117
+ return None
118
+ try:
119
+ return float(value)
120
+ except (ValueError, TypeError):
121
+ return None
@@ -0,0 +1,39 @@
1
+ """Internal utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def american_to_decimal(american: int | float) -> float:
7
+ """Convert American odds to decimal odds."""
8
+ if american > 0:
9
+ return american / 100 + 1
10
+ return 100 / abs(american) + 1
11
+
12
+
13
+ def decimal_to_american(decimal: float) -> int:
14
+ """Convert decimal odds to American odds."""
15
+ if decimal >= 2.0:
16
+ return round((decimal - 1) * 100)
17
+ return round(-100 / (decimal - 1))
18
+
19
+
20
+ def american_to_probability(american: int | float) -> float:
21
+ """Convert American odds to implied probability (0-1)."""
22
+ if american > 0:
23
+ return 100 / (american + 100)
24
+ return abs(american) / (abs(american) + 100)
25
+
26
+
27
+ def _clean_params(params: dict) -> dict:
28
+ """Remove None values and join lists with commas."""
29
+ cleaned = {}
30
+ for key, value in params.items():
31
+ if value is None:
32
+ continue
33
+ if isinstance(value, bool):
34
+ cleaned[key] = str(value).lower()
35
+ elif isinstance(value, list):
36
+ cleaned[key] = ",".join(str(v) for v in value)
37
+ else:
38
+ cleaned[key] = value
39
+ return cleaned