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,121 @@
|
|
|
1
|
+
"""Canonical native-event capability contract.
|
|
2
|
+
|
|
3
|
+
The Rust extension exposes a low-level capability map whose names are tied to
|
|
4
|
+
its release history (for example ``rust_batched_tape``). Public selectors,
|
|
5
|
+
tests, and documentation need a stable vocabulary instead. This module is
|
|
6
|
+
the single Python-side source of truth for the currently certified
|
|
7
|
+
single-symbol R2 surface. Full-contract 0.4 flags are additive and only
|
|
8
|
+
normalize to the wider vocabulary when the extension advertises the complete
|
|
9
|
+
capability gate.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from hashlib import sha256
|
|
15
|
+
import json
|
|
16
|
+
from types import MappingProxyType
|
|
17
|
+
from typing import Mapping
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
NATIVE_EVENT_CAPABILITY_MATRIX_VERSION = "full-contract-v2-0.4"
|
|
21
|
+
|
|
22
|
+
_CAPABILITIES = {
|
|
23
|
+
"single_symbol": True,
|
|
24
|
+
"market": True,
|
|
25
|
+
"limit": True,
|
|
26
|
+
"stop_market": True,
|
|
27
|
+
"stop_limit": True,
|
|
28
|
+
"place": True,
|
|
29
|
+
"cancel": True,
|
|
30
|
+
"amend": True,
|
|
31
|
+
"replace": True,
|
|
32
|
+
"reduce_only": True,
|
|
33
|
+
"quantity_constraints": True,
|
|
34
|
+
"gtc": True,
|
|
35
|
+
"gtd": True,
|
|
36
|
+
"ioc": True,
|
|
37
|
+
"fok": True,
|
|
38
|
+
"parent_child": True,
|
|
39
|
+
"oco": True,
|
|
40
|
+
"funding": True,
|
|
41
|
+
"liquidation": True,
|
|
42
|
+
"multi_symbol": True,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
NATIVE_EVENT_CAPABILITY_MATRIX: Mapping[str, bool] = MappingProxyType(_CAPABILITIES)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def native_event_capability_matrix() -> dict[str, bool]:
|
|
49
|
+
"""Return a mutable copy of the canonical capability matrix."""
|
|
50
|
+
|
|
51
|
+
return dict(NATIVE_EVENT_CAPABILITY_MATRIX)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def capability_matrix_fingerprint() -> str:
|
|
55
|
+
"""Return a reproducible SHA-256 fingerprint for the capability contract."""
|
|
56
|
+
|
|
57
|
+
payload = {
|
|
58
|
+
"version": NATIVE_EVENT_CAPABILITY_MATRIX_VERSION,
|
|
59
|
+
"capabilities": dict(sorted(NATIVE_EVENT_CAPABILITY_MATRIX.items())),
|
|
60
|
+
}
|
|
61
|
+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
62
|
+
return sha256(encoded).hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_native_event_capabilities(raw: Mapping[str, object] | None) -> dict[str, bool]:
|
|
66
|
+
"""Map extension-specific flags into the stable public vocabulary.
|
|
67
|
+
|
|
68
|
+
Unknown raw flags are intentionally ignored. A raw flag cannot silently
|
|
69
|
+
enable a capability that is outside the certified matrix; a later release
|
|
70
|
+
must update this module and its tests first.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
source = {str(key): bool(value) for key, value in (raw or {}).items()}
|
|
74
|
+
lifecycle = source.get("reactive_session", False) or source.get("r1_single_symbol", False)
|
|
75
|
+
place_cancel = source.get("r1_place_cancel_market_limit_gtc", False)
|
|
76
|
+
r2 = source.get("r2_stop_amend_replace_reduce_only_constraints", False)
|
|
77
|
+
batched = source.get("rust_batched_tape", False) or source.get("rust_batched_tape_audit", False)
|
|
78
|
+
full = source.get("native_event_v2_full_contract", False)
|
|
79
|
+
|
|
80
|
+
normalized = native_event_capability_matrix()
|
|
81
|
+
normalized["single_symbol"] = bool(full or lifecycle or batched)
|
|
82
|
+
normalized["market"] = bool(full or place_cancel or batched)
|
|
83
|
+
normalized["limit"] = bool(full or place_cancel or batched)
|
|
84
|
+
normalized["stop_market"] = bool(full or r2)
|
|
85
|
+
normalized["stop_limit"] = bool(full or r2)
|
|
86
|
+
normalized["place"] = bool(full or place_cancel or batched)
|
|
87
|
+
normalized["cancel"] = bool(full or place_cancel or batched)
|
|
88
|
+
normalized["amend"] = bool(full or r2)
|
|
89
|
+
normalized["replace"] = bool(full or r2)
|
|
90
|
+
normalized["reduce_only"] = bool(full or r2)
|
|
91
|
+
normalized["quantity_constraints"] = bool(full or r2)
|
|
92
|
+
normalized["gtc"] = bool(full or place_cancel or batched)
|
|
93
|
+
if full:
|
|
94
|
+
normalized.update({
|
|
95
|
+
"gtd": True, "ioc": True, "fok": True, "parent_child": True,
|
|
96
|
+
"oco": True, "funding": True, "liquidation": True, "multi_symbol": True,
|
|
97
|
+
})
|
|
98
|
+
else:
|
|
99
|
+
normalized.update({
|
|
100
|
+
"gtd": False, "ioc": False, "fok": False, "parent_child": False,
|
|
101
|
+
"oco": False, "funding": False, "liquidation": False, "multi_symbol": False,
|
|
102
|
+
})
|
|
103
|
+
return normalized
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_native_event_capability_matrix(matrix: Mapping[str, object]) -> None:
|
|
107
|
+
"""Raise if a consumer attempts to advertise an unknown capability."""
|
|
108
|
+
|
|
109
|
+
unknown = sorted(set(matrix) - set(NATIVE_EVENT_CAPABILITY_MATRIX))
|
|
110
|
+
if unknown:
|
|
111
|
+
raise ValueError(f"unknown native-event capability fields: {unknown}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = [
|
|
115
|
+
"NATIVE_EVENT_CAPABILITY_MATRIX_VERSION",
|
|
116
|
+
"NATIVE_EVENT_CAPABILITY_MATRIX",
|
|
117
|
+
"capability_matrix_fingerprint",
|
|
118
|
+
"native_event_capability_matrix",
|
|
119
|
+
"normalize_native_event_capabilities",
|
|
120
|
+
"validate_native_event_capability_matrix",
|
|
121
|
+
]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Strict parity certificates for native-event execution artifacts.
|
|
2
|
+
|
|
3
|
+
This module deliberately lives above the execution kernels. It compares
|
|
4
|
+
observable lifecycle/accounting artifacts and therefore can certify Python,
|
|
5
|
+
Rust, and replay results without making any backend responsible for another
|
|
6
|
+
backend's object model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from hashlib import sha256
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, Mapping
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from .native_event_capabilities import NATIVE_EVENT_CAPABILITY_MATRIX
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFAULT_NUMERIC_ATOL = 1e-12
|
|
23
|
+
_NUMERIC_FIELDS = (
|
|
24
|
+
"equity",
|
|
25
|
+
"positions",
|
|
26
|
+
"fees",
|
|
27
|
+
"funding",
|
|
28
|
+
"turnover",
|
|
29
|
+
"initial_margin",
|
|
30
|
+
"maintenance_margin",
|
|
31
|
+
)
|
|
32
|
+
_DISCRETE_FIELDS = (
|
|
33
|
+
"liquidated",
|
|
34
|
+
"liquidation_bar",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NativeEventParityError(AssertionError):
|
|
39
|
+
"""Raised when two native-event artifacts are not lifecycle-equivalent."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class NativeEventParityCertificate:
|
|
44
|
+
"""Serializable summary returned by :func:`assert_native_event_full_parity`."""
|
|
45
|
+
|
|
46
|
+
passed: bool
|
|
47
|
+
numeric_atol: float
|
|
48
|
+
compared_fields: tuple[str, ...]
|
|
49
|
+
missing_fields: tuple[str, ...]
|
|
50
|
+
candidate_fingerprint: str
|
|
51
|
+
oracle_fingerprint: str
|
|
52
|
+
command_fingerprint: str | None = None
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict[str, Any]:
|
|
55
|
+
return {
|
|
56
|
+
"passed": self.passed,
|
|
57
|
+
"numeric_atol": self.numeric_atol,
|
|
58
|
+
"compared_fields": list(self.compared_fields),
|
|
59
|
+
"missing_fields": list(self.missing_fields),
|
|
60
|
+
"candidate_fingerprint": self.candidate_fingerprint,
|
|
61
|
+
"oracle_fingerprint": self.oracle_fingerprint,
|
|
62
|
+
"command_fingerprint": self.command_fingerprint,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _metadata(result: object) -> Mapping[str, object]:
|
|
67
|
+
value = getattr(result, "metadata", None)
|
|
68
|
+
return value if isinstance(value, Mapping) else {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _array(value: object, *, name: str) -> np.ndarray | None:
|
|
72
|
+
if value is None:
|
|
73
|
+
return None
|
|
74
|
+
if isinstance(value, pd.Series):
|
|
75
|
+
return value.to_numpy(copy=True)
|
|
76
|
+
if isinstance(value, pd.DataFrame):
|
|
77
|
+
if name == "positions":
|
|
78
|
+
columns = [column for column in value.columns if str(column).startswith("Position_")]
|
|
79
|
+
if columns:
|
|
80
|
+
return value[columns].to_numpy(copy=True)
|
|
81
|
+
if name in value:
|
|
82
|
+
return value[name].to_numpy(copy=True)
|
|
83
|
+
return value.to_numpy(copy=True)
|
|
84
|
+
return np.asarray(value).copy()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _result_field(result: object, name: str) -> np.ndarray | object | None:
|
|
88
|
+
value = getattr(result, name, None)
|
|
89
|
+
if name == "turnover" and value is None:
|
|
90
|
+
diagnostics = getattr(result, "diagnostics", None)
|
|
91
|
+
value = diagnostics.get("turnover") if isinstance(diagnostics, pd.DataFrame) else None
|
|
92
|
+
if name in {"initial_margin", "maintenance_margin"} and value is None:
|
|
93
|
+
margin = getattr(result, "margin", None)
|
|
94
|
+
if isinstance(margin, pd.DataFrame):
|
|
95
|
+
value = margin.get(name)
|
|
96
|
+
if value is None:
|
|
97
|
+
value = _metadata(result).get(name)
|
|
98
|
+
return _array(value, name=name) if name not in _DISCRETE_FIELDS else value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _stable_bytes(value: object) -> bytes:
|
|
102
|
+
if isinstance(value, Mapping):
|
|
103
|
+
value = {str(key): value[key] for key in sorted(value, key=str)}
|
|
104
|
+
return json.dumps(value, sort_keys=True, default=str, separators=(",", ":")).encode("utf-8")
|
|
105
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
106
|
+
return json.dumps(value, sort_keys=True, default=str, separators=(",", ":")).encode("utf-8")
|
|
107
|
+
array = np.asarray(value)
|
|
108
|
+
if array.dtype.kind in "OUS":
|
|
109
|
+
payload = [str(item) for item in array.reshape(-1)]
|
|
110
|
+
return json.dumps({"shape": array.shape, "values": payload}, separators=(",", ":")).encode("utf-8")
|
|
111
|
+
contiguous = np.ascontiguousarray(array)
|
|
112
|
+
return b"|".join((str(contiguous.dtype).encode(), repr(contiguous.shape).encode(), contiguous.tobytes()))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fingerprint(fields: Mapping[str, object]) -> str:
|
|
116
|
+
digest = sha256()
|
|
117
|
+
for name in sorted(fields):
|
|
118
|
+
digest.update(name.encode("utf-8"))
|
|
119
|
+
digest.update(b"=")
|
|
120
|
+
digest.update(_stable_bytes(fields[name]))
|
|
121
|
+
digest.update(b"\n")
|
|
122
|
+
return digest.hexdigest()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _record_value(record: object, name: str, default: object = None) -> object:
|
|
126
|
+
if isinstance(record, Mapping):
|
|
127
|
+
return record.get(name, default)
|
|
128
|
+
return getattr(record, name, default)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fill_records(result: object) -> list[tuple[object, ...]] | None:
|
|
132
|
+
arrays = {name: getattr(result, f"fill_{name}", None) for name in (
|
|
133
|
+
"bar", "order_id", "side", "qty", "price", "fee"
|
|
134
|
+
)}
|
|
135
|
+
if all(value is not None for value in arrays.values()):
|
|
136
|
+
length = len(np.asarray(arrays["bar"]))
|
|
137
|
+
return [
|
|
138
|
+
tuple(np.asarray(arrays[name])[idx].item() for name in arrays)
|
|
139
|
+
for idx in range(length)
|
|
140
|
+
]
|
|
141
|
+
fills = getattr(result, "fills", None)
|
|
142
|
+
if fills is None:
|
|
143
|
+
fills = _metadata(result).get("fills")
|
|
144
|
+
if fills is None:
|
|
145
|
+
return None
|
|
146
|
+
records = []
|
|
147
|
+
for fill in fills:
|
|
148
|
+
side = _record_value(fill, "side")
|
|
149
|
+
side = getattr(side, "value", side)
|
|
150
|
+
records.append(
|
|
151
|
+
(
|
|
152
|
+
str(_record_value(fill, "timestamp")),
|
|
153
|
+
str(_record_value(fill, "symbol")),
|
|
154
|
+
side,
|
|
155
|
+
float(_record_value(fill, "qty")),
|
|
156
|
+
float(_record_value(fill, "price")),
|
|
157
|
+
float(_record_value(fill, "fee", 0.0)),
|
|
158
|
+
str(_record_value(fill, "order_id")),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return records
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _event_records(result: object) -> list[tuple[object, ...]] | None:
|
|
165
|
+
arrays = {
|
|
166
|
+
name: getattr(result, f"event_{name}", None)
|
|
167
|
+
for name in ("bar", "kind", "status", "order_id", "target_id")
|
|
168
|
+
}
|
|
169
|
+
if all(value is not None for value in arrays.values()):
|
|
170
|
+
length = len(np.asarray(arrays["bar"]))
|
|
171
|
+
return [
|
|
172
|
+
tuple(np.asarray(arrays[name])[idx].item() for name in arrays)
|
|
173
|
+
for idx in range(length)
|
|
174
|
+
]
|
|
175
|
+
ledger = _metadata(result).get("compact_order_event_ledger")
|
|
176
|
+
if ledger is None:
|
|
177
|
+
ledger = _metadata(result).get("event_ledger")
|
|
178
|
+
if ledger is None:
|
|
179
|
+
return None
|
|
180
|
+
if isinstance(ledger, Mapping):
|
|
181
|
+
names = ("bar", "kind", "status", "order_id", "target_id")
|
|
182
|
+
if all(name in ledger for name in names):
|
|
183
|
+
arrays = {name: np.asarray(ledger[name]) for name in names}
|
|
184
|
+
return [tuple(arrays[name][idx].item() for name in names) for idx in range(len(arrays["bar"]))]
|
|
185
|
+
names = ("bar", "event_type", "status", "command_index", "related_command_index")
|
|
186
|
+
if all(hasattr(ledger, name) for name in names):
|
|
187
|
+
arrays = {name: np.asarray(getattr(ledger, name)) for name in names}
|
|
188
|
+
return [
|
|
189
|
+
(
|
|
190
|
+
arrays["bar"][idx].item(),
|
|
191
|
+
arrays["event_type"][idx].item(),
|
|
192
|
+
arrays["status"][idx].item(),
|
|
193
|
+
arrays["command_index"][idx].item(),
|
|
194
|
+
arrays["related_command_index"][idx].item(),
|
|
195
|
+
)
|
|
196
|
+
for idx in range(len(arrays["bar"]))
|
|
197
|
+
]
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _command_fingerprint(command_tape: object) -> str:
|
|
202
|
+
if isinstance(command_tape, Mapping):
|
|
203
|
+
fields = command_tape
|
|
204
|
+
else:
|
|
205
|
+
names = (
|
|
206
|
+
"effective_bar", "command_ptr", "command_action", "command_order_id",
|
|
207
|
+
"command_status", "command_symbol", "command_sequence",
|
|
208
|
+
)
|
|
209
|
+
fields = {name: getattr(command_tape, name) for name in names if hasattr(command_tape, name)}
|
|
210
|
+
if not fields:
|
|
211
|
+
raise ValueError("command_tape must expose at least one deterministic command field")
|
|
212
|
+
return _fingerprint({str(name): value for name, value in fields.items()})
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _snapshot(result: object) -> dict[str, object]:
|
|
216
|
+
fields: dict[str, object] = {}
|
|
217
|
+
for name in _NUMERIC_FIELDS:
|
|
218
|
+
value = _result_field(result, name)
|
|
219
|
+
if value is not None:
|
|
220
|
+
fields[name] = value
|
|
221
|
+
fills = _fill_records(result)
|
|
222
|
+
if fills is not None:
|
|
223
|
+
fields["fills"] = fills
|
|
224
|
+
events = _event_records(result)
|
|
225
|
+
if events is not None:
|
|
226
|
+
fields["events"] = events
|
|
227
|
+
for name in _DISCRETE_FIELDS:
|
|
228
|
+
value = getattr(result, name, None)
|
|
229
|
+
if value is not None:
|
|
230
|
+
fields[name] = value
|
|
231
|
+
return fields
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def assert_native_event_full_parity(
|
|
235
|
+
candidate: object,
|
|
236
|
+
oracle: object,
|
|
237
|
+
*,
|
|
238
|
+
numeric_atol: float = DEFAULT_NUMERIC_ATOL,
|
|
239
|
+
capabilities: Mapping[str, object] | None = None,
|
|
240
|
+
command_tape: object | tuple[object, object] | None = None,
|
|
241
|
+
require_full: bool = True,
|
|
242
|
+
) -> dict[str, object]:
|
|
243
|
+
"""Compare complete observable lifecycle and accounting artifacts.
|
|
244
|
+
|
|
245
|
+
Discrete lifecycle artifacts (fills, event order, statuses, and boolean
|
|
246
|
+
state) must match exactly. Numeric paths use ``rtol=0`` and the supplied
|
|
247
|
+
absolute tolerance. ``require_full=True`` requires fills and event ledgers
|
|
248
|
+
on both sides; use ``False`` only for an explicitly scalar/minimal run.
|
|
249
|
+
``command_tape`` may be one shared tape or ``(candidate, oracle)``.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
atol = float(numeric_atol)
|
|
253
|
+
if atol < 0.0 or not np.isfinite(atol):
|
|
254
|
+
raise ValueError("numeric_atol must be finite and >= 0")
|
|
255
|
+
left = _snapshot(candidate)
|
|
256
|
+
right = _snapshot(oracle)
|
|
257
|
+
capabilities = dict(NATIVE_EVENT_CAPABILITY_MATRIX if capabilities is None else capabilities)
|
|
258
|
+
compared: list[str] = []
|
|
259
|
+
missing: list[str] = []
|
|
260
|
+
|
|
261
|
+
for name in _NUMERIC_FIELDS:
|
|
262
|
+
left_value = left.get(name)
|
|
263
|
+
right_value = right.get(name)
|
|
264
|
+
required = name in {"equity", "positions", "fees", "turnover", "initial_margin", "maintenance_margin"}
|
|
265
|
+
if name == "funding":
|
|
266
|
+
required = bool(capabilities.get("funding", False))
|
|
267
|
+
if left_value is None or right_value is None:
|
|
268
|
+
if required:
|
|
269
|
+
missing.append(name)
|
|
270
|
+
continue
|
|
271
|
+
lhs = np.asarray(left_value)
|
|
272
|
+
rhs = np.asarray(right_value)
|
|
273
|
+
if lhs.shape != rhs.shape:
|
|
274
|
+
raise NativeEventParityError(f"{name} shape mismatch: {lhs.shape} != {rhs.shape}")
|
|
275
|
+
if not np.allclose(lhs, rhs, rtol=0.0, atol=atol, equal_nan=True):
|
|
276
|
+
difference = float(np.nanmax(np.abs(lhs.astype(float) - rhs.astype(float))))
|
|
277
|
+
raise NativeEventParityError(f"{name} mismatch: max_abs_diff={difference:.17g}, atol={atol:.17g}")
|
|
278
|
+
compared.append(name)
|
|
279
|
+
|
|
280
|
+
for name in _DISCRETE_FIELDS:
|
|
281
|
+
if name not in left or name not in right:
|
|
282
|
+
if name in {"liquidated", "liquidation_bar"} and not capabilities.get("liquidation", False):
|
|
283
|
+
continue
|
|
284
|
+
missing.append(name)
|
|
285
|
+
continue
|
|
286
|
+
if left[name] != right[name]:
|
|
287
|
+
raise NativeEventParityError(f"{name} mismatch: {left[name]!r} != {right[name]!r}")
|
|
288
|
+
compared.append(name)
|
|
289
|
+
|
|
290
|
+
for name in ("fills", "events"):
|
|
291
|
+
lhs = left.get(name)
|
|
292
|
+
rhs = right.get(name)
|
|
293
|
+
if lhs is None or rhs is None:
|
|
294
|
+
if require_full:
|
|
295
|
+
missing.append(name)
|
|
296
|
+
continue
|
|
297
|
+
if lhs != rhs:
|
|
298
|
+
raise NativeEventParityError(f"{name} lifecycle sequence mismatch")
|
|
299
|
+
compared.append(name)
|
|
300
|
+
|
|
301
|
+
command_fingerprint = None
|
|
302
|
+
if command_tape is not None:
|
|
303
|
+
if isinstance(command_tape, tuple):
|
|
304
|
+
if len(command_tape) != 2:
|
|
305
|
+
raise ValueError("command_tape tuple must contain (candidate_tape, oracle_tape)")
|
|
306
|
+
left_command = _command_fingerprint(command_tape[0])
|
|
307
|
+
right_command = _command_fingerprint(command_tape[1])
|
|
308
|
+
if left_command != right_command:
|
|
309
|
+
raise NativeEventParityError("command sequence/effective-bar fingerprint mismatch")
|
|
310
|
+
command_fingerprint = left_command
|
|
311
|
+
else:
|
|
312
|
+
command_fingerprint = _command_fingerprint(command_tape)
|
|
313
|
+
compared.append("command_tape")
|
|
314
|
+
|
|
315
|
+
if missing:
|
|
316
|
+
raise NativeEventParityError(f"parity artifacts missing: {sorted(set(missing))}")
|
|
317
|
+
candidate_fingerprint = _fingerprint(left)
|
|
318
|
+
oracle_fingerprint = _fingerprint(right)
|
|
319
|
+
certificate = NativeEventParityCertificate(
|
|
320
|
+
passed=True,
|
|
321
|
+
numeric_atol=atol,
|
|
322
|
+
compared_fields=tuple(compared),
|
|
323
|
+
missing_fields=tuple(missing),
|
|
324
|
+
candidate_fingerprint=candidate_fingerprint,
|
|
325
|
+
oracle_fingerprint=oracle_fingerprint,
|
|
326
|
+
command_fingerprint=command_fingerprint,
|
|
327
|
+
)
|
|
328
|
+
return certificate.to_dict()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = [
|
|
332
|
+
"DEFAULT_NUMERIC_ATOL",
|
|
333
|
+
"NativeEventParityCertificate",
|
|
334
|
+
"NativeEventParityError",
|
|
335
|
+
"assert_native_event_full_parity",
|
|
336
|
+
]
|