tradeodds 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,72 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ dist-ssr/
7
+
8
+ # Environment variables (NEVER commit)
9
+ .env
10
+ .env.local
11
+ .env.*.local
12
+ backend/.env
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.pyc
17
+ *.pyo
18
+ backend/venv/
19
+ *.egg-info/
20
+
21
+ # OS files
22
+ .DS_Store
23
+ Thumbs.db
24
+
25
+ # IDE
26
+ .vscode/
27
+ .idea/
28
+ .claude/
29
+ *.swp
30
+ *.swo
31
+
32
+ # Logs
33
+ *.log
34
+ npm-debug.log*
35
+
36
+ # Lock files (using npm)
37
+ bun.lockb
38
+
39
+ # Content engine output
40
+ backend/scripts/output/
41
+
42
+ # GuessTheMove dedup ledger (local state)
43
+ video/props/.guess_ledger.json
44
+
45
+ # YouTube OAuth credentials (NEVER commit)
46
+ backend/scripts/client_secret.json
47
+ backend/scripts/youtube_token.json
48
+ video/props/.youtube_uploads.json
49
+
50
+ # TikTok OAuth credentials (NEVER commit)
51
+ backend/scripts/tiktok_token.json
52
+ video/props/.tiktok_uploads.json
53
+
54
+ # Agent reports (ephemeral, regenerated nightly)
55
+ backend/scripts/agents/reports/*.json
56
+
57
+ # Astro content hub (generated at build)
58
+ content/.astro/
59
+ content/dist/
60
+
61
+ # Misc
62
+ *.tsbuildinfo
63
+
64
+ # Claude dev handoff docs (internal session prompts, not production code)
65
+ Context/HANDOFF_POST_MIGRATION.md
66
+ Context/HANDOFF_TRADEODDS_MIGRATION.md
67
+ Context/PROMPT_PHASE_11A.md
68
+ Context/SESSION_PROMPT_RECOMMENDATIONS.md
69
+ Context/RECOMMENDATIONS.md
70
+ .vercel
71
+ .env*.local
72
+ .mcp.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TradeOdds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: tradeodds
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the TradeOdds REST API — quantitative pattern analysis on ~3,200 symbols.
5
+ Project-URL: Homepage, https://tradeodds.io
6
+ Project-URL: Documentation, https://tradeodds.io/api-docs
7
+ Project-URL: Repository, https://github.com/cpoly/tradeodds
8
+ Project-URL: Issues, https://github.com/cpoly/tradeodds/issues
9
+ Author-email: TradeOdds <support@tradeodds.io>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 TradeOdds
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: api,factor-match,finance,quantitative,sdk,tradeodds,trading
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Intended Audience :: Financial and Insurance Industry
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Topic :: Office/Business :: Financial :: Investment
44
+ Requires-Python: >=3.9
45
+ Requires-Dist: requests>=2.28.0
46
+ Provides-Extra: dev
47
+ Requires-Dist: mypy>=1.0; extra == 'dev'
48
+ Requires-Dist: pytest>=7.0; extra == 'dev'
49
+ Requires-Dist: types-requests; extra == 'dev'
50
+ Description-Content-Type: text/markdown
51
+
52
+ # tradeodds — Python SDK
53
+
54
+ Official Python client for the [TradeOdds](https://tradeodds.io) REST API. Run quantitative pattern analysis on ~3,200 symbols (US equities, ETFs, major crypto) with one function call.
55
+
56
+ ```bash
57
+ pip install tradeodds
58
+ ```
59
+
60
+ ## Quickstart
61
+
62
+ ```python
63
+ from tradeodds import TradeOddsClient
64
+
65
+ client = TradeOddsClient() # reads TRADEODDS_API_KEY from env
66
+
67
+ result = client.analyze(
68
+ symbol="SPY",
69
+ forward_period="5d",
70
+ conditions={"daily_change": True, "vix_level": True, "regime": True},
71
+ lookback_years="20y",
72
+ )
73
+
74
+ stats = result["forward_stats"]
75
+ print(f"{result['match_count']} matches | win rate {stats['win_rate']:.0%} | median {stats['median_return']:+.2%}")
76
+ ```
77
+
78
+ Get an API key at <https://tradeodds.io/account>. Free tier ships with the platform — pay-as-you-go pricing kicks in at $0.05/analyze and $0.15/factor-match.
79
+
80
+ ## What's in the box
81
+
82
+ | Method | Endpoint | Auth | Cost |
83
+ |---|---|---|---|
84
+ | `client.symbols()` | `GET /api/v1/symbols` | none | free |
85
+ | `client.analyze(symbol, ...)` | `POST /api/v1/analyze` | API key | $0.05 |
86
+ | `client.factor_match(...)` | `POST /api/v1/factor-match` | API key | $0.15 |
87
+
88
+ ## Installation
89
+
90
+ ```bash
91
+ pip install tradeodds
92
+ ```
93
+
94
+ Requires Python 3.9+. The only runtime dependency is [`requests`](https://requests.readthedocs.io).
95
+
96
+ ## Configuration
97
+
98
+ | Source | Variable | Notes |
99
+ |---|---|---|
100
+ | Env | `TRADEODDS_API_KEY` | `sk-to-...` token. Default auth source. |
101
+ | Env | `TRADEODDS_BASE_URL` | Override for staging / self-host. Defaults to production. |
102
+ | Constructor | `TradeOddsClient(api_key=..., base_url=..., timeout=200.0)` | Explicit overrides win over env. |
103
+
104
+ ```python
105
+ from tradeodds import TradeOddsClient
106
+
107
+ client = TradeOddsClient(api_key="sk-to-abc123...", timeout=60.0)
108
+ ```
109
+
110
+ ## API reference
111
+
112
+ ### `client.symbols(active_only=True)`
113
+
114
+ List every symbol available for analysis.
115
+
116
+ ```python
117
+ universe = client.symbols()
118
+ print(f"{universe['count']} symbols")
119
+ spy = next(s for s in universe["symbols"] if s["symbol"] == "SPY")
120
+ ```
121
+
122
+ ### `client.analyze(symbol, **kwargs)`
123
+
124
+ Returns probability-weighted historical analogs for the symbol's current DNA fingerprint. Auth required. $0.05/call.
125
+
126
+ | Arg | Type | Default | Notes |
127
+ |---|---|---|---|
128
+ | `symbol` | str | — | Ticker (case-insensitive). |
129
+ | `reference_period` | `1d` `5d` `1m` … | `1d` | How recent the observed window is. |
130
+ | `forward_period` | `1d` `5d` `20d` … | `5d` | Trading days forward to compute outcomes. |
131
+ | `conditions` | `DNAConditions` | `{daily_change: True}` | Toggle factors: `vix_level`, `regime`, `rsi_zone`, `streak`, `macro_risk`, etc. |
132
+ | `lookback_years` | `1y` `5y` `20y` `max` | `max` | History window for matches. |
133
+ | `price_tolerance` | int 0-3 | 0 | Bucket-step tolerance for price. |
134
+ | `vix_tolerance` | int 0-3 | 0 | Bucket-step tolerance for VIX. |
135
+
136
+ ### `client.factor_match(**kwargs)`
137
+
138
+ Scans all symbols for whose current state matches the supplied conditions. Returns a ranked list with historical forward-return stats. Auth required. $0.15/call.
139
+
140
+ ```python
141
+ result = client.factor_match(
142
+ conditions={"vix_level": True, "regime": True, "rsi_zone": True},
143
+ filters={"is_etf": False, "price_min": 10},
144
+ perf_filter={"metric": "win_5d", "operator": "gt", "threshold": 0.6},
145
+ min_instances=20,
146
+ )
147
+ ```
148
+
149
+ ## Error handling
150
+
151
+ Errors are typed exceptions that preserve the structured envelope (`code`, `hint`, `request_id`):
152
+
153
+ ```python
154
+ from tradeodds import ApiError, AuthError, NotFoundError, RateLimitError
155
+
156
+ try:
157
+ result = client.analyze(symbol="SPY")
158
+ except RateLimitError as exc:
159
+ print(exc.code, exc.message, exc.hint, exc.request_id)
160
+ except AuthError:
161
+ print("Set TRADEODDS_API_KEY")
162
+ except NotFoundError:
163
+ print("Unknown symbol")
164
+ except ApiError as exc:
165
+ print(f"[{exc.status_code}] {exc.code}: {exc.message}")
166
+ ```
167
+
168
+ | Exception | When |
169
+ |---|---|
170
+ | `AuthError` | 401 — missing or invalid key |
171
+ | `NotFoundError` | 404 — unknown symbol |
172
+ | `ValidationError` | 400 / 422 — bad request body |
173
+ | `RateLimitError` | 429 — daily or per-minute cap |
174
+ | `ServerError` | 5xx and client-side timeouts (504 from server-side timeout) |
175
+ | `ApiError` | base class for all of the above |
176
+
177
+ Every exception exposes:
178
+
179
+ - `exc.code` — machine-readable code (e.g. `rate_limit_exceeded`)
180
+ - `exc.message` — human-readable summary
181
+ - `exc.hint` — suggested remediation, when the API provides one
182
+ - `exc.request_id` — server request id for support contact
183
+ - `exc.status_code` — HTTP status
184
+
185
+ ## License
186
+
187
+ MIT. See `LICENSE`.
@@ -0,0 +1,136 @@
1
+ # tradeodds — Python SDK
2
+
3
+ Official Python client for the [TradeOdds](https://tradeodds.io) REST API. Run quantitative pattern analysis on ~3,200 symbols (US equities, ETFs, major crypto) with one function call.
4
+
5
+ ```bash
6
+ pip install tradeodds
7
+ ```
8
+
9
+ ## Quickstart
10
+
11
+ ```python
12
+ from tradeodds import TradeOddsClient
13
+
14
+ client = TradeOddsClient() # reads TRADEODDS_API_KEY from env
15
+
16
+ result = client.analyze(
17
+ symbol="SPY",
18
+ forward_period="5d",
19
+ conditions={"daily_change": True, "vix_level": True, "regime": True},
20
+ lookback_years="20y",
21
+ )
22
+
23
+ stats = result["forward_stats"]
24
+ print(f"{result['match_count']} matches | win rate {stats['win_rate']:.0%} | median {stats['median_return']:+.2%}")
25
+ ```
26
+
27
+ Get an API key at <https://tradeodds.io/account>. Free tier ships with the platform — pay-as-you-go pricing kicks in at $0.05/analyze and $0.15/factor-match.
28
+
29
+ ## What's in the box
30
+
31
+ | Method | Endpoint | Auth | Cost |
32
+ |---|---|---|---|
33
+ | `client.symbols()` | `GET /api/v1/symbols` | none | free |
34
+ | `client.analyze(symbol, ...)` | `POST /api/v1/analyze` | API key | $0.05 |
35
+ | `client.factor_match(...)` | `POST /api/v1/factor-match` | API key | $0.15 |
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install tradeodds
41
+ ```
42
+
43
+ Requires Python 3.9+. The only runtime dependency is [`requests`](https://requests.readthedocs.io).
44
+
45
+ ## Configuration
46
+
47
+ | Source | Variable | Notes |
48
+ |---|---|---|
49
+ | Env | `TRADEODDS_API_KEY` | `sk-to-...` token. Default auth source. |
50
+ | Env | `TRADEODDS_BASE_URL` | Override for staging / self-host. Defaults to production. |
51
+ | Constructor | `TradeOddsClient(api_key=..., base_url=..., timeout=200.0)` | Explicit overrides win over env. |
52
+
53
+ ```python
54
+ from tradeodds import TradeOddsClient
55
+
56
+ client = TradeOddsClient(api_key="sk-to-abc123...", timeout=60.0)
57
+ ```
58
+
59
+ ## API reference
60
+
61
+ ### `client.symbols(active_only=True)`
62
+
63
+ List every symbol available for analysis.
64
+
65
+ ```python
66
+ universe = client.symbols()
67
+ print(f"{universe['count']} symbols")
68
+ spy = next(s for s in universe["symbols"] if s["symbol"] == "SPY")
69
+ ```
70
+
71
+ ### `client.analyze(symbol, **kwargs)`
72
+
73
+ Returns probability-weighted historical analogs for the symbol's current DNA fingerprint. Auth required. $0.05/call.
74
+
75
+ | Arg | Type | Default | Notes |
76
+ |---|---|---|---|
77
+ | `symbol` | str | — | Ticker (case-insensitive). |
78
+ | `reference_period` | `1d` `5d` `1m` … | `1d` | How recent the observed window is. |
79
+ | `forward_period` | `1d` `5d` `20d` … | `5d` | Trading days forward to compute outcomes. |
80
+ | `conditions` | `DNAConditions` | `{daily_change: True}` | Toggle factors: `vix_level`, `regime`, `rsi_zone`, `streak`, `macro_risk`, etc. |
81
+ | `lookback_years` | `1y` `5y` `20y` `max` | `max` | History window for matches. |
82
+ | `price_tolerance` | int 0-3 | 0 | Bucket-step tolerance for price. |
83
+ | `vix_tolerance` | int 0-3 | 0 | Bucket-step tolerance for VIX. |
84
+
85
+ ### `client.factor_match(**kwargs)`
86
+
87
+ Scans all symbols for whose current state matches the supplied conditions. Returns a ranked list with historical forward-return stats. Auth required. $0.15/call.
88
+
89
+ ```python
90
+ result = client.factor_match(
91
+ conditions={"vix_level": True, "regime": True, "rsi_zone": True},
92
+ filters={"is_etf": False, "price_min": 10},
93
+ perf_filter={"metric": "win_5d", "operator": "gt", "threshold": 0.6},
94
+ min_instances=20,
95
+ )
96
+ ```
97
+
98
+ ## Error handling
99
+
100
+ Errors are typed exceptions that preserve the structured envelope (`code`, `hint`, `request_id`):
101
+
102
+ ```python
103
+ from tradeodds import ApiError, AuthError, NotFoundError, RateLimitError
104
+
105
+ try:
106
+ result = client.analyze(symbol="SPY")
107
+ except RateLimitError as exc:
108
+ print(exc.code, exc.message, exc.hint, exc.request_id)
109
+ except AuthError:
110
+ print("Set TRADEODDS_API_KEY")
111
+ except NotFoundError:
112
+ print("Unknown symbol")
113
+ except ApiError as exc:
114
+ print(f"[{exc.status_code}] {exc.code}: {exc.message}")
115
+ ```
116
+
117
+ | Exception | When |
118
+ |---|---|
119
+ | `AuthError` | 401 — missing or invalid key |
120
+ | `NotFoundError` | 404 — unknown symbol |
121
+ | `ValidationError` | 400 / 422 — bad request body |
122
+ | `RateLimitError` | 429 — daily or per-minute cap |
123
+ | `ServerError` | 5xx and client-side timeouts (504 from server-side timeout) |
124
+ | `ApiError` | base class for all of the above |
125
+
126
+ Every exception exposes:
127
+
128
+ - `exc.code` — machine-readable code (e.g. `rate_limit_exceeded`)
129
+ - `exc.message` — human-readable summary
130
+ - `exc.hint` — suggested remediation, when the API provides one
131
+ - `exc.request_id` — server request id for support contact
132
+ - `exc.status_code` — HTTP status
133
+
134
+ ## License
135
+
136
+ MIT. See `LICENSE`.
@@ -0,0 +1,43 @@
1
+ """TradeOdds SDK quickstart.
2
+
3
+ Set TRADEODDS_API_KEY in your environment, then run:
4
+
5
+ python examples/quickstart.py
6
+
7
+ Outputs the historical win rate and median 5-day forward return for SPY when
8
+ VIX is in its current regime — an end-to-end live API call in <30 lines.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+
16
+ from tradeodds import ApiError, TradeOddsClient
17
+
18
+
19
+ def main() -> int:
20
+ if not os.environ.get("TRADEODDS_API_KEY"):
21
+ print("Set TRADEODDS_API_KEY first. Get a key at https://tradeodds.io/account.")
22
+ return 1
23
+
24
+ with TradeOddsClient() as client:
25
+ try:
26
+ result = client.analyze(
27
+ symbol="SPY",
28
+ forward_period="5d",
29
+ conditions={"daily_change": True, "vix_level": True, "regime": True},
30
+ lookback_years="20y",
31
+ )
32
+ except ApiError as exc:
33
+ print(f"API error [{exc.code}]: {exc.message}")
34
+ if exc.hint:
35
+ print(f"hint: {exc.hint}")
36
+ return 1
37
+
38
+ print(json.dumps(result, indent=2, default=str)[:1500])
39
+ return 0
40
+
41
+
42
+ if __name__ == "__main__":
43
+ sys.exit(main())
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tradeodds"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the TradeOdds REST API — quantitative pattern analysis on ~3,200 symbols."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "TradeOdds", email = "support@tradeodds.io" }]
13
+ keywords = ["tradeodds", "trading", "quantitative", "finance", "api", "sdk", "factor-match"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Office/Business :: Financial :: Investment",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.28.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "mypy>=1.0",
34
+ "types-requests",
35
+ "pytest>=7.0",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://tradeodds.io"
40
+ Documentation = "https://tradeodds.io/api-docs"
41
+ Repository = "https://github.com/cpoly/tradeodds"
42
+ Issues = "https://github.com/cpoly/tradeodds/issues"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["tradeodds"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = [
49
+ "tradeodds/",
50
+ "examples/",
51
+ "README.md",
52
+ "LICENSE",
53
+ ]
@@ -0,0 +1,62 @@
1
+ """TradeOdds — official Python SDK.
2
+
3
+ Quickstart:
4
+
5
+ >>> from tradeodds import TradeOddsClient
6
+ >>> client = TradeOddsClient() # reads TRADEODDS_API_KEY
7
+ >>> result = client.analyze(symbol="SPY", conditions={"vix_level": True})
8
+ >>> print(f"{result['match_count']} historical matches")
9
+ """
10
+ from .client import DEFAULT_BASE_URL, TradeOddsClient, __version__
11
+ from .exceptions import (
12
+ ApiError,
13
+ AuthError,
14
+ NotFoundError,
15
+ RateLimitError,
16
+ ServerError,
17
+ ValidationError,
18
+ )
19
+ from .types import (
20
+ AnalyzeResult,
21
+ DNAConditions,
22
+ FactorMatchResult,
23
+ ForwardPeriod,
24
+ LookbackYears,
25
+ PerfFilter,
26
+ PerfMetric,
27
+ PerfOperator,
28
+ ReferencePeriod,
29
+ ScreenerFilters,
30
+ Symbol,
31
+ SymbolListResponse,
32
+ )
33
+
34
+ # Backwards-compatibility alias — many SDKs expose `Client`.
35
+ Client = TradeOddsClient
36
+
37
+ __all__ = [
38
+ "__version__",
39
+ "Client",
40
+ "TradeOddsClient",
41
+ "DEFAULT_BASE_URL",
42
+ # Exceptions
43
+ "ApiError",
44
+ "AuthError",
45
+ "NotFoundError",
46
+ "RateLimitError",
47
+ "ServerError",
48
+ "ValidationError",
49
+ # Types
50
+ "AnalyzeResult",
51
+ "DNAConditions",
52
+ "FactorMatchResult",
53
+ "ForwardPeriod",
54
+ "LookbackYears",
55
+ "PerfFilter",
56
+ "PerfMetric",
57
+ "PerfOperator",
58
+ "ReferencePeriod",
59
+ "ScreenerFilters",
60
+ "Symbol",
61
+ "SymbolListResponse",
62
+ ]
@@ -0,0 +1,268 @@
1
+ """Synchronous HTTP client for the TradeOdds REST API.
2
+
3
+ Thin wrapper around ``requests``: one method per endpoint, typed inputs,
4
+ typed exceptions on 4xx/5xx with the structured envelope's ``code`` / ``hint``
5
+ / ``request_id`` preserved as exception attributes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import requests
13
+
14
+ from .exceptions import (
15
+ ApiError,
16
+ AuthError,
17
+ NotFoundError,
18
+ RateLimitError,
19
+ ServerError,
20
+ ValidationError,
21
+ )
22
+ from .types import (
23
+ AnalyzeResult,
24
+ DNAConditions,
25
+ FactorMatchResult,
26
+ ForwardPeriod,
27
+ LookbackYears,
28
+ PerfFilter,
29
+ ReferencePeriod,
30
+ ScreenerFilters,
31
+ SymbolListResponse,
32
+ )
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ DEFAULT_BASE_URL = "https://tradeodds-production.up.railway.app"
37
+
38
+
39
+ def _exception_for_status(status: int) -> type:
40
+ if status == 401:
41
+ return AuthError
42
+ if status == 404:
43
+ return NotFoundError
44
+ if status == 429:
45
+ return RateLimitError
46
+ if status in (400, 422):
47
+ return ValidationError
48
+ if status >= 500:
49
+ return ServerError
50
+ return ApiError
51
+
52
+
53
+ class TradeOddsClient:
54
+ """Synchronous client for the TradeOdds public REST API.
55
+
56
+ Reads ``TRADEODDS_API_KEY`` from the environment by default. Override with
57
+ ``api_key=`` / ``base_url=`` / ``timeout=`` constructor args. Pass a custom
58
+ ``session=requests.Session()`` to share connection pooling or attach
59
+ retries. ``symbols()`` works without an API key;
60
+ ``analyze()`` and ``factor_match()`` require one.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ api_key: Optional[str] = None,
66
+ *,
67
+ base_url: Optional[str] = None,
68
+ timeout: float = 200.0,
69
+ session: Optional[requests.Session] = None,
70
+ ) -> None:
71
+ self.api_key = api_key or os.environ.get("TRADEODDS_API_KEY")
72
+ self.base_url = (
73
+ base_url
74
+ or os.environ.get("TRADEODDS_BASE_URL")
75
+ or DEFAULT_BASE_URL
76
+ ).rstrip("/")
77
+ self.timeout = timeout
78
+ self._session = session or requests.Session()
79
+
80
+ # ── Public methods ──────────────────────────────────────────────
81
+
82
+ def symbols(self, *, active_only: bool = True) -> SymbolListResponse:
83
+ """``GET /api/v1/symbols`` — list every available instrument. No auth required."""
84
+ return self._request(
85
+ "GET",
86
+ "/api/v1/symbols",
87
+ params={"active_only": str(active_only).lower()},
88
+ auth_required=False,
89
+ )
90
+
91
+ def analyze(
92
+ self,
93
+ symbol: str,
94
+ *,
95
+ reference_period: ReferencePeriod = "1d",
96
+ forward_period: ForwardPeriod = "5d",
97
+ conditions: Optional[DNAConditions] = None,
98
+ lookback_years: LookbackYears = "max",
99
+ price_tolerance: int = 0,
100
+ vix_tolerance: int = 0,
101
+ ) -> AnalyzeResult:
102
+ """``POST /api/v1/analyze`` — historical pattern analysis on one symbol.
103
+
104
+ Returns match count, win rate, median forward return, and historical
105
+ instances. Billed at $0.05/call. Daily cap: 1,000/key. See README for
106
+ the full conditions vocabulary.
107
+ """
108
+ body: Dict[str, Any] = {
109
+ "symbol": symbol,
110
+ "reference_period": reference_period,
111
+ "forward_period": forward_period,
112
+ "lookback_years": lookback_years,
113
+ "price_tolerance": price_tolerance,
114
+ "vix_tolerance": vix_tolerance,
115
+ }
116
+ if conditions is not None:
117
+ body["conditions"] = dict(conditions)
118
+ return self._request("POST", "/api/v1/analyze", json=body, auth_required=True)
119
+
120
+ def factor_match(
121
+ self,
122
+ *,
123
+ forward_periods: Optional[List[ForwardPeriod]] = None,
124
+ conditions: Optional[DNAConditions] = None,
125
+ filters: Optional[ScreenerFilters] = None,
126
+ perf_filter: Optional[PerfFilter] = None,
127
+ history_range: LookbackYears = "max",
128
+ min_instances: int = 10,
129
+ price_tolerance: int = 0,
130
+ vix_tolerance: int = 0,
131
+ ) -> FactorMatchResult:
132
+ """``POST /api/v1/factor-match`` — scan all symbols for current DNA matches.
133
+
134
+ Returns a ranked list with historical forward-return stats per symbol.
135
+ Billed at $0.15/call. Daily cap: 200/key. Server caps at 180s; narrow
136
+ ``filters`` if you time out.
137
+ """
138
+ body: Dict[str, Any] = {
139
+ "forward_periods": forward_periods or ["1d", "5d", "20d"],
140
+ "history_range": history_range,
141
+ "min_instances": min_instances,
142
+ "price_tolerance": price_tolerance,
143
+ "vix_tolerance": vix_tolerance,
144
+ }
145
+ if conditions is not None:
146
+ body["conditions"] = dict(conditions)
147
+ if filters is not None:
148
+ body["filters"] = dict(filters)
149
+ if perf_filter is not None:
150
+ body["perf_filter"] = dict(perf_filter)
151
+ return self._request(
152
+ "POST", "/api/v1/factor-match", json=body, auth_required=True
153
+ )
154
+
155
+ # ── Internals ───────────────────────────────────────────────────
156
+
157
+ def _headers(self, auth_required: bool) -> Dict[str, str]:
158
+ headers = {
159
+ "User-Agent": f"tradeodds-python/{__version__}",
160
+ "X-Client": "python-sdk",
161
+ "Accept": "application/json",
162
+ }
163
+ if auth_required or self.api_key:
164
+ if not self.api_key:
165
+ raise AuthError(
166
+ "API key required.",
167
+ code="missing_api_key",
168
+ hint=(
169
+ "Pass api_key=... to TradeOddsClient or set the "
170
+ "TRADEODDS_API_KEY environment variable. "
171
+ "Get a key at https://tradeodds.io/account."
172
+ ),
173
+ status_code=401,
174
+ )
175
+ headers["Authorization"] = f"Bearer {self.api_key}"
176
+ return headers
177
+
178
+ def _request(
179
+ self,
180
+ method: str,
181
+ path: str,
182
+ *,
183
+ params: Optional[Dict[str, Any]] = None,
184
+ json: Optional[Dict[str, Any]] = None,
185
+ auth_required: bool = False,
186
+ ) -> Any:
187
+ url = f"{self.base_url}{path}"
188
+ try:
189
+ resp = self._session.request(
190
+ method,
191
+ url,
192
+ params=params,
193
+ json=json,
194
+ headers=self._headers(auth_required),
195
+ timeout=self.timeout,
196
+ )
197
+ except requests.Timeout as exc:
198
+ raise ServerError(
199
+ f"Request to {path} timed out after {self.timeout}s.",
200
+ code="client_timeout",
201
+ hint="Increase TradeOddsClient(timeout=...) or narrow your filters.",
202
+ status_code=None,
203
+ ) from exc
204
+ except requests.RequestException as exc:
205
+ raise ApiError(
206
+ f"Network error talking to TradeOdds: {exc}",
207
+ code="network_error",
208
+ hint="Check your connection and TRADEODDS_BASE_URL.",
209
+ ) from exc
210
+
211
+ if resp.status_code >= 400:
212
+ self._raise_for_status(resp)
213
+
214
+ if not resp.content:
215
+ return None
216
+ try:
217
+ return resp.json()
218
+ except ValueError as exc: # malformed JSON — should never happen
219
+ raise ServerError(
220
+ f"Invalid JSON response from {path}: {exc}",
221
+ code="invalid_response",
222
+ status_code=resp.status_code,
223
+ ) from exc
224
+
225
+ @staticmethod
226
+ def _raise_for_status(resp: requests.Response) -> None:
227
+ """Translate an error response into a typed SDK exception."""
228
+ request_id = resp.headers.get("X-Request-Id")
229
+ try:
230
+ payload = resp.json()
231
+ except ValueError:
232
+ payload = {}
233
+
234
+ envelope = payload.get("error") if isinstance(payload, dict) else None
235
+ if isinstance(envelope, dict):
236
+ code = envelope.get("code") or "api_error"
237
+ message = envelope.get("message") or resp.reason or "API error."
238
+ hint = envelope.get("hint")
239
+ request_id = envelope.get("request_id") or request_id
240
+ else:
241
+ # Non-v1 routes still use FastAPI's `{"detail": "..."}` shape.
242
+ code = "api_error"
243
+ detail = payload.get("detail") if isinstance(payload, dict) else None
244
+ message = detail if isinstance(detail, str) else (resp.reason or "API error.")
245
+ hint = None
246
+
247
+ exc_cls = _exception_for_status(resp.status_code)
248
+ raise exc_cls(
249
+ message,
250
+ code=code,
251
+ hint=hint,
252
+ request_id=request_id,
253
+ status_code=resp.status_code,
254
+ )
255
+
256
+ # ── Context-manager support ────────────────────────────────────
257
+
258
+ def close(self) -> None:
259
+ self._session.close()
260
+
261
+ def __enter__(self) -> "TradeOddsClient":
262
+ return self
263
+
264
+ def __exit__(self, *_exc: Any) -> None:
265
+ self.close()
266
+
267
+
268
+ __all__ = ["TradeOddsClient", "DEFAULT_BASE_URL", "__version__"]
@@ -0,0 +1,72 @@
1
+ """Typed exceptions for the TradeOdds SDK.
2
+
3
+ All errors raised by `TradeOddsClient` inherit from `ApiError`. The structured
4
+ error envelope returned by the API (`code`, `message`, `hint`, `request_id`)
5
+ is preserved as attributes so callers and LLM agents can branch on the error
6
+ code rather than parsing prose.
7
+ """
8
+ from typing import Optional
9
+
10
+
11
+ class ApiError(Exception):
12
+ """Base class for all TradeOdds API errors.
13
+
14
+ Attributes:
15
+ code: Machine-readable error code (e.g. ``rate_limit_exceeded``).
16
+ message: Human-readable error message from the API.
17
+ hint: Suggested remediation, if the API provided one.
18
+ request_id: Server-side request id for support contact.
19
+ status_code: HTTP status code that triggered the error.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ code: str = "api_error",
27
+ hint: Optional[str] = None,
28
+ request_id: Optional[str] = None,
29
+ status_code: Optional[int] = None,
30
+ ) -> None:
31
+ super().__init__(message)
32
+ self.code = code
33
+ self.message = message
34
+ self.hint = hint
35
+ self.request_id = request_id
36
+ self.status_code = status_code
37
+
38
+ def __repr__(self) -> str:
39
+ return (
40
+ f"{self.__class__.__name__}(code={self.code!r}, "
41
+ f"status_code={self.status_code!r}, message={self.message!r})"
42
+ )
43
+
44
+
45
+ class AuthError(ApiError):
46
+ """Raised on 401 — missing or invalid API key."""
47
+
48
+
49
+ class NotFoundError(ApiError):
50
+ """Raised on 404 — unknown symbol or resource."""
51
+
52
+
53
+ class RateLimitError(ApiError):
54
+ """Raised on 429 — daily or per-minute quota exceeded."""
55
+
56
+
57
+ class ValidationError(ApiError):
58
+ """Raised on 422 / 400 — invalid request body or parameters."""
59
+
60
+
61
+ class ServerError(ApiError):
62
+ """Raised on 5xx — server-side failure or timeout (504)."""
63
+
64
+
65
+ __all__ = [
66
+ "ApiError",
67
+ "AuthError",
68
+ "NotFoundError",
69
+ "RateLimitError",
70
+ "ValidationError",
71
+ "ServerError",
72
+ ]
@@ -0,0 +1,112 @@
1
+ """TypedDict definitions for TradeOdds request/response shapes.
2
+
3
+ These mirror the Pydantic models in ``backend/app/api/public_api.py``. Using
4
+ TypedDicts (instead of dataclasses) keeps the SDK zero-runtime-cost: requests
5
+ and responses round-trip as plain dicts, but editor / mypy autocomplete still
6
+ works.
7
+
8
+ For analyze / factor-match responses the server returns a rich nested object
9
+ that varies by tier — those are typed as ``Dict[str, Any]`` here so the SDK
10
+ doesn't drift when the response schema is extended. Inspect ``result.keys()``
11
+ or ``json.dumps(result, indent=2)`` on a real call to discover fields.
12
+ """
13
+ from typing import Any, Dict, List, Literal, Optional
14
+
15
+ try:
16
+ # Python 3.11+: TypedDict supports total=False per-key with NotRequired.
17
+ from typing import NotRequired, TypedDict
18
+ except ImportError: # pragma: no cover - Python 3.9 / 3.10
19
+ from typing_extensions import NotRequired, TypedDict # type: ignore[assignment]
20
+
21
+
22
+ # ── Reference / forward windows ────────────────────────────────────────
23
+
24
+ ReferencePeriod = Literal["1d", "2d", "3d", "4d", "5d", "1w", "1m"]
25
+ ForwardPeriod = Literal["1d", "2d", "3d", "4d", "5d", "20d", "1w", "1m"]
26
+ LookbackYears = Literal["1y", "3y", "5y", "10y", "20y", "max"]
27
+ PerfMetric = Literal["win_1d", "win_5d", "win_20d", "mean_1d", "mean_5d", "mean_20d"]
28
+ PerfOperator = Literal["gt", "lt"]
29
+
30
+
31
+ # ── Symbols ────────────────────────────────────────────────────────────
32
+
33
+ class Symbol(TypedDict):
34
+ symbol: str
35
+ name: Optional[str]
36
+ sector: Optional[str]
37
+ is_high_liquidity: bool
38
+ is_etf: bool
39
+
40
+
41
+ class SymbolListResponse(TypedDict):
42
+ count: int
43
+ symbols: List[Symbol]
44
+
45
+
46
+ # ── Analyze / factor-match request shapes ──────────────────────────────
47
+
48
+ class DNAConditions(TypedDict, total=False):
49
+ """DNA filter flags accepted by /analyze and /factor-match.
50
+
51
+ Every key is optional — omit or set ``False`` to ignore a factor. ``daily_change``
52
+ defaults to True server-side. ``magnitude_mode`` only applies when ``magnitude``
53
+ is True.
54
+ """
55
+ daily_change: bool
56
+ magnitude: bool
57
+ magnitude_mode: Literal["add", "replace"]
58
+ vix_level: bool
59
+ vix_move: bool
60
+ regime: bool
61
+ rel_vol: bool
62
+ rsi_zone: bool
63
+ rsi_slope: bool
64
+ structure_precision: bool
65
+ earnings_prox: bool
66
+ streak: bool
67
+ volume_streak: bool
68
+ earnings_perf: bool
69
+ overnight_gap: bool
70
+ analyst_trend: bool
71
+ month_of_year: bool
72
+ macro_risk: bool
73
+
74
+
75
+ class ScreenerFilters(TypedDict, total=False):
76
+ is_etf: bool
77
+ is_crypto: bool
78
+ is_gold_standard: bool
79
+ sector: str
80
+ industry: str
81
+ symbols: List[str]
82
+ price_min: float
83
+ price_max: float
84
+ volume_min: float
85
+
86
+
87
+ class PerfFilter(TypedDict):
88
+ metric: PerfMetric
89
+ operator: PerfOperator
90
+ threshold: float
91
+
92
+
93
+ # Response payloads vary by tier and feature flag — keep them open so the SDK
94
+ # doesn't break when the server adds fields.
95
+ AnalyzeResult = Dict[str, Any]
96
+ FactorMatchResult = Dict[str, Any]
97
+
98
+
99
+ __all__ = [
100
+ "ReferencePeriod",
101
+ "ForwardPeriod",
102
+ "LookbackYears",
103
+ "PerfMetric",
104
+ "PerfOperator",
105
+ "Symbol",
106
+ "SymbolListResponse",
107
+ "DNAConditions",
108
+ "ScreenerFilters",
109
+ "PerfFilter",
110
+ "AnalyzeResult",
111
+ "FactorMatchResult",
112
+ ]