options-analysis-suite 0.1.0a1__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.
- options_analysis_suite-0.1.0a1/.gitignore +24 -0
- options_analysis_suite-0.1.0a1/CHANGELOG.md +75 -0
- options_analysis_suite-0.1.0a1/LICENSE +21 -0
- options_analysis_suite-0.1.0a1/Makefile +40 -0
- options_analysis_suite-0.1.0a1/PKG-INFO +221 -0
- options_analysis_suite-0.1.0a1/README.md +187 -0
- options_analysis_suite-0.1.0a1/pyproject.toml +100 -0
- options_analysis_suite-0.1.0a1/scripts/gen_models.py +113 -0
- options_analysis_suite-0.1.0a1/src/oas/__init__.py +49 -0
- options_analysis_suite-0.1.0a1/src/oas/_generated/__init__.py +0 -0
- options_analysis_suite-0.1.0a1/src/oas/_generated/models.py +1068 -0
- options_analysis_suite-0.1.0a1/src/oas/_manifest.py +110 -0
- options_analysis_suite-0.1.0a1/src/oas/_transport.py +113 -0
- options_analysis_suite-0.1.0a1/src/oas/calibration.py +275 -0
- options_analysis_suite-0.1.0a1/src/oas/client.py +994 -0
- options_analysis_suite-0.1.0a1/src/oas/credentials.py +57 -0
- options_analysis_suite-0.1.0a1/src/oas/errors.py +129 -0
- options_analysis_suite-0.1.0a1/tests/__init__.py +0 -0
- options_analysis_suite-0.1.0a1/tests/conftest.py +49 -0
- options_analysis_suite-0.1.0a1/tests/fixtures/openapi.snapshot.json +1 -0
- options_analysis_suite-0.1.0a1/tests/test_calibration.py +297 -0
- options_analysis_suite-0.1.0a1/tests/test_client_live.py +26 -0
- options_analysis_suite-0.1.0a1/tests/test_client_mocked.py +223 -0
- options_analysis_suite-0.1.0a1/tests/test_drift.py +108 -0
- options_analysis_suite-0.1.0a1/tests/test_fixture_freshness.py +50 -0
- options_analysis_suite-0.1.0a1/tests/test_methods_mocked.py +273 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
build/
|
|
3
|
+
dist/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
|
|
7
|
+
# Virtual envs
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Caches
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `options-analysis-suite` Python SDK are documented
|
|
4
|
+
in this file.
|
|
5
|
+
|
|
6
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
|
+
and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
|
+
|
|
9
|
+
While the SDK is in `0.x`, the public surface may change between minor
|
|
10
|
+
releases — pin to a specific `0.0.x` if you need stability. Breaking changes
|
|
11
|
+
will be called out in this file with **Breaking** at the start of the bullet.
|
|
12
|
+
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
## [0.1.0a1] — 2026-04-28
|
|
16
|
+
|
|
17
|
+
First public alpha. Architecture decided, full surface implemented, drift
|
|
18
|
+
checked against the deployed OpenAPI spec.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `OASClient` with 49 typed methods covering every typed `/v1/*` operationId
|
|
23
|
+
from the data-api OpenAPI spec.
|
|
24
|
+
- **Compute (9):** `price`, `greeks`, `exposure`, `scenario`, `sensitivity`,
|
|
25
|
+
`max_pain`, `expected_move`, `probability` (+ `probability_simple` /
|
|
26
|
+
`probability_full` typed helpers), `calibrate`.
|
|
27
|
+
- **Data (40):** snapshot, metrics, regime, IV surface, Greeks history,
|
|
28
|
+
scanner, market structure (ATS/OTC/blocks), calendars (economic/IPO/
|
|
29
|
+
dividend/split), corporate actions, news, history, FRED, Treasury
|
|
30
|
+
auctions, bond ETF + TRACE.
|
|
31
|
+
- `Calibration` domain helper that wraps a `/v1/compute/calibrate` result.
|
|
32
|
+
- `cal.price(...)` / `cal.greeks(...)` forward `modelParams=cal.params`
|
|
33
|
+
automatically.
|
|
34
|
+
- `cal.save("path.json")` / `Calibration.from_json("path.json", client=...)`
|
|
35
|
+
for durable persistence; the 30-second-only `calibrationId` is
|
|
36
|
+
intentionally not surfaced.
|
|
37
|
+
- `_format_version=1` written to disk; loading newer files raises
|
|
38
|
+
`ValueError`.
|
|
39
|
+
- Symbol forwarding to `/price` / `/greeks` is suppressed only when the
|
|
40
|
+
caller supplies full numeric inputs **including** numeric `t`, so
|
|
41
|
+
compute-only API keys don't trip the server's data-scope check.
|
|
42
|
+
- `iter_metrics(symbols, batch_size=50)` — chunks a large symbol list across
|
|
43
|
+
`/v1/data/metrics/batch` calls.
|
|
44
|
+
- Typed exceptions: `OASError`, `AuthenticationError`, `ValidationError`,
|
|
45
|
+
`PermissionDeniedError` (with `.required_scope`), `NotFoundError`,
|
|
46
|
+
`RateLimitError` (with `.retry_after`, `.bucket`), `CalibrationQuotaError`
|
|
47
|
+
(with `.resets_at`), `ConcurrencyLimitError` (with `.current`, `.max`),
|
|
48
|
+
`ServerError`.
|
|
49
|
+
- `BrokerCredentials` BYOK helpers: `TradierCredentials(token=...)`,
|
|
50
|
+
`TastytradeCredentials(refresh_token=..., client_secret=...)`.
|
|
51
|
+
- Generated Pydantic v2 response models in `oas._generated.models`
|
|
52
|
+
(88 classes from the OpenAPI spec). Post-processed to use
|
|
53
|
+
`extra='ignore'` so additive server fields don't break older SDK
|
|
54
|
+
versions.
|
|
55
|
+
- Drift-test suite (`tests/test_drift.py`) gates SDK coverage against
|
|
56
|
+
`/openapi.json`. Strict-equality gate enabled.
|
|
57
|
+
- Pinned spec fixture at `tests/fixtures/openapi.snapshot.json` for offline
|
|
58
|
+
test runs; `make test-live` checks freshness against deployed prod.
|
|
59
|
+
|
|
60
|
+
### Notes
|
|
61
|
+
|
|
62
|
+
- `py.typed` is intentionally **not** shipped in this release: the
|
|
63
|
+
hand-written surface (client/errors/credentials/transport) is mypy-strict
|
|
64
|
+
clean, but the auto-generated module has type-check warnings that won't
|
|
65
|
+
resolve until `datamodel-code-generator` produces fully mypy-clean output.
|
|
66
|
+
Methods still return typed Pydantic models at runtime.
|
|
67
|
+
- `Typing :: Typed` classifier withheld for the same reason.
|
|
68
|
+
- ListResponse-backed methods return `dict[str, Any]` so server-side
|
|
69
|
+
top-level metadata (date, page, count, direction, etc.) is preserved.
|
|
70
|
+
These will tighten to typed models as endpoint-specific schemas land
|
|
71
|
+
upstream.
|
|
72
|
+
- Sync-only client. `AsyncOASClient` deferred until there's a user need.
|
|
73
|
+
|
|
74
|
+
[Unreleased]: https://github.com/Options-Analysis-Suite/options-analysis-suite-python/compare/v0.1.0a1...HEAD
|
|
75
|
+
[0.1.0a1]: https://github.com/Options-Analysis-Suite/options-analysis-suite-python/releases/tag/v0.1.0a1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Options Analysis Suite
|
|
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,40 @@
|
|
|
1
|
+
.PHONY: install gen lint typecheck test test-live build clean
|
|
2
|
+
|
|
3
|
+
# Lint/typecheck/test targets route through ./.venv/bin/* so they work
|
|
4
|
+
# without activating the virtualenv first. `make install` provisions .venv
|
|
5
|
+
# via uv. `make build` calls `uv build` directly (no .venv lookup needed).
|
|
6
|
+
PY := .venv/bin/python
|
|
7
|
+
PYTEST := .venv/bin/pytest
|
|
8
|
+
RUFF := .venv/bin/ruff
|
|
9
|
+
MYPY := .venv/bin/mypy
|
|
10
|
+
|
|
11
|
+
install:
|
|
12
|
+
uv venv --python 3.12 .venv
|
|
13
|
+
uv pip install --python .venv -e ".[dev,codegen]"
|
|
14
|
+
|
|
15
|
+
# Re-derive Pydantic models from the live OpenAPI spec. Requires datamodel-codegen.
|
|
16
|
+
gen:
|
|
17
|
+
$(PY) scripts/gen_models.py
|
|
18
|
+
|
|
19
|
+
lint:
|
|
20
|
+
$(RUFF) check src tests scripts
|
|
21
|
+
|
|
22
|
+
typecheck:
|
|
23
|
+
$(MYPY)
|
|
24
|
+
|
|
25
|
+
# Unit + drift tests (no network). Reads tests/fixtures/openapi.snapshot.json by
|
|
26
|
+
# default; set OAS_OPENAPI_PATH=- to force a live fetch instead.
|
|
27
|
+
test:
|
|
28
|
+
$(PYTEST) -m "not live" -v
|
|
29
|
+
|
|
30
|
+
# Integration test against the deployed API. Requires OAS_API_KEY in env.
|
|
31
|
+
# Also runs the fixture-freshness check that diffs the pinned snapshot against
|
|
32
|
+
# the live /openapi.json — refresh tests/fixtures/openapi.snapshot.json if it fails.
|
|
33
|
+
test-live:
|
|
34
|
+
$(PYTEST) -m live -v
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
uv build
|
|
38
|
+
|
|
39
|
+
clean:
|
|
40
|
+
rm -rf build dist *.egg-info src/oas.egg-info .pytest_cache .mypy_cache .ruff_cache
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: options-analysis-suite
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Python SDK for the Options Analysis Suite API: 17 pricing models, auto-calibration, GEX/DEX exposure, IV surfaces, and pre-computed market data.
|
|
5
|
+
Project-URL: Homepage, https://optionsanalysissuite.com
|
|
6
|
+
Project-URL: Documentation, https://data.optionsanalysissuite.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Options-Analysis-Suite/options-analysis-suite-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/Options-Analysis-Suite/options-analysis-suite-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Options Analysis Suite
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: calibration,exposure,greeks,heston,options,quant,sabr,trading
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
24
|
+
Requires-Dist: pydantic<3.0,>=2.5
|
|
25
|
+
Provides-Extra: codegen
|
|
26
|
+
Requires-Dist: datamodel-code-generator>=0.26; extra == 'codegen'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
30
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Options Analysis Suite — Python SDK
|
|
36
|
+
|
|
37
|
+
Type-safe Python client for the [Options Analysis Suite API](https://optionsanalysissuite.com).
|
|
38
|
+
|
|
39
|
+
> **Status: alpha (`0.1.0a1`)** — full coverage of every typed `/v1/*`
|
|
40
|
+
> operationId, plus a `Calibration` domain helper. Drift-checked against
|
|
41
|
+
> the deployed OpenAPI spec.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install options-analysis-suite
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> Available on PyPI after the first `v*` tag is published. Until then,
|
|
50
|
+
> install from source with `pip install -e .` from a repo checkout.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from oas import OASClient, TradierCredentials
|
|
56
|
+
|
|
57
|
+
with OASClient(api_key="oas_live_...") as client:
|
|
58
|
+
# Data: cached EOD analytics
|
|
59
|
+
snap = client.snapshot("SPY")
|
|
60
|
+
print(snap.atmIv, snap.netGex, snap.maxPain)
|
|
61
|
+
|
|
62
|
+
if snap.maxPainCurve:
|
|
63
|
+
for row in snap.maxPainCurve:
|
|
64
|
+
print(row.strike, row.totalPain)
|
|
65
|
+
|
|
66
|
+
# Compute: 17 pricing models, full Greeks, exposure, expected move...
|
|
67
|
+
price = client.price(model="bs", is_call=True, S=650, K=650, r=0.05,
|
|
68
|
+
q=0.012, sigma=0.15, t=0.25)
|
|
69
|
+
greeks = client.greeks(model="heston", is_call=True, S=650, K=650, r=0.05,
|
|
70
|
+
q=0.012, sigma=0.15, t=0.25)
|
|
71
|
+
|
|
72
|
+
# Calibrate once, persist, reuse — never re-touches the calibrationId TTL.
|
|
73
|
+
cal = client.calibrate(
|
|
74
|
+
"SPY", model="heston",
|
|
75
|
+
broker=TradierCredentials(token="..."),
|
|
76
|
+
)
|
|
77
|
+
cal.save("spy_heston.json")
|
|
78
|
+
|
|
79
|
+
# Evaluate the calibrated model anywhere across the chain.
|
|
80
|
+
fair = cal.price(is_call=True, K=655, expiry="2026-06-19")
|
|
81
|
+
|
|
82
|
+
# Stream batched metrics without manually paging.
|
|
83
|
+
for m in client.iter_metrics(["SPY", "QQQ", "IWM", "DIA"], batch_size=50):
|
|
84
|
+
print(m.symbol, m.ivRank)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Calibration round-trip
|
|
88
|
+
|
|
89
|
+
A `Calibration` is the durable wrapper around a `/v1/compute/calibrate` result.
|
|
90
|
+
The fitted `params` dict survives a JSON round-trip; the 30-second-only
|
|
91
|
+
`calibrationId` is intentionally not surfaced.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Load a saved calibration in another process / hours later.
|
|
95
|
+
from oas import Calibration, OASClient
|
|
96
|
+
|
|
97
|
+
cal = Calibration.from_json("spy_heston.json")
|
|
98
|
+
with OASClient(api_key="oas_live_...") as client:
|
|
99
|
+
cal.bind(client) # attach so cal.price() / cal.greeks() can fire HTTP
|
|
100
|
+
price = cal.price(is_call=True, K=650, S=650, r=0.05, q=0.012,
|
|
101
|
+
sigma=0.15, t=0.25)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Errors
|
|
105
|
+
|
|
106
|
+
Every error subclass carries the HTTP status, the server's structured `code`
|
|
107
|
+
field (when present), and any extra fields the server returned.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from oas.errors import NotFoundError, RateLimitError, CalibrationQuotaError
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
snap = client.snapshot("UNKNOWN")
|
|
114
|
+
except NotFoundError as e:
|
|
115
|
+
print(f"warehouse miss: {e}")
|
|
116
|
+
except RateLimitError as e:
|
|
117
|
+
print(f"slow down — retry in {e.retry_after}s (bucket: {e.bucket})")
|
|
118
|
+
except CalibrationQuotaError as e:
|
|
119
|
+
print(f"calibration quota exhausted; resets at {e.resets_at}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
See [`src/oas/errors.py`](src/oas/errors.py) for the full hierarchy.
|
|
123
|
+
|
|
124
|
+
## Models
|
|
125
|
+
|
|
126
|
+
Response models live in `oas._generated.models` and are auto-generated from
|
|
127
|
+
the OpenAPI spec at build time. The generated classes use `extra='ignore'`
|
|
128
|
+
so additive server fields (e.g., a new metric in `MetricsResponse`) don't
|
|
129
|
+
break older SDK versions — older SDKs will simply omit unknown fields.
|
|
130
|
+
|
|
131
|
+
To regenerate against the latest spec:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
make gen
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Set up dev env
|
|
141
|
+
make install
|
|
142
|
+
|
|
143
|
+
# Lint / typecheck / test
|
|
144
|
+
make lint
|
|
145
|
+
make typecheck
|
|
146
|
+
make test # unit + drift, no network. Uses tests/fixtures/openapi.snapshot.json
|
|
147
|
+
make test-live # integration (needs OAS_API_KEY env var). Also re-checks the
|
|
148
|
+
# pinned spec fixture against the deployed /openapi.json.
|
|
149
|
+
# Refresh the fixture with:
|
|
150
|
+
# curl -s https://data.optionsanalysissuite.com/openapi.json \
|
|
151
|
+
# > tests/fixtures/openapi.snapshot.json
|
|
152
|
+
|
|
153
|
+
# Build distribution
|
|
154
|
+
make build
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Drift gate
|
|
158
|
+
|
|
159
|
+
The SDK's `_manifest.py` registers every operationId → `OASClient` method
|
|
160
|
+
pairing. `tests/test_drift.py` runs four checks against the spec:
|
|
161
|
+
|
|
162
|
+
1. Every `ENDPOINTS` operationId actually exists in `/openapi.json`
|
|
163
|
+
2. Every registered `Endpoint.sdk_method` resolves to a real `OASClient` attr
|
|
164
|
+
3. Every registered `Endpoint.path` is a path in the spec
|
|
165
|
+
4. **Strict gate**: spec ops − ignored − expected-missing == `ENDPOINTS` keys.
|
|
166
|
+
Catches both unfinished stubs left in the allowlist and surprise spec
|
|
167
|
+
additions.
|
|
168
|
+
|
|
169
|
+
The `EXPECTED_MISSING_OPERATION_IDS` allowlist is **empty** — the SDK
|
|
170
|
+
covers every typed operation. The `live` test suite double-checks that
|
|
171
|
+
the pinned `tests/fixtures/openapi.snapshot.json` still matches deployed
|
|
172
|
+
prod.
|
|
173
|
+
|
|
174
|
+
## Release process (maintainers)
|
|
175
|
+
|
|
176
|
+
Releases are tag-driven via the `publish.yml` GitHub Actions workflow.
|
|
177
|
+
The publish job uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
|
|
178
|
+
(OIDC) — no API tokens are stored anywhere.
|
|
179
|
+
|
|
180
|
+
**One-time setup (on PyPI):**
|
|
181
|
+
|
|
182
|
+
1. Visit https://pypi.org/manage/account/publishing/
|
|
183
|
+
2. Add a pending publisher with:
|
|
184
|
+
- Owner: `Options-Analysis-Suite`
|
|
185
|
+
- Repository: `options-analysis-suite-python`
|
|
186
|
+
- Workflow filename: `publish.yml`
|
|
187
|
+
- Environment: `pypi`
|
|
188
|
+
3. (After the first publish, the project will exist on PyPI and the
|
|
189
|
+
pending publisher becomes a regular trusted publisher.)
|
|
190
|
+
|
|
191
|
+
**One-time setup (on GitHub):**
|
|
192
|
+
|
|
193
|
+
The publish job runs in a GitHub environment named `pypi` (see
|
|
194
|
+
`environment: pypi` in `publish.yml`). Create it under **Settings →
|
|
195
|
+
Environments → New environment**. Optional but recommended: require
|
|
196
|
+
manual approval, restrict to the `main` branch, and add reviewers so a
|
|
197
|
+
stray tag push can't ship to PyPI without a human in the loop.
|
|
198
|
+
|
|
199
|
+
**Per-release:**
|
|
200
|
+
|
|
201
|
+
1. Bump `version` in `pyproject.toml` to the new version
|
|
202
|
+
(e.g. `0.1.0a1` → `0.1.0`).
|
|
203
|
+
2. Update `CHANGELOG.md` under the new version heading; move
|
|
204
|
+
`[Unreleased]` items into it.
|
|
205
|
+
3. Commit (`chore: release vX.Y.Z`).
|
|
206
|
+
4. Tag the commit: `git tag vX.Y.Z` (the `v` prefix is required — the
|
|
207
|
+
publish workflow only triggers on tags matching `v*`).
|
|
208
|
+
5. Push the tag: `git push origin vX.Y.Z`.
|
|
209
|
+
|
|
210
|
+
The workflow:
|
|
211
|
+
- Verifies the tag's version matches `pyproject.toml`
|
|
212
|
+
- Runs lint + typecheck + offline tests
|
|
213
|
+
- Builds sdist + wheel via `uv build`
|
|
214
|
+
- Publishes to PyPI through the OIDC trusted publisher
|
|
215
|
+
|
|
216
|
+
If the tag-vs-pyproject guard fails, fix `pyproject.toml`, retag with
|
|
217
|
+
`git tag -d` + recreate, and re-push.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Options Analysis Suite — Python SDK
|
|
2
|
+
|
|
3
|
+
Type-safe Python client for the [Options Analysis Suite API](https://optionsanalysissuite.com).
|
|
4
|
+
|
|
5
|
+
> **Status: alpha (`0.1.0a1`)** — full coverage of every typed `/v1/*`
|
|
6
|
+
> operationId, plus a `Calibration` domain helper. Drift-checked against
|
|
7
|
+
> the deployed OpenAPI spec.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install options-analysis-suite
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> Available on PyPI after the first `v*` tag is published. Until then,
|
|
16
|
+
> install from source with `pip install -e .` from a repo checkout.
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from oas import OASClient, TradierCredentials
|
|
22
|
+
|
|
23
|
+
with OASClient(api_key="oas_live_...") as client:
|
|
24
|
+
# Data: cached EOD analytics
|
|
25
|
+
snap = client.snapshot("SPY")
|
|
26
|
+
print(snap.atmIv, snap.netGex, snap.maxPain)
|
|
27
|
+
|
|
28
|
+
if snap.maxPainCurve:
|
|
29
|
+
for row in snap.maxPainCurve:
|
|
30
|
+
print(row.strike, row.totalPain)
|
|
31
|
+
|
|
32
|
+
# Compute: 17 pricing models, full Greeks, exposure, expected move...
|
|
33
|
+
price = client.price(model="bs", is_call=True, S=650, K=650, r=0.05,
|
|
34
|
+
q=0.012, sigma=0.15, t=0.25)
|
|
35
|
+
greeks = client.greeks(model="heston", is_call=True, S=650, K=650, r=0.05,
|
|
36
|
+
q=0.012, sigma=0.15, t=0.25)
|
|
37
|
+
|
|
38
|
+
# Calibrate once, persist, reuse — never re-touches the calibrationId TTL.
|
|
39
|
+
cal = client.calibrate(
|
|
40
|
+
"SPY", model="heston",
|
|
41
|
+
broker=TradierCredentials(token="..."),
|
|
42
|
+
)
|
|
43
|
+
cal.save("spy_heston.json")
|
|
44
|
+
|
|
45
|
+
# Evaluate the calibrated model anywhere across the chain.
|
|
46
|
+
fair = cal.price(is_call=True, K=655, expiry="2026-06-19")
|
|
47
|
+
|
|
48
|
+
# Stream batched metrics without manually paging.
|
|
49
|
+
for m in client.iter_metrics(["SPY", "QQQ", "IWM", "DIA"], batch_size=50):
|
|
50
|
+
print(m.symbol, m.ivRank)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Calibration round-trip
|
|
54
|
+
|
|
55
|
+
A `Calibration` is the durable wrapper around a `/v1/compute/calibrate` result.
|
|
56
|
+
The fitted `params` dict survives a JSON round-trip; the 30-second-only
|
|
57
|
+
`calibrationId` is intentionally not surfaced.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# Load a saved calibration in another process / hours later.
|
|
61
|
+
from oas import Calibration, OASClient
|
|
62
|
+
|
|
63
|
+
cal = Calibration.from_json("spy_heston.json")
|
|
64
|
+
with OASClient(api_key="oas_live_...") as client:
|
|
65
|
+
cal.bind(client) # attach so cal.price() / cal.greeks() can fire HTTP
|
|
66
|
+
price = cal.price(is_call=True, K=650, S=650, r=0.05, q=0.012,
|
|
67
|
+
sigma=0.15, t=0.25)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Errors
|
|
71
|
+
|
|
72
|
+
Every error subclass carries the HTTP status, the server's structured `code`
|
|
73
|
+
field (when present), and any extra fields the server returned.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from oas.errors import NotFoundError, RateLimitError, CalibrationQuotaError
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
snap = client.snapshot("UNKNOWN")
|
|
80
|
+
except NotFoundError as e:
|
|
81
|
+
print(f"warehouse miss: {e}")
|
|
82
|
+
except RateLimitError as e:
|
|
83
|
+
print(f"slow down — retry in {e.retry_after}s (bucket: {e.bucket})")
|
|
84
|
+
except CalibrationQuotaError as e:
|
|
85
|
+
print(f"calibration quota exhausted; resets at {e.resets_at}")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
See [`src/oas/errors.py`](src/oas/errors.py) for the full hierarchy.
|
|
89
|
+
|
|
90
|
+
## Models
|
|
91
|
+
|
|
92
|
+
Response models live in `oas._generated.models` and are auto-generated from
|
|
93
|
+
the OpenAPI spec at build time. The generated classes use `extra='ignore'`
|
|
94
|
+
so additive server fields (e.g., a new metric in `MetricsResponse`) don't
|
|
95
|
+
break older SDK versions — older SDKs will simply omit unknown fields.
|
|
96
|
+
|
|
97
|
+
To regenerate against the latest spec:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
make gen
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Set up dev env
|
|
107
|
+
make install
|
|
108
|
+
|
|
109
|
+
# Lint / typecheck / test
|
|
110
|
+
make lint
|
|
111
|
+
make typecheck
|
|
112
|
+
make test # unit + drift, no network. Uses tests/fixtures/openapi.snapshot.json
|
|
113
|
+
make test-live # integration (needs OAS_API_KEY env var). Also re-checks the
|
|
114
|
+
# pinned spec fixture against the deployed /openapi.json.
|
|
115
|
+
# Refresh the fixture with:
|
|
116
|
+
# curl -s https://data.optionsanalysissuite.com/openapi.json \
|
|
117
|
+
# > tests/fixtures/openapi.snapshot.json
|
|
118
|
+
|
|
119
|
+
# Build distribution
|
|
120
|
+
make build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Drift gate
|
|
124
|
+
|
|
125
|
+
The SDK's `_manifest.py` registers every operationId → `OASClient` method
|
|
126
|
+
pairing. `tests/test_drift.py` runs four checks against the spec:
|
|
127
|
+
|
|
128
|
+
1. Every `ENDPOINTS` operationId actually exists in `/openapi.json`
|
|
129
|
+
2. Every registered `Endpoint.sdk_method` resolves to a real `OASClient` attr
|
|
130
|
+
3. Every registered `Endpoint.path` is a path in the spec
|
|
131
|
+
4. **Strict gate**: spec ops − ignored − expected-missing == `ENDPOINTS` keys.
|
|
132
|
+
Catches both unfinished stubs left in the allowlist and surprise spec
|
|
133
|
+
additions.
|
|
134
|
+
|
|
135
|
+
The `EXPECTED_MISSING_OPERATION_IDS` allowlist is **empty** — the SDK
|
|
136
|
+
covers every typed operation. The `live` test suite double-checks that
|
|
137
|
+
the pinned `tests/fixtures/openapi.snapshot.json` still matches deployed
|
|
138
|
+
prod.
|
|
139
|
+
|
|
140
|
+
## Release process (maintainers)
|
|
141
|
+
|
|
142
|
+
Releases are tag-driven via the `publish.yml` GitHub Actions workflow.
|
|
143
|
+
The publish job uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
|
|
144
|
+
(OIDC) — no API tokens are stored anywhere.
|
|
145
|
+
|
|
146
|
+
**One-time setup (on PyPI):**
|
|
147
|
+
|
|
148
|
+
1. Visit https://pypi.org/manage/account/publishing/
|
|
149
|
+
2. Add a pending publisher with:
|
|
150
|
+
- Owner: `Options-Analysis-Suite`
|
|
151
|
+
- Repository: `options-analysis-suite-python`
|
|
152
|
+
- Workflow filename: `publish.yml`
|
|
153
|
+
- Environment: `pypi`
|
|
154
|
+
3. (After the first publish, the project will exist on PyPI and the
|
|
155
|
+
pending publisher becomes a regular trusted publisher.)
|
|
156
|
+
|
|
157
|
+
**One-time setup (on GitHub):**
|
|
158
|
+
|
|
159
|
+
The publish job runs in a GitHub environment named `pypi` (see
|
|
160
|
+
`environment: pypi` in `publish.yml`). Create it under **Settings →
|
|
161
|
+
Environments → New environment**. Optional but recommended: require
|
|
162
|
+
manual approval, restrict to the `main` branch, and add reviewers so a
|
|
163
|
+
stray tag push can't ship to PyPI without a human in the loop.
|
|
164
|
+
|
|
165
|
+
**Per-release:**
|
|
166
|
+
|
|
167
|
+
1. Bump `version` in `pyproject.toml` to the new version
|
|
168
|
+
(e.g. `0.1.0a1` → `0.1.0`).
|
|
169
|
+
2. Update `CHANGELOG.md` under the new version heading; move
|
|
170
|
+
`[Unreleased]` items into it.
|
|
171
|
+
3. Commit (`chore: release vX.Y.Z`).
|
|
172
|
+
4. Tag the commit: `git tag vX.Y.Z` (the `v` prefix is required — the
|
|
173
|
+
publish workflow only triggers on tags matching `v*`).
|
|
174
|
+
5. Push the tag: `git push origin vX.Y.Z`.
|
|
175
|
+
|
|
176
|
+
The workflow:
|
|
177
|
+
- Verifies the tag's version matches `pyproject.toml`
|
|
178
|
+
- Runs lint + typecheck + offline tests
|
|
179
|
+
- Builds sdist + wheel via `uv build`
|
|
180
|
+
- Publishes to PyPI through the OIDC trusted publisher
|
|
181
|
+
|
|
182
|
+
If the tag-vs-pyproject guard fails, fix `pyproject.toml`, retag with
|
|
183
|
+
`git tag -d` + recreate, and re-push.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "options-analysis-suite"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Python SDK for the Options Analysis Suite API: 17 pricing models, auto-calibration, GEX/DEX exposure, IV surfaces, and pre-computed market data."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Options Analysis Suite" }]
|
|
13
|
+
keywords = ["options", "trading", "quant", "greeks", "exposure", "calibration", "heston", "sabr"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
24
|
+
]
|
|
25
|
+
# Note: no Typing :: Typed classifier and no `py.typed` marker until the
|
|
26
|
+
# datamodel-codegen output is mypy-clean. The hand-written surface
|
|
27
|
+
# (client.py, errors.py, credentials.py, _transport.py) is fully typed and
|
|
28
|
+
# passes `mypy --strict`; the generated module is excluded.
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.27,<1.0",
|
|
31
|
+
"pydantic>=2.5,<3.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7.4",
|
|
37
|
+
"respx>=0.20",
|
|
38
|
+
"ruff>=0.5",
|
|
39
|
+
"mypy>=1.8",
|
|
40
|
+
"twine>=5.0",
|
|
41
|
+
]
|
|
42
|
+
codegen = [
|
|
43
|
+
"datamodel-code-generator>=0.26",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://optionsanalysissuite.com"
|
|
48
|
+
Documentation = "https://data.optionsanalysissuite.com/docs"
|
|
49
|
+
Repository = "https://github.com/Options-Analysis-Suite/options-analysis-suite-python"
|
|
50
|
+
Changelog = "https://github.com/Options-Analysis-Suite/options-analysis-suite-python/blob/main/CHANGELOG.md"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/oas"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
include = [
|
|
57
|
+
"src/oas",
|
|
58
|
+
"tests",
|
|
59
|
+
"scripts",
|
|
60
|
+
"README.md",
|
|
61
|
+
"CHANGELOG.md",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
"Makefile",
|
|
64
|
+
"pyproject.toml",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
line-length = 100
|
|
69
|
+
target-version = "py310"
|
|
70
|
+
# Generated models are not hand-written; lint them only via the codegen script's
|
|
71
|
+
# post-processing — not by hand-editing the file.
|
|
72
|
+
extend-exclude = ["src/oas/_generated"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "W", "I", "B", "UP", "N"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint.per-file-ignores]
|
|
78
|
+
# Pricing-convention argument names: S=spot, K=strike, etc. Allowed everywhere
|
|
79
|
+
# the math actually demands them; renaming to lowercase would obscure the
|
|
80
|
+
# Black-Scholes correspondence.
|
|
81
|
+
"src/oas/client.py" = ["N803"]
|
|
82
|
+
"src/oas/calibration.py" = ["N803"]
|
|
83
|
+
# _manifest.py is a hand-aligned table mapping operationIds → Endpoint(...)
|
|
84
|
+
# entries. The visual alignment is more valuable than a 100-char wrap.
|
|
85
|
+
"src/oas/_manifest.py" = ["E501"]
|
|
86
|
+
|
|
87
|
+
[tool.pytest.ini_options]
|
|
88
|
+
testpaths = ["tests"]
|
|
89
|
+
markers = [
|
|
90
|
+
"live: integration tests that hit the deployed API (require OAS_API_KEY env var)",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[tool.mypy]
|
|
94
|
+
python_version = "3.10"
|
|
95
|
+
strict = true
|
|
96
|
+
files = ["src/oas"]
|
|
97
|
+
# datamodel-codegen output uses old-style `confloat(...)` / `constr(...)` annotations
|
|
98
|
+
# that mypy doesn't accept as type expressions even though Pydantic handles them at
|
|
99
|
+
# runtime. The generated file is mechanically produced, so don't gate dev on it.
|
|
100
|
+
exclude = ["src/oas/_generated/.*"]
|