quantbt-engine 1.0.7__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.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Versioned option venue conventions.
|
|
3
|
+
|
|
4
|
+
These conventions are descriptive configuration, not a pricing engine and not a
|
|
5
|
+
claim that venue portfolio margin is exactly replicated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, Tuple
|
|
12
|
+
|
|
13
|
+
from .schema import ExerciseStyle, PremiumConvention, SettlementStyle
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class OptionVenueConvention:
|
|
18
|
+
venue: str
|
|
19
|
+
convention_id: str
|
|
20
|
+
premium_convention: PremiumConvention
|
|
21
|
+
exercise_style: ExerciseStyle
|
|
22
|
+
settlement_style: SettlementStyle
|
|
23
|
+
premium_currency: str
|
|
24
|
+
settlement_currency: str
|
|
25
|
+
quote_currency: str
|
|
26
|
+
supported_underlyings: Tuple[str, ...] = ()
|
|
27
|
+
fee_schedule_id: str = ""
|
|
28
|
+
margin_schedule_id: str = ""
|
|
29
|
+
exact_venue_margin: bool = False
|
|
30
|
+
notes: str = ""
|
|
31
|
+
metadata: Dict = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
object.__setattr__(self, "venue", str(self.venue).lower().strip())
|
|
35
|
+
object.__setattr__(self, "premium_convention", _coerce(PremiumConvention, self.premium_convention, "premium_convention"))
|
|
36
|
+
object.__setattr__(self, "exercise_style", _coerce(ExerciseStyle, self.exercise_style, "exercise_style"))
|
|
37
|
+
object.__setattr__(self, "settlement_style", _coerce(SettlementStyle, self.settlement_style, "settlement_style"))
|
|
38
|
+
if not self.venue or not self.convention_id:
|
|
39
|
+
raise ValueError("venue and convention_id are required")
|
|
40
|
+
for field_name in ("premium_currency", "settlement_currency", "quote_currency"):
|
|
41
|
+
value = getattr(self, field_name)
|
|
42
|
+
if not value:
|
|
43
|
+
raise ValueError(f"{field_name} is required")
|
|
44
|
+
object.__setattr__(self, field_name, str(value).upper())
|
|
45
|
+
object.__setattr__(self, "supported_underlyings", tuple(str(value).upper() for value in self.supported_underlyings))
|
|
46
|
+
_validate_convention(self)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def signature(self) -> Tuple:
|
|
50
|
+
return (
|
|
51
|
+
self.venue,
|
|
52
|
+
self.convention_id,
|
|
53
|
+
self.premium_convention.value,
|
|
54
|
+
self.exercise_style.value,
|
|
55
|
+
self.settlement_style.value,
|
|
56
|
+
self.premium_currency,
|
|
57
|
+
self.settlement_currency,
|
|
58
|
+
self.quote_currency,
|
|
59
|
+
self.supported_underlyings,
|
|
60
|
+
self.fee_schedule_id,
|
|
61
|
+
self.margin_schedule_id,
|
|
62
|
+
bool(self.exact_venue_margin),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def deribit_inverse_option_convention(
|
|
67
|
+
*,
|
|
68
|
+
underlying: str = "BTC",
|
|
69
|
+
version: str = "deribit_inverse_v1",
|
|
70
|
+
) -> OptionVenueConvention:
|
|
71
|
+
base = str(underlying).upper()
|
|
72
|
+
if base not in {"BTC", "ETH"}:
|
|
73
|
+
raise ValueError("Deribit inverse convention currently supports BTC or ETH")
|
|
74
|
+
return OptionVenueConvention(
|
|
75
|
+
venue="deribit",
|
|
76
|
+
convention_id=version,
|
|
77
|
+
premium_convention=PremiumConvention.INVERSE_BASE,
|
|
78
|
+
exercise_style=ExerciseStyle.EUROPEAN,
|
|
79
|
+
settlement_style=SettlementStyle.CASH,
|
|
80
|
+
premium_currency=base,
|
|
81
|
+
settlement_currency=base,
|
|
82
|
+
quote_currency="USD",
|
|
83
|
+
supported_underlyings=(base,),
|
|
84
|
+
fee_schedule_id=f"deribit_{base.lower()}_inverse_options",
|
|
85
|
+
margin_schedule_id="deribit_pm_external_or_scenario_approximation",
|
|
86
|
+
exact_venue_margin=False,
|
|
87
|
+
notes="Inverse premium and settlement are in base currency; native margin is approximation unless validated externally.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def deribit_linear_usdc_option_convention(
|
|
92
|
+
*,
|
|
93
|
+
underlying: str = "BTC",
|
|
94
|
+
version: str = "deribit_linear_usdc_v1",
|
|
95
|
+
settlement_style: SettlementStyle = SettlementStyle.FUTURE_THEN_CASH,
|
|
96
|
+
) -> OptionVenueConvention:
|
|
97
|
+
base = str(underlying).upper()
|
|
98
|
+
return OptionVenueConvention(
|
|
99
|
+
venue="deribit",
|
|
100
|
+
convention_id=version,
|
|
101
|
+
premium_convention=PremiumConvention.LINEAR_QUOTE,
|
|
102
|
+
exercise_style=ExerciseStyle.EUROPEAN,
|
|
103
|
+
settlement_style=settlement_style,
|
|
104
|
+
premium_currency="USDC",
|
|
105
|
+
settlement_currency="USDC",
|
|
106
|
+
quote_currency="USDC",
|
|
107
|
+
supported_underlyings=(base,),
|
|
108
|
+
fee_schedule_id="deribit_linear_usdc_options",
|
|
109
|
+
margin_schedule_id="deribit_pm_external_or_scenario_approximation",
|
|
110
|
+
exact_venue_margin=False,
|
|
111
|
+
notes="Linear USDC option convention supports economic cash or future-then-cash settlement representation.",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def binance_european_options_convention(
|
|
116
|
+
*,
|
|
117
|
+
underlying: str = "BTC",
|
|
118
|
+
version: str = "binance_european_options_v1",
|
|
119
|
+
) -> OptionVenueConvention:
|
|
120
|
+
base = str(underlying).upper()
|
|
121
|
+
return OptionVenueConvention(
|
|
122
|
+
venue="binance",
|
|
123
|
+
convention_id=version,
|
|
124
|
+
premium_convention=PremiumConvention.LINEAR_QUOTE,
|
|
125
|
+
exercise_style=ExerciseStyle.EUROPEAN,
|
|
126
|
+
settlement_style=SettlementStyle.CASH,
|
|
127
|
+
premium_currency="USDT",
|
|
128
|
+
settlement_currency="USDT",
|
|
129
|
+
quote_currency="USDT",
|
|
130
|
+
supported_underlyings=(base,),
|
|
131
|
+
fee_schedule_id="binance_options_versioned_external",
|
|
132
|
+
margin_schedule_id="binance_options_external_or_scenario_approximation",
|
|
133
|
+
exact_venue_margin=False,
|
|
134
|
+
notes="Binance config is schema/convention metadata only until official fee/margin parity tests are added.",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _coerce(enum_cls, value, field_name: str):
|
|
139
|
+
if isinstance(value, enum_cls):
|
|
140
|
+
return value
|
|
141
|
+
try:
|
|
142
|
+
return enum_cls(str(value))
|
|
143
|
+
except ValueError as exc:
|
|
144
|
+
raise ValueError(f"{field_name} must be one of {[item.value for item in enum_cls]}") from exc
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _validate_convention(convention: OptionVenueConvention) -> None:
|
|
148
|
+
if convention.premium_convention is PremiumConvention.INVERSE_BASE:
|
|
149
|
+
if convention.premium_currency != convention.settlement_currency:
|
|
150
|
+
raise ValueError("inverse convention requires premium_currency == settlement_currency")
|
|
151
|
+
if convention.quote_currency == convention.premium_currency:
|
|
152
|
+
raise ValueError("inverse convention requires quote_currency distinct from base premium currency")
|
|
153
|
+
elif convention.premium_convention is PremiumConvention.LINEAR_QUOTE:
|
|
154
|
+
if convention.premium_currency != convention.quote_currency:
|
|
155
|
+
raise ValueError("linear convention requires premium_currency == quote_currency")
|
|
156
|
+
elif convention.premium_convention is PremiumConvention.QUANTO:
|
|
157
|
+
if convention.premium_currency == convention.settlement_currency == convention.quote_currency:
|
|
158
|
+
raise ValueError("quanto convention requires at least one distinct currency")
|
quantbt/options/data.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical option chain data validation.
|
|
3
|
+
|
|
4
|
+
The canonical chain is long-form. Phase 1 validates structure only; Phase 3
|
|
5
|
+
will compile this data into a ragged/CSR option tape.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Iterable, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CANONICAL_OPTION_CHAIN_COLUMNS = (
|
|
17
|
+
"timestamp_ns",
|
|
18
|
+
"instrument_id",
|
|
19
|
+
"venue",
|
|
20
|
+
"underlying_id",
|
|
21
|
+
"expiry_ns",
|
|
22
|
+
"strike",
|
|
23
|
+
"option_kind",
|
|
24
|
+
"bid_price",
|
|
25
|
+
"bid_size",
|
|
26
|
+
"ask_price",
|
|
27
|
+
"ask_size",
|
|
28
|
+
"mark_price",
|
|
29
|
+
"last_price",
|
|
30
|
+
"index_price",
|
|
31
|
+
"forward_price",
|
|
32
|
+
"mark_iv",
|
|
33
|
+
"bid_iv",
|
|
34
|
+
"ask_iv",
|
|
35
|
+
"delta",
|
|
36
|
+
"gamma",
|
|
37
|
+
"vega",
|
|
38
|
+
"theta",
|
|
39
|
+
"open_interest",
|
|
40
|
+
"volume",
|
|
41
|
+
"quote_currency",
|
|
42
|
+
"settlement_currency",
|
|
43
|
+
"sequence_id",
|
|
44
|
+
"source_latency_ns",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
REQUIRED_OPTION_CHAIN_COLUMNS = (
|
|
48
|
+
"timestamp_ns",
|
|
49
|
+
"instrument_id",
|
|
50
|
+
"venue",
|
|
51
|
+
"underlying_id",
|
|
52
|
+
"expiry_ns",
|
|
53
|
+
"strike",
|
|
54
|
+
"option_kind",
|
|
55
|
+
"bid_price",
|
|
56
|
+
"bid_size",
|
|
57
|
+
"ask_price",
|
|
58
|
+
"ask_size",
|
|
59
|
+
"mark_price",
|
|
60
|
+
"index_price",
|
|
61
|
+
"forward_price",
|
|
62
|
+
"quote_currency",
|
|
63
|
+
"settlement_currency",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_option_chain_frame(
|
|
68
|
+
frame: pd.DataFrame,
|
|
69
|
+
*,
|
|
70
|
+
required_columns: Sequence[str] = REQUIRED_OPTION_CHAIN_COLUMNS,
|
|
71
|
+
max_spread_bps: Optional[float] = None,
|
|
72
|
+
reject_crossed: bool = True,
|
|
73
|
+
) -> pd.DataFrame:
|
|
74
|
+
"""
|
|
75
|
+
Validate and return a sorted canonical long-form option chain copy.
|
|
76
|
+
|
|
77
|
+
This function intentionally avoids filling missing market values. Missing
|
|
78
|
+
fields must remain visible to later tape compilation and no-lookahead tests.
|
|
79
|
+
"""
|
|
80
|
+
if not isinstance(frame, pd.DataFrame):
|
|
81
|
+
raise TypeError("option chain must be a pandas DataFrame")
|
|
82
|
+
missing = [column for column in required_columns if column not in frame.columns]
|
|
83
|
+
if missing:
|
|
84
|
+
raise ValueError(f"option chain missing required columns: {missing}")
|
|
85
|
+
out = frame.copy()
|
|
86
|
+
_coerce_int64(out, ("timestamp_ns", "expiry_ns", "sequence_id", "source_latency_ns"), required=set(required_columns))
|
|
87
|
+
_coerce_float(
|
|
88
|
+
out,
|
|
89
|
+
(
|
|
90
|
+
"strike",
|
|
91
|
+
"bid_price",
|
|
92
|
+
"bid_size",
|
|
93
|
+
"ask_price",
|
|
94
|
+
"ask_size",
|
|
95
|
+
"mark_price",
|
|
96
|
+
"last_price",
|
|
97
|
+
"index_price",
|
|
98
|
+
"forward_price",
|
|
99
|
+
"mark_iv",
|
|
100
|
+
"bid_iv",
|
|
101
|
+
"ask_iv",
|
|
102
|
+
"delta",
|
|
103
|
+
"gamma",
|
|
104
|
+
"vega",
|
|
105
|
+
"theta",
|
|
106
|
+
"open_interest",
|
|
107
|
+
"volume",
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
_normalize_strings(out, ("instrument_id", "underlying_id", "quote_currency", "settlement_currency"))
|
|
111
|
+
out["venue"] = out["venue"].astype(str).str.strip().str.lower()
|
|
112
|
+
out["option_kind"] = out["option_kind"].astype(str).str.strip().str.lower()
|
|
113
|
+
_validate_positive(out, ("timestamp_ns", "expiry_ns", "strike", "index_price", "forward_price"))
|
|
114
|
+
_validate_non_negative(out, ("bid_price", "bid_size", "ask_price", "ask_size", "mark_price"))
|
|
115
|
+
if reject_crossed and bool((out["bid_price"] > out["ask_price"]).any()):
|
|
116
|
+
raise ValueError("option chain contains crossed quotes: bid_price > ask_price")
|
|
117
|
+
if bool((out["bid_price"] <= 0.0).any()):
|
|
118
|
+
raise ValueError("option chain requires bid_price > 0 in Phase 1 canonical validation")
|
|
119
|
+
if bool((out["ask_price"] <= 0.0).any()):
|
|
120
|
+
raise ValueError("option chain requires ask_price > 0 in Phase 1 canonical validation")
|
|
121
|
+
if max_spread_bps is not None:
|
|
122
|
+
if max_spread_bps < 0.0:
|
|
123
|
+
raise ValueError("max_spread_bps must be >= 0")
|
|
124
|
+
mid = 0.5 * (out["bid_price"].to_numpy() + out["ask_price"].to_numpy())
|
|
125
|
+
spread_bps = np.divide(
|
|
126
|
+
out["ask_price"].to_numpy() - out["bid_price"].to_numpy(),
|
|
127
|
+
mid,
|
|
128
|
+
out=np.full(len(out), np.inf, dtype=np.float64),
|
|
129
|
+
where=mid > 0.0,
|
|
130
|
+
) * 10_000.0
|
|
131
|
+
if bool((spread_bps > float(max_spread_bps)).any()):
|
|
132
|
+
raise ValueError("option chain contains quotes wider than max_spread_bps")
|
|
133
|
+
if bool((out["expiry_ns"] <= out["timestamp_ns"]).any()):
|
|
134
|
+
raise ValueError("option chain contains expired quotes")
|
|
135
|
+
if not set(out["option_kind"].unique()).issubset({"call", "put"}):
|
|
136
|
+
raise ValueError("option_kind must be call or put")
|
|
137
|
+
out = out.sort_values(["timestamp_ns", "sequence_id", "instrument_id"] if "sequence_id" in out else ["timestamp_ns", "instrument_id"])
|
|
138
|
+
out = out.reset_index(drop=True)
|
|
139
|
+
if bool(out.duplicated(subset=[column for column in ("timestamp_ns", "instrument_id", "sequence_id") if column in out]).any()):
|
|
140
|
+
raise ValueError("option chain contains duplicate timestamp/instrument/sequence rows")
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _coerce_int64(frame: pd.DataFrame, columns: Iterable[str], *, required: set[str]) -> None:
|
|
145
|
+
for column in columns:
|
|
146
|
+
if column not in frame:
|
|
147
|
+
if column in required:
|
|
148
|
+
raise ValueError(f"missing required integer column {column!r}")
|
|
149
|
+
continue
|
|
150
|
+
frame[column] = pd.to_numeric(frame[column], errors="raise").astype("int64")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _coerce_float(frame: pd.DataFrame, columns: Iterable[str]) -> None:
|
|
154
|
+
for column in columns:
|
|
155
|
+
if column in frame:
|
|
156
|
+
frame[column] = pd.to_numeric(frame[column], errors="raise").astype("float64")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normalize_strings(frame: pd.DataFrame, columns: Iterable[str]) -> None:
|
|
160
|
+
for column in columns:
|
|
161
|
+
if column in frame:
|
|
162
|
+
frame[column] = frame[column].astype(str).str.strip()
|
|
163
|
+
for column in ("quote_currency", "settlement_currency"):
|
|
164
|
+
if column in frame:
|
|
165
|
+
frame[column] = frame[column].str.upper()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _validate_positive(frame: pd.DataFrame, columns: Iterable[str]) -> None:
|
|
169
|
+
for column in columns:
|
|
170
|
+
if column in frame and bool((frame[column] <= 0).any()):
|
|
171
|
+
raise ValueError(f"{column} must be > 0")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _validate_non_negative(frame: pd.DataFrame, columns: Iterable[str]) -> None:
|
|
175
|
+
for column in columns:
|
|
176
|
+
if column in frame and bool((frame[column] < 0).any()):
|
|
177
|
+
raise ValueError(f"{column} must be >= 0")
|