morphiq-sdk 0.1.0__py3-none-any.whl
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.
- morphiq_sdk/__init__.py +89 -0
- morphiq_sdk/client.py +469 -0
- morphiq_sdk/errors.py +102 -0
- morphiq_sdk/market_data.py +91 -0
- morphiq_sdk/models.py +442 -0
- morphiq_sdk/pandas.py +275 -0
- morphiq_sdk/py.typed +0 -0
- morphiq_sdk/workflows.py +264 -0
- morphiq_sdk-0.1.0.dist-info/METADATA +245 -0
- morphiq_sdk-0.1.0.dist-info/RECORD +14 -0
- morphiq_sdk-0.1.0.dist-info/WHEEL +4 -0
- morphiq_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- morphiq_sdk_yahoo/__init__.py +88 -0
- morphiq_sdk_yahoo/py.typed +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Provider-neutral price data. No data fetching or optional dependencies live here."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import UTC, date, datetime
|
|
5
|
+
from math import isfinite
|
|
6
|
+
from typing import Literal, Protocol
|
|
7
|
+
|
|
8
|
+
PriceInterval = Literal["1d", "1m", "5m", "15m", "30m", "60m"]
|
|
9
|
+
PriceAdjustment = Literal["split", "split_and_dividend", "unadjusted"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class PriceBar:
|
|
14
|
+
"""A close with a session date or timezone-aware bar-start label.
|
|
15
|
+
|
|
16
|
+
The label is not an exact quote/close timestamp. A final intraday bar may
|
|
17
|
+
still be forming; it must not automatically become a regime request's as_of.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
time: date | datetime
|
|
21
|
+
close: float
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if not isfinite(self.close) or self.close <= 0:
|
|
25
|
+
raise ValueError("Close must be finite and positive")
|
|
26
|
+
if not isinstance(self.time, date):
|
|
27
|
+
raise ValueError("A price label must be a date or datetime")
|
|
28
|
+
if isinstance(self.time, datetime) and self.time.utcoffset() is None:
|
|
29
|
+
raise ValueError("Intraday timestamps must include a timezone")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class PriceHistory:
|
|
34
|
+
"""Nonempty, oldest-first prices with explicit provider and adjustment identity.
|
|
35
|
+
|
|
36
|
+
Gaps between bars are preserved. No interpolation, resampling or currency
|
|
37
|
+
conversion is implied. Prices retain the source's quoted units.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
symbol: str
|
|
41
|
+
interval: PriceInterval
|
|
42
|
+
adjustment: PriceAdjustment
|
|
43
|
+
source: str
|
|
44
|
+
bars: tuple[PriceBar, ...]
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
if not isinstance(self.bars, tuple) or not self.bars:
|
|
48
|
+
raise ValueError("History requires a nonempty tuple of bars")
|
|
49
|
+
if self.interval not in ("1d", "1m", "5m", "15m", "30m", "60m"):
|
|
50
|
+
raise ValueError("Unsupported price interval")
|
|
51
|
+
if self.adjustment not in ("split", "split_and_dividend", "unadjusted"):
|
|
52
|
+
raise ValueError("Unknown price adjustment")
|
|
53
|
+
if not self.symbol.strip() or not self.source.strip():
|
|
54
|
+
raise ValueError("Symbol and source are required")
|
|
55
|
+
for bar in self.bars:
|
|
56
|
+
intraday_label = isinstance(bar.time, datetime)
|
|
57
|
+
if intraday_label != (self.interval != "1d"):
|
|
58
|
+
raise ValueError("Daily bars need session dates; intraday bars need datetimes")
|
|
59
|
+
labels = [
|
|
60
|
+
bar.time.astimezone(UTC) if isinstance(bar.time, datetime) else bar.time
|
|
61
|
+
for bar in self.bars
|
|
62
|
+
]
|
|
63
|
+
if any(left >= right for left, right in zip(labels, labels[1:], strict=False)):
|
|
64
|
+
raise ValueError("Price labels must be unique and ordered oldest to newest")
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def prices(self) -> tuple[float, ...]:
|
|
68
|
+
return tuple(bar.close for bar in self.bars)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def latest(self) -> PriceBar:
|
|
72
|
+
return self.bars[-1]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PriceProvider(Protocol):
|
|
76
|
+
"""Optional source of regular-session price bars, separate from the API client.
|
|
77
|
+
|
|
78
|
+
Date bounds use the instrument's exchange calendar: start inclusive, end
|
|
79
|
+
exclusive. Providers declare adjustments in the result and raise on invalid
|
|
80
|
+
or unavailable data. Available history can be shorter than the requested
|
|
81
|
+
date range; callers enforce their own required observation count.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def history(
|
|
85
|
+
self,
|
|
86
|
+
symbol: str,
|
|
87
|
+
*,
|
|
88
|
+
start: date,
|
|
89
|
+
end: date,
|
|
90
|
+
interval: PriceInterval = "1d",
|
|
91
|
+
) -> PriceHistory: ...
|
morphiq_sdk/models.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""API wire models for the reviewed HTTP contract; no pricing logic lives here."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, Generic, Literal, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, FiniteFloat, PrivateAttr
|
|
6
|
+
|
|
7
|
+
ExerciseStyle = Literal["european", "american"]
|
|
8
|
+
OptionType = Literal["call", "put"]
|
|
9
|
+
PricingModel = Literal[
|
|
10
|
+
"auto", "black_scholes_merton", "bjerksund_stensland_2002", "bjerksund_stensland_1993"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Request(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="forbid", strict=True, allow_inf_nan=False, frozen=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DividendEvent(_Request):
|
|
19
|
+
"""Cash per share at an ex-dividend time expressed in years from valuation."""
|
|
20
|
+
|
|
21
|
+
ex_time_years: FiniteFloat
|
|
22
|
+
cash_amount: FiniteFloat
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _OptionRequest(_Request):
|
|
26
|
+
"""Common option inputs. Rates and yield are decimals; time is in years.
|
|
27
|
+
|
|
28
|
+
Numeric/model-domain validation belongs to the service. The SDK rejects
|
|
29
|
+
malformed wire types and non-finite values without clamping or repricing.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
exercise_style: ExerciseStyle
|
|
33
|
+
option_type: OptionType
|
|
34
|
+
spot: FiniteFloat
|
|
35
|
+
strike: FiniteFloat
|
|
36
|
+
time_to_expiry_years: FiniteFloat
|
|
37
|
+
risk_free_rate: FiniteFloat
|
|
38
|
+
dividend_yield: FiniteFloat
|
|
39
|
+
pricing_model: PricingModel = "auto"
|
|
40
|
+
model_version: str | None = None
|
|
41
|
+
dividend_schedule: list[DividendEvent] = Field(default_factory=list, max_length=16)
|
|
42
|
+
|
|
43
|
+
def to_wire(self) -> dict[str, Any]:
|
|
44
|
+
"""Validate current inputs, preserving omission and explicit null/empty values."""
|
|
45
|
+
values = self.model_dump(exclude={"dividend_schedule"}, exclude_unset=True)
|
|
46
|
+
# Freezing the model does not freeze lists or track their in-place edits.
|
|
47
|
+
# Include nonempty schedules and revalidate edits before JSON serialization.
|
|
48
|
+
if self.dividend_schedule or "dividend_schedule" in self.model_fields_set:
|
|
49
|
+
values["dividend_schedule"] = self.dividend_schedule
|
|
50
|
+
validated = type(self).model_validate(values)
|
|
51
|
+
result = validated.model_dump(mode="json", exclude_unset=True)
|
|
52
|
+
# This required wire field has a convenience default in Python.
|
|
53
|
+
result["pricing_model"] = self.pricing_model
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PriceRequest(_OptionRequest):
|
|
58
|
+
"""Price an option at a supplied annual volatility, expressed as a decimal."""
|
|
59
|
+
|
|
60
|
+
volatility: FiniteFloat
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ImpliedVolatilityRequest(_OptionRequest):
|
|
64
|
+
"""Solve volatility from a per-share market price in the spot/strike currency.
|
|
65
|
+
|
|
66
|
+
Volatility is an output, not a request field. The service validates premiums
|
|
67
|
+
and numerical domains without SDK-side clamping or local solving.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
market_price: FiniteFloat
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _Response(BaseModel):
|
|
74
|
+
# Retain additive fields. A new resolved model name must not break decoding.
|
|
75
|
+
model_config = ConfigDict(extra="allow", strict=True, allow_inf_nan=False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GreeksRequest(PriceRequest):
|
|
79
|
+
"""Price and all ten Greeks at a supplied volatility, using the pricing inputs.
|
|
80
|
+
|
|
81
|
+
Cash-dividend schedules are forwarded unchanged. The service owns model
|
|
82
|
+
selection, schedule support and numerical-domain validation.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AnalyticsRequest(PriceRequest):
|
|
87
|
+
"""Price and Greeks, optionally evaluated at IV solved from a market premium.
|
|
88
|
+
|
|
89
|
+
The wire contract requires volatility even with a premium; the service uses
|
|
90
|
+
it only when market_price is absent or null. Preserve both fields unchanged.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
market_price: FiniteFloat | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Greeks(_Response):
|
|
97
|
+
"""Unscaled API values: time derivatives per year; volatility/rates per unit.
|
|
98
|
+
|
|
99
|
+
No calendar-day, percentage-point or contract-multiplier conversion occurs.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
delta: FiniteFloat
|
|
103
|
+
gamma: FiniteFloat
|
|
104
|
+
theta: FiniteFloat
|
|
105
|
+
vega: FiniteFloat
|
|
106
|
+
rho: FiniteFloat
|
|
107
|
+
vanna: FiniteFloat
|
|
108
|
+
volga: FiniteFloat
|
|
109
|
+
charm: FiniteFloat
|
|
110
|
+
veta: FiniteFloat
|
|
111
|
+
color: FiniteFloat
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class GreeksResult(_Response):
|
|
115
|
+
price: FiniteFloat
|
|
116
|
+
greeks: Greeks
|
|
117
|
+
pricing_model: str
|
|
118
|
+
model_version: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ImpliedVolatilityResult(_Response):
|
|
122
|
+
"""Unmodified solver diagnostics. Volatility is a decimal fraction.
|
|
123
|
+
|
|
124
|
+
Known residual bases are ``forward_normalised_price`` and ``option_price``;
|
|
125
|
+
retain future basis names without interpreting or converting their values.
|
|
126
|
+
``used_fallback`` describes the algorithm, not solution quality.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
implied_volatility: FiniteFloat
|
|
130
|
+
iterations: int = Field(ge=0)
|
|
131
|
+
used_fallback: bool
|
|
132
|
+
residual: FiniteFloat
|
|
133
|
+
residual_basis: str
|
|
134
|
+
pricing_model: str
|
|
135
|
+
model_version: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AnalyticsResult(GreeksResult):
|
|
139
|
+
"""Combined result; the nested IV solve has no separate commercial charge."""
|
|
140
|
+
|
|
141
|
+
implied_volatility: ImpliedVolatilityResult | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class EngineIdentity(_Response):
|
|
145
|
+
name: str
|
|
146
|
+
version: str
|
|
147
|
+
revision: str
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PolicyMetadata(_Response):
|
|
151
|
+
snapshot_id: str
|
|
152
|
+
revision: str
|
|
153
|
+
plan: str
|
|
154
|
+
records: list[str]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _CommercialMetadata(_Response):
|
|
158
|
+
fcu_quoted: int | None = Field(default=None, ge=0)
|
|
159
|
+
fcu_charged: int | None = Field(default=None, ge=0)
|
|
160
|
+
fcu_schedule_id: str | None = None
|
|
161
|
+
fcu_schedule_version: str | None = None
|
|
162
|
+
policy: PolicyMetadata | None = None
|
|
163
|
+
remaining_fcu_instance_estimate: int | None = Field(default=None, ge=0)
|
|
164
|
+
budget_period: str | None = None
|
|
165
|
+
budget_reset_at: str | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ResponseMetadata(_CommercialMetadata):
|
|
169
|
+
"""Server provenance and optional commercial CU evidence.
|
|
170
|
+
|
|
171
|
+
``units_charged`` is an internal compute weight, not a commercial CU charge.
|
|
172
|
+
Missing ``fcu_charged`` means unreported, not zero.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
request_id: str
|
|
176
|
+
api_version: str
|
|
177
|
+
engine_version: str
|
|
178
|
+
engines: list[EngineIdentity]
|
|
179
|
+
units_charged: int = Field(ge=0)
|
|
180
|
+
compute_schedule_version: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ServiceErrorMetadata(_CommercialMetadata):
|
|
184
|
+
request_id: str
|
|
185
|
+
api_version: str
|
|
186
|
+
service_version: str
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PriceResult(_Response):
|
|
190
|
+
price: FiniteFloat
|
|
191
|
+
exercise_style: ExerciseStyle
|
|
192
|
+
option_type: OptionType
|
|
193
|
+
pricing_model: str
|
|
194
|
+
model_version: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class _EvidenceResponse(_Response):
|
|
198
|
+
_raw_body: bytes = PrivateAttr(default=b"")
|
|
199
|
+
_headers: dict[str, str] = PrivateAttr(default_factory=dict)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def raw_body(self) -> bytes:
|
|
203
|
+
"""JSON response bytes, with any reflected API key redacted."""
|
|
204
|
+
return self._raw_body
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def headers(self) -> dict[str, str]:
|
|
208
|
+
"""HTTP response headers without retaining a credential-bearing request."""
|
|
209
|
+
return self._headers.copy()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class PriceResponse(_EvidenceResponse):
|
|
213
|
+
result: PriceResult
|
|
214
|
+
metadata: ResponseMetadata
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class GreeksResponse(_EvidenceResponse):
|
|
218
|
+
result: GreeksResult
|
|
219
|
+
metadata: ResponseMetadata
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ImpliedVolatilityResponse(_EvidenceResponse):
|
|
223
|
+
result: ImpliedVolatilityResult
|
|
224
|
+
metadata: ResponseMetadata
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class AnalyticsResponse(_EvidenceResponse):
|
|
228
|
+
result: AnalyticsResult
|
|
229
|
+
metadata: ResponseMetadata
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class RegimeRequest(_Request):
|
|
233
|
+
"""Comparable positive prices, oldest first; no sampling interval is assumed.
|
|
234
|
+
|
|
235
|
+
V1 requires 512 prices and consumes the latest 507. Supply split-adjusted
|
|
236
|
+
closes without cash-dividend total-return adjustment. The service validates
|
|
237
|
+
timestamp/domain semantics and request byte limits; no cropping occurs here.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
prices: list[Annotated[FiniteFloat, Field(gt=0)]] = Field(min_length=512)
|
|
241
|
+
as_of: str | None = Field(default=None, max_length=64)
|
|
242
|
+
|
|
243
|
+
def to_wire(self) -> dict[str, Any]:
|
|
244
|
+
# Revalidate mutable price lists without dropping explicit null timestamps.
|
|
245
|
+
values = self.model_dump(exclude={"prices"}, exclude_unset=True)
|
|
246
|
+
values["prices"] = self.prices
|
|
247
|
+
validated = type(self).model_validate(values)
|
|
248
|
+
return validated.model_dump(mode="json", exclude_unset=True)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
RegimeDecisionLabel = Literal["not_eligible", "supported", "unsupported", "unresolved"]
|
|
252
|
+
RegimeDirectionLabel = Literal["UP", "DOWN", "NEUTRAL"]
|
|
253
|
+
RegimeStateLabel = Literal["LOWER_VOL", "HIGHER_VOL"]
|
|
254
|
+
_Label = TypeVar("_Label")
|
|
255
|
+
_Prediction = TypeVar("_Prediction")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class RegimeScore(_Response, Generic[_Label]):
|
|
259
|
+
label: _Label
|
|
260
|
+
value: FiniteFloat = Field(ge=0, le=1)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class RegimePrediction(_Response, Generic[_Label]):
|
|
264
|
+
label: _Label
|
|
265
|
+
scores: list[RegimeScore[_Label]]
|
|
266
|
+
selection: Literal["argmax"]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class RegimeDecisionPrediction(RegimePrediction[RegimeDecisionLabel]):
|
|
270
|
+
scores: list[RegimeScore[RegimeDecisionLabel]] = Field(min_length=4, max_length=4)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class RegimeDirectionPrediction(RegimePrediction[RegimeDirectionLabel]):
|
|
274
|
+
scores: list[RegimeScore[RegimeDirectionLabel]] = Field(min_length=3, max_length=3)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class RegimeStatePrediction(RegimePrediction[RegimeStateLabel]):
|
|
278
|
+
scores: list[RegimeScore[RegimeStateLabel]] = Field(min_length=2, max_length=2)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class RegimeBasis(_Response):
|
|
282
|
+
target: Literal[
|
|
283
|
+
"reference_procedure_outcome",
|
|
284
|
+
"selected_state_tendency",
|
|
285
|
+
"filtered_scale_order_membership",
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class RegimeAvailable(_Response, Generic[_Prediction]):
|
|
290
|
+
status: Literal["available"]
|
|
291
|
+
basis: RegimeBasis
|
|
292
|
+
prediction: _Prediction
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class RegimeNotApplicable(_Response):
|
|
296
|
+
status: Literal["not_applicable"]
|
|
297
|
+
basis: RegimeBasis
|
|
298
|
+
prediction: None
|
|
299
|
+
reason_code: Literal["predicted_not_eligible"]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class RegimeUnavailable(_Response):
|
|
303
|
+
status: Literal["unavailable"]
|
|
304
|
+
basis: RegimeBasis
|
|
305
|
+
prediction: None
|
|
306
|
+
reason_code: Literal["prediction_unavailable"]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class RegimeScoreContract(_Response):
|
|
310
|
+
kind: Literal["uncalibrated_softmax"]
|
|
311
|
+
version: str
|
|
312
|
+
sum_tolerance: FiniteFloat
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class RegimeResult(_Response):
|
|
316
|
+
score_contract: RegimeScoreContract
|
|
317
|
+
decision: Annotated[
|
|
318
|
+
RegimeAvailable[RegimeDecisionPrediction] | RegimeNotApplicable | RegimeUnavailable,
|
|
319
|
+
Field(discriminator="status"),
|
|
320
|
+
]
|
|
321
|
+
direction: Annotated[
|
|
322
|
+
RegimeAvailable[RegimeDirectionPrediction] | RegimeNotApplicable | RegimeUnavailable,
|
|
323
|
+
Field(discriminator="status"),
|
|
324
|
+
]
|
|
325
|
+
state: Annotated[
|
|
326
|
+
RegimeAvailable[RegimeStatePrediction] | RegimeNotApplicable | RegimeUnavailable,
|
|
327
|
+
Field(discriminator="status"),
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class RegimeProvenance(_Response):
|
|
332
|
+
model_id: str
|
|
333
|
+
model_status: str
|
|
334
|
+
model_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
335
|
+
preprocessing: str
|
|
336
|
+
teacher_executed: bool
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class RegimeConsumedPriceRange(_Response):
|
|
340
|
+
start_index: int = Field(ge=0)
|
|
341
|
+
end_index_exclusive: int = Field(ge=0)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class RegimeMetadata(_CommercialMetadata):
|
|
345
|
+
response_contract_version: str
|
|
346
|
+
request_id: str
|
|
347
|
+
api_version: str
|
|
348
|
+
service_version: str
|
|
349
|
+
engines: list[EngineIdentity]
|
|
350
|
+
provenance: RegimeProvenance
|
|
351
|
+
received_price_count: int = Field(ge=0)
|
|
352
|
+
window_price_count: int = Field(ge=0)
|
|
353
|
+
price_count: int = Field(ge=0)
|
|
354
|
+
return_count: int = Field(ge=0)
|
|
355
|
+
consumed_price_range: RegimeConsumedPriceRange
|
|
356
|
+
observation_index: int = Field(ge=0)
|
|
357
|
+
input_price_index: int = Field(ge=0)
|
|
358
|
+
as_of: str | None
|
|
359
|
+
return_type: Literal["log"]
|
|
360
|
+
return_unit: Literal["fraction"]
|
|
361
|
+
inference_time_us: int = Field(ge=0)
|
|
362
|
+
all_prices_consumed: bool
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class RegimeResponse(_EvidenceResponse):
|
|
366
|
+
result: RegimeResult
|
|
367
|
+
metadata: RegimeMetadata
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ErrorDetails(_Response):
|
|
371
|
+
code: str
|
|
372
|
+
message: str
|
|
373
|
+
field: str | None = None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class ErrorEnvelope(_Response):
|
|
377
|
+
error: ErrorDetails
|
|
378
|
+
metadata: ResponseMetadata | ServiceErrorMetadata
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
_OptionRequestType = TypeVar("_OptionRequestType", bound=_OptionRequest)
|
|
382
|
+
_OptionResultType = TypeVar("_OptionResultType", bound=_Response)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class _BatchRequest(_Request, Generic[_OptionRequestType]):
|
|
386
|
+
# This is the reviewed wire limit, not an account/plan limit.
|
|
387
|
+
requests: list[_OptionRequestType] = Field(min_length=1, max_length=1024)
|
|
388
|
+
|
|
389
|
+
def to_wire(self) -> dict[str, Any]:
|
|
390
|
+
validated = type(self).model_validate({"requests": self.requests})
|
|
391
|
+
return {"requests": [request.to_wire() for request in validated.requests]}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class BatchPriceRequest(_BatchRequest[PriceRequest]):
|
|
395
|
+
"""One explicit batch of pricing requests in caller order."""
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class BatchGreeksRequest(_BatchRequest[GreeksRequest]):
|
|
399
|
+
"""One explicit batch of price-and-Greeks requests."""
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class BatchImpliedVolatilityRequest(_BatchRequest[ImpliedVolatilityRequest]):
|
|
403
|
+
"""One explicit batch of IV solves."""
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class BatchAnalyticsRequest(_BatchRequest[AnalyticsRequest]):
|
|
407
|
+
"""One explicit batch of combined analytics requests."""
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class BatchSuccess(_Response, Generic[_OptionResultType]):
|
|
411
|
+
index: int = Field(ge=0)
|
|
412
|
+
status: Literal["success"]
|
|
413
|
+
result: _OptionResultType
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class BatchFailure(_Response):
|
|
417
|
+
index: int = Field(ge=0)
|
|
418
|
+
status: Literal["error"]
|
|
419
|
+
error: ErrorDetails
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class _BatchResponse(_EvidenceResponse, Generic[_OptionResultType]):
|
|
423
|
+
metadata: ResponseMetadata
|
|
424
|
+
results: list[
|
|
425
|
+
Annotated[BatchSuccess[_OptionResultType] | BatchFailure, Field(discriminator="status")]
|
|
426
|
+
]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class BatchPriceResponse(_BatchResponse[PriceResult]):
|
|
430
|
+
"""Ordered outcomes and one metadata/charge block for the entire batch."""
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class BatchGreeksResponse(_BatchResponse[GreeksResult]):
|
|
434
|
+
"""Ordered Greeks outcomes and batch-level charge evidence."""
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class BatchImpliedVolatilityResponse(_BatchResponse[ImpliedVolatilityResult]):
|
|
438
|
+
"""Ordered IV outcomes and batch-level charge evidence."""
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class BatchAnalyticsResponse(_BatchResponse[AnalyticsResult]):
|
|
442
|
+
"""Ordered combined analytics outcomes and batch-level charge evidence."""
|