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.
Files changed (133) hide show
  1. quantbt/__init__.py +862 -0
  2. quantbt/adapters/__init__.py +5 -0
  3. quantbt/adapters/nautilus/__init__.py +42 -0
  4. quantbt/adapters/nautilus/_dependency.py +53 -0
  5. quantbt/adapters/nautilus/backend.py +639 -0
  6. quantbt/adapters/nautilus/instruments.py +275 -0
  7. quantbt/adapters/nautilus/options.py +465 -0
  8. quantbt/adapters/nautilus/reports.py +231 -0
  9. quantbt/backends/__init__.py +29 -0
  10. quantbt/backends/_native_event_rust.py +2168 -0
  11. quantbt/backends/native_event.py +5048 -0
  12. quantbt/backends/native_option.py +796 -0
  13. quantbt/backends/native_portfolio.py +928 -0
  14. quantbt/backends/native_vectorized.py +1299 -0
  15. quantbt/backtester.py +485 -0
  16. quantbt/benchmarks/__init__.py +1 -0
  17. quantbt/benchmarks/compare_phase9_parity.py +302 -0
  18. quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
  19. quantbt/benchmarks/profile_phase7.py +415 -0
  20. quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
  21. quantbt/benchmarks/run_optimization_overhead.py +195 -0
  22. quantbt/benchmarks/run_options_engine.py +272 -0
  23. quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
  24. quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
  25. quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
  26. quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
  27. quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
  28. quantbt/benchmarks/run_phase14_service_loop.py +694 -0
  29. quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
  30. quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
  31. quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
  32. quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
  33. quantbt/benchmarks/run_phase31_intrabar.py +441 -0
  34. quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
  35. quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
  36. quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
  37. quantbt/benchmarks/run_phase7.py +654 -0
  38. quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
  39. quantbt/core/__init__.py +323 -0
  40. quantbt/core/arbitrage.py +767 -0
  41. quantbt/core/basket.py +234 -0
  42. quantbt/core/certification.py +274 -0
  43. quantbt/core/constraints.py +155 -0
  44. quantbt/core/engine.py +1144 -0
  45. quantbt/core/event.py +869 -0
  46. quantbt/core/execution_contract.py +197 -0
  47. quantbt/core/execution_depth.py +613 -0
  48. quantbt/core/intrabar_kernel.py +1906 -0
  49. quantbt/core/intrabar_reference.py +907 -0
  50. quantbt/core/intrabar_session.py +156 -0
  51. quantbt/core/market_tape.py +480 -0
  52. quantbt/core/native_event_capabilities.py +121 -0
  53. quantbt/core/native_event_parity.py +336 -0
  54. quantbt/core/order_compiler.py +422 -0
  55. quantbt/core/orders.py +319 -0
  56. quantbt/core/portfolio.py +392 -0
  57. quantbt/core/preprocessor.py +256 -0
  58. quantbt/core/reactive.py +173 -0
  59. quantbt/core/results.py +341 -0
  60. quantbt/core/schema.py +211 -0
  61. quantbt/core/scopes.py +96 -0
  62. quantbt/core/structured_orders.py +378 -0
  63. quantbt/core/types.py +87 -0
  64. quantbt/core/vectorized.py +203 -0
  65. quantbt/endpoint.py +4465 -0
  66. quantbt/engines.py +1014 -0
  67. quantbt/metrics/__init__.py +45 -0
  68. quantbt/metrics/options_analytics.py +46 -0
  69. quantbt/metrics/performance.py +541 -0
  70. quantbt/optimization/__init__.py +94 -0
  71. quantbt/optimization/callbacks.py +116 -0
  72. quantbt/optimization/candidate_selection.py +397 -0
  73. quantbt/optimization/config.py +84 -0
  74. quantbt/optimization/constraints.py +22 -0
  75. quantbt/optimization/evaluator.py +21 -0
  76. quantbt/optimization/evaluators/__init__.py +32 -0
  77. quantbt/optimization/evaluators/arbitrage.py +22 -0
  78. quantbt/optimization/evaluators/generic.py +34 -0
  79. quantbt/optimization/evaluators/grid_dca.py +22 -0
  80. quantbt/optimization/evaluators/intrabar.py +54 -0
  81. quantbt/optimization/evaluators/native_event.py +51 -0
  82. quantbt/optimization/evaluators/options.py +22 -0
  83. quantbt/optimization/evaluators/portfolio.py +42 -0
  84. quantbt/optimization/evaluators/signal.py +43 -0
  85. quantbt/optimization/multiseed.py +176 -0
  86. quantbt/optimization/objectives.py +221 -0
  87. quantbt/optimization/optimizer.py +500 -0
  88. quantbt/optimization/result.py +80 -0
  89. quantbt/optimization/samplers.py +84 -0
  90. quantbt/optimization/space.py +223 -0
  91. quantbt/options/__init__.py +215 -0
  92. quantbt/options/cache.py +116 -0
  93. quantbt/options/conventions.py +158 -0
  94. quantbt/options/data.py +177 -0
  95. quantbt/options/execution.py +600 -0
  96. quantbt/options/fees.py +135 -0
  97. quantbt/options/greeks.py +173 -0
  98. quantbt/options/hedging.py +232 -0
  99. quantbt/options/iv.py +188 -0
  100. quantbt/options/ledger.py +263 -0
  101. quantbt/options/lifecycle.py +94 -0
  102. quantbt/options/margin.py +311 -0
  103. quantbt/options/packages.py +147 -0
  104. quantbt/options/pricing.py +191 -0
  105. quantbt/options/schema.py +227 -0
  106. quantbt/options/selectors.py +281 -0
  107. quantbt/options/strategy.py +249 -0
  108. quantbt/options/surface.py +142 -0
  109. quantbt/options/tape.py +226 -0
  110. quantbt/options/templates/__init__.py +33 -0
  111. quantbt/options/templates/packages.py +354 -0
  112. quantbt/portfolio.py +606 -0
  113. quantbt/py.typed +1 -0
  114. quantbt/reporting/__init__.py +38 -0
  115. quantbt/reporting/arbitrage_audit.py +211 -0
  116. quantbt/reporting/nautilus_bundle.py +758 -0
  117. quantbt/reporting/nautilus_certification.py +226 -0
  118. quantbt/reporting/nautilus_diagnostics.py +225 -0
  119. quantbt/reporting/parity.py +496 -0
  120. quantbt/reporting/portfolio_audit.py +206 -0
  121. quantbt/reporting/portfolio_nautilus.py +140 -0
  122. quantbt/sizing/__init__.py +3 -0
  123. quantbt/sizing/fast.py +67 -0
  124. quantbt/sizing/modes.py +162 -0
  125. quantbt/viz/__init__.py +4 -0
  126. quantbt/viz/plots.py +310 -0
  127. quantbt/viz/themes.py +102 -0
  128. quantbt/walkforward.py +3136 -0
  129. quantbt_engine-1.0.7.dist-info/METADATA +891 -0
  130. quantbt_engine-1.0.7.dist-info/RECORD +133 -0
  131. quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
  132. quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
  133. 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
+ ]