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,156 @@
1
+ """Session-aware intrabar execution primitives.
2
+
3
+ These objects are intentionally data-only. Calendar, timezone, and entry-window
4
+ logic are normalized before the execution kernel so the hot path never needs to
5
+ parse datetimes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+ from typing import Optional, Sequence
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+
20
+ class EntryPositionPolicy(str, Enum):
21
+ CURRENT_BEHAVIOR = "current_behavior"
22
+ FLAT_ONLY = "flat_only"
23
+ REVERSE = "reverse"
24
+
25
+
26
+ class SessionCounterBasis(str, Enum):
27
+ FILLED_ENTRY = "filled_entry"
28
+ ACCEPTED_ENTRY = "accepted_entry"
29
+
30
+
31
+ class ProtectiveExitReentryPolicy(str, Enum):
32
+ ALLOW = "allow"
33
+ SUPPRESS_SIGNAL_BAR = "suppress_signal_bar"
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class SessionExecutionPolicy:
38
+ entry_position_policy: EntryPositionPolicy = EntryPositionPolicy.CURRENT_BEHAVIOR
39
+ max_long_entries_per_session: Optional[int] = None
40
+ max_short_entries_per_session: Optional[int] = None
41
+ counter_basis: SessionCounterBasis = SessionCounterBasis.FILLED_ENTRY
42
+ cancel_pending_on_session_change: bool = True
43
+ suppress_entry_on_force_flat_bar: bool = True
44
+ protective_exit_reentry_policy: ProtectiveExitReentryPolicy = ProtectiveExitReentryPolicy.ALLOW
45
+
46
+ def __post_init__(self) -> None:
47
+ object.__setattr__(self, "entry_position_policy", EntryPositionPolicy(self.entry_position_policy))
48
+ object.__setattr__(self, "counter_basis", SessionCounterBasis(self.counter_basis))
49
+ object.__setattr__(self, "protective_exit_reentry_policy", ProtectiveExitReentryPolicy(self.protective_exit_reentry_policy))
50
+ for name in ("max_long_entries_per_session", "max_short_entries_per_session"):
51
+ value = getattr(self, name)
52
+ if value is not None and int(value) < 0:
53
+ raise ValueError(f"{name} must be >= 0 when provided")
54
+ if value is not None:
55
+ object.__setattr__(self, name, int(value))
56
+
57
+ def to_metadata(self) -> dict:
58
+ return {
59
+ "entry_position_policy": self.entry_position_policy.value,
60
+ "max_long_entries_per_session": self.max_long_entries_per_session,
61
+ "max_short_entries_per_session": self.max_short_entries_per_session,
62
+ "counter_basis": self.counter_basis.value,
63
+ "cancel_pending_on_session_change": bool(self.cancel_pending_on_session_change),
64
+ "suppress_entry_on_force_flat_bar": bool(self.suppress_entry_on_force_flat_bar),
65
+ "protective_exit_reentry_policy": self.protective_exit_reentry_policy.value,
66
+ }
67
+
68
+ @classmethod
69
+ def from_metadata(cls, metadata: Optional[dict]) -> Optional["SessionExecutionPolicy"]:
70
+ if metadata is None:
71
+ return None
72
+ if isinstance(metadata, SessionExecutionPolicy):
73
+ return metadata
74
+ return cls(**dict(metadata))
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class IntrabarSessionTape:
79
+ session_id: np.ndarray
80
+ entry_allowed_at_open: np.ndarray
81
+ force_flat_at_open: np.ndarray
82
+ signature: str = ""
83
+
84
+ def __post_init__(self) -> None:
85
+ session_id = np.ascontiguousarray(self.session_id, dtype=np.int64)
86
+ entry_allowed = np.ascontiguousarray(self.entry_allowed_at_open, dtype=np.bool_)
87
+ force_flat = np.ascontiguousarray(self.force_flat_at_open, dtype=np.bool_)
88
+ n = len(session_id)
89
+ if len(entry_allowed) != n or len(force_flat) != n:
90
+ raise ValueError("session tape arrays must have the same length")
91
+ session_id.setflags(write=False)
92
+ entry_allowed.setflags(write=False)
93
+ force_flat.setflags(write=False)
94
+ object.__setattr__(self, "session_id", session_id)
95
+ object.__setattr__(self, "entry_allowed_at_open", entry_allowed)
96
+ object.__setattr__(self, "force_flat_at_open", force_flat)
97
+ signature = self.signature or self._build_signature(session_id, entry_allowed, force_flat)
98
+ object.__setattr__(self, "signature", signature)
99
+
100
+ @classmethod
101
+ def from_index(
102
+ cls,
103
+ index: Sequence,
104
+ *,
105
+ timezone: str = "UTC",
106
+ session_key: str = "local_date",
107
+ entry_windows: Sequence[tuple[str, str]] = (),
108
+ force_flat_time: Optional[str] = None,
109
+ ) -> "IntrabarSessionTape":
110
+ idx = pd.DatetimeIndex(pd.to_datetime(index))
111
+ if idx.tz is None:
112
+ if not timezone:
113
+ raise ValueError("timezone is required for naive session indexes")
114
+ idx = idx.tz_localize(timezone)
115
+ local = idx.tz_convert(timezone)
116
+ if session_key != "local_date":
117
+ raise NotImplementedError("IntrabarSessionTape.from_index currently supports session_key='local_date'")
118
+ dates = pd.Index(local.date)
119
+ _, session_id = np.unique(dates.astype(str), return_inverse=True)
120
+ minutes = local.hour.to_numpy(dtype=np.int64) * 60 + local.minute.to_numpy(dtype=np.int64)
121
+ if entry_windows:
122
+ entry_allowed = np.zeros(len(local), dtype=np.bool_)
123
+ for start, end in entry_windows:
124
+ start_min = _parse_hhmm(start)
125
+ end_min = _parse_hhmm(end)
126
+ entry_allowed |= (minutes >= start_min) & (minutes <= end_min)
127
+ else:
128
+ entry_allowed = np.ones(len(local), dtype=np.bool_)
129
+ force_flat = np.zeros(len(local), dtype=np.bool_)
130
+ if force_flat_time is not None:
131
+ force_flat[:] = minutes == _parse_hhmm(force_flat_time)
132
+ return cls(
133
+ session_id=np.ascontiguousarray(session_id, dtype=np.int64),
134
+ entry_allowed_at_open=entry_allowed,
135
+ force_flat_at_open=force_flat,
136
+ )
137
+
138
+ @staticmethod
139
+ def _build_signature(session_id: np.ndarray, entry_allowed: np.ndarray, force_flat: np.ndarray) -> str:
140
+ h = hashlib.blake2b(digest_size=16)
141
+ for arr in (session_id, entry_allowed, force_flat):
142
+ h.update(np.ascontiguousarray(arr).view(np.uint8))
143
+ payload = {
144
+ "session_id": str(session_id.dtype),
145
+ "entry_allowed": str(entry_allowed.dtype),
146
+ "force_flat": str(force_flat.dtype),
147
+ "rows": int(len(session_id)),
148
+ "hash": h.hexdigest(),
149
+ }
150
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
151
+
152
+
153
+ def _parse_hhmm(value: str) -> int:
154
+ hour, minute = str(value).split(":", 1)
155
+ return int(hour) * 60 + int(minute)
156
+
@@ -0,0 +1,480 @@
1
+ """
2
+ Strict market tape preparation for execution-certified engines.
3
+
4
+ This module intentionally does not reuse the compatibility preprocessor. The
5
+ existing preprocessor is permissive for legacy notebooks; Phase 31 engines need
6
+ explicit validation and a certificate before kernels run.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ import hashlib
13
+ from typing import Dict, Optional, Sequence, Union
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+
19
+ SeriesMap = Dict[str, pd.Series]
20
+ FrameMap = Dict[str, pd.DataFrame]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class MarketValidationCertificate:
25
+ signature: str
26
+ row_count: int
27
+ symbol_count: int
28
+ timezone: str
29
+ first_timestamp_ns: int
30
+ last_timestamp_ns: int
31
+ finite_ok: bool
32
+ ohlc_ok: bool
33
+ monotonic_ok: bool
34
+ unique_ok: bool
35
+ alignment_ok: bool
36
+ bar_timestamp_semantics: str = "close"
37
+ validator_version: str = "market_tape_v1"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class PreparedMarketTape:
42
+ timestamps_ns: np.ndarray
43
+ symbols: tuple[str, ...]
44
+ opens: np.ndarray
45
+ highs: np.ndarray
46
+ lows: np.ndarray
47
+ closes: np.ndarray
48
+ volumes: np.ndarray
49
+ funding_rates: np.ndarray
50
+ funding_event_mask: np.ndarray
51
+ bar_timestamp_semantics: str
52
+ signature: str
53
+ validation_certificate: MarketValidationCertificate
54
+
55
+ @property
56
+ def n_bars(self) -> int:
57
+ return int(self.opens.shape[0])
58
+
59
+ @property
60
+ def n_symbols(self) -> int:
61
+ return int(self.opens.shape[1])
62
+
63
+
64
+ def prepare_market_tape(
65
+ *,
66
+ data: Optional[Union[pd.DataFrame, FrameMap]] = None,
67
+ opens: Optional[Union[pd.Series, SeriesMap]] = None,
68
+ highs: Optional[Union[pd.Series, SeriesMap]] = None,
69
+ lows: Optional[Union[pd.Series, SeriesMap]] = None,
70
+ closes: Optional[Union[pd.Series, SeriesMap]] = None,
71
+ volumes: Optional[Union[pd.Series, SeriesMap]] = None,
72
+ datetime_index: Optional[pd.DatetimeIndex] = None,
73
+ symbols: Optional[Sequence[str]] = None,
74
+ funding_rate: Union[float, pd.Series, Dict[str, Union[float, pd.Series]]] = 0.0,
75
+ funding_event_timestamps: Optional[Union[pd.DatetimeIndex, Sequence]] = None,
76
+ funding_event_rates: Optional[Union[Sequence, pd.Series, Dict[str, Union[Sequence, pd.Series]]]] = None,
77
+ use_funding: bool = True,
78
+ validation_mode: str = "strict",
79
+ missing_funding_policy: str = "raise",
80
+ source_timezone: Optional[str] = None,
81
+ bar_timestamp_semantics: str = "close",
82
+ ) -> PreparedMarketTape:
83
+ """
84
+ Build a strict, immutable OHLCV/funding tape.
85
+
86
+ `validation_mode="strict"` rejects unsorted, duplicate, missing, NaN, and
87
+ invalid OHLC data. It does not forward-fill or fallback high/low to close.
88
+ """
89
+ mode = str(validation_mode).lower().strip()
90
+ if mode not in {"strict", "trusted_prepared", "debug"}:
91
+ raise ValueError("validation_mode must be strict, trusted_prepared, or debug")
92
+ timestamp_semantics = _normalize_bar_timestamp_semantics(bar_timestamp_semantics)
93
+ if isinstance(data, PreparedMarketTape):
94
+ if data.bar_timestamp_semantics != timestamp_semantics:
95
+ raise ValueError(
96
+ "prepared market tape bar_timestamp_semantics does not match requested semantics"
97
+ )
98
+ return data
99
+
100
+ frames, symbol_list = _frames_from_inputs(
101
+ data=data,
102
+ opens=opens,
103
+ highs=highs,
104
+ lows=lows,
105
+ closes=closes,
106
+ volumes=volumes,
107
+ datetime_index=datetime_index,
108
+ symbols=symbols,
109
+ source_timezone=source_timezone,
110
+ )
111
+ if not symbol_list:
112
+ raise ValueError("at least one symbol is required")
113
+ idx = frames[symbol_list[0]].index
114
+ _validate_index(idx, name=symbol_list[0])
115
+ for symbol in symbol_list[1:]:
116
+ if not frames[symbol].index.equals(idx):
117
+ raise ValueError(f"symbol {symbol!r} index is not aligned to {symbol_list[0]!r}")
118
+
119
+ n = len(idx)
120
+ m = len(symbol_list)
121
+ opens_m = np.empty((n, m), dtype=np.float64)
122
+ highs_m = np.empty((n, m), dtype=np.float64)
123
+ lows_m = np.empty((n, m), dtype=np.float64)
124
+ closes_m = np.empty((n, m), dtype=np.float64)
125
+ volumes_m = np.empty((n, m), dtype=np.float64)
126
+ for j, symbol in enumerate(symbol_list):
127
+ frame = frames[symbol]
128
+ _validate_ohlcv_frame(frame, symbol)
129
+ opens_m[:, j] = frame["open"].to_numpy(dtype=np.float64)
130
+ highs_m[:, j] = frame["high"].to_numpy(dtype=np.float64)
131
+ lows_m[:, j] = frame["low"].to_numpy(dtype=np.float64)
132
+ closes_m[:, j] = frame["close"].to_numpy(dtype=np.float64)
133
+ volumes_m[:, j] = frame["volume"].to_numpy(dtype=np.float64)
134
+
135
+ ohlcv = np.stack((opens_m, highs_m, lows_m, closes_m, volumes_m), axis=2)
136
+ finite_ok = bool(np.isfinite(ohlcv).all())
137
+ if not finite_ok:
138
+ raise ValueError("OHLCV contains NaN or infinite values")
139
+ ohlc_ok = bool(
140
+ (
141
+ (lows_m <= opens_m)
142
+ & (lows_m <= closes_m)
143
+ & (highs_m >= opens_m)
144
+ & (highs_m >= closes_m)
145
+ & (highs_m >= lows_m)
146
+ & (opens_m > 0.0)
147
+ & (highs_m > 0.0)
148
+ & (lows_m > 0.0)
149
+ & (closes_m > 0.0)
150
+ & (volumes_m >= 0.0)
151
+ ).all()
152
+ )
153
+ if not ohlc_ok:
154
+ raise ValueError("invalid OHLCV invariant")
155
+
156
+ timestamps_ns = idx.view("int64").astype(np.int64, copy=True)
157
+ funding_m, funding_mask = _prepare_funding_matrix(
158
+ funding_rate=funding_rate,
159
+ funding_event_timestamps=funding_event_timestamps,
160
+ funding_event_rates=funding_event_rates,
161
+ use_funding=use_funding,
162
+ symbols=symbol_list,
163
+ idx=idx,
164
+ missing_funding_policy=missing_funding_policy,
165
+ source_timezone=source_timezone,
166
+ )
167
+ signature = _signature(
168
+ timestamps_ns,
169
+ symbol_list,
170
+ opens_m,
171
+ highs_m,
172
+ lows_m,
173
+ closes_m,
174
+ volumes_m,
175
+ funding_m,
176
+ funding_mask.astype(np.float64),
177
+ metadata=f"bar_timestamp_semantics={timestamp_semantics}",
178
+ )
179
+ cert = MarketValidationCertificate(
180
+ signature=signature,
181
+ row_count=int(n),
182
+ symbol_count=int(m),
183
+ timezone=str(idx.tz),
184
+ first_timestamp_ns=int(timestamps_ns[0]),
185
+ last_timestamp_ns=int(timestamps_ns[-1]),
186
+ finite_ok=finite_ok,
187
+ ohlc_ok=ohlc_ok,
188
+ monotonic_ok=True,
189
+ unique_ok=True,
190
+ alignment_ok=True,
191
+ bar_timestamp_semantics=timestamp_semantics,
192
+ )
193
+ arrays = (timestamps_ns, opens_m, highs_m, lows_m, closes_m, volumes_m, funding_m, funding_mask)
194
+ for arr in arrays:
195
+ arr.setflags(write=False)
196
+ return PreparedMarketTape(
197
+ timestamps_ns=np.ascontiguousarray(timestamps_ns),
198
+ symbols=tuple(symbol_list),
199
+ opens=np.ascontiguousarray(opens_m),
200
+ highs=np.ascontiguousarray(highs_m),
201
+ lows=np.ascontiguousarray(lows_m),
202
+ closes=np.ascontiguousarray(closes_m),
203
+ volumes=np.ascontiguousarray(volumes_m),
204
+ funding_rates=np.ascontiguousarray(funding_m),
205
+ funding_event_mask=np.ascontiguousarray(funding_mask),
206
+ bar_timestamp_semantics=timestamp_semantics,
207
+ signature=signature,
208
+ validation_certificate=cert,
209
+ )
210
+
211
+
212
+ def _frames_from_inputs(
213
+ *,
214
+ data,
215
+ opens,
216
+ highs,
217
+ lows,
218
+ closes,
219
+ volumes,
220
+ datetime_index,
221
+ symbols,
222
+ source_timezone,
223
+ ) -> tuple[FrameMap, list[str]]:
224
+ if data is not None:
225
+ if isinstance(data, pd.DataFrame):
226
+ symbol_list = list(symbols or ["DEFAULT"])
227
+ if len(symbol_list) != 1:
228
+ raise ValueError("single DataFrame market tape requires one symbol")
229
+ return {symbol_list[0]: _standard_frame(data, datetime_index, source_timezone=source_timezone)}, symbol_list
230
+ symbol_list = list(symbols or data.keys())
231
+ return {symbol: _standard_frame(data[symbol], datetime_index=None, source_timezone=source_timezone) for symbol in symbol_list}, symbol_list
232
+
233
+ if closes is None:
234
+ raise ValueError("closes or data is required")
235
+ if isinstance(closes, pd.Series):
236
+ symbol_list = list(symbols or ["DEFAULT"])
237
+ if len(symbol_list) != 1:
238
+ raise ValueError("single Series market tape requires one symbol")
239
+ symbol = symbol_list[0]
240
+ idx = _strict_index(datetime_index if datetime_index is not None else closes.index, name=symbol, source_timezone=source_timezone)
241
+ frame = pd.DataFrame(
242
+ {
243
+ "open": _series_for_symbol(opens, symbol, idx, required=True),
244
+ "high": _series_for_symbol(highs, symbol, idx, required=True),
245
+ "low": _series_for_symbol(lows, symbol, idx, required=True),
246
+ "close": _align_exact(closes, idx, "close"),
247
+ "volume": _series_for_symbol(volumes, symbol, idx, required=False),
248
+ },
249
+ index=idx,
250
+ )
251
+ return {symbol: frame}, symbol_list
252
+
253
+ symbol_list = list(symbols or closes.keys())
254
+ idx = _strict_index(datetime_index if datetime_index is not None else closes[symbol_list[0]].index, name=symbol_list[0], source_timezone=source_timezone)
255
+ frames = {}
256
+ for symbol in symbol_list:
257
+ frames[symbol] = pd.DataFrame(
258
+ {
259
+ "open": _series_for_symbol(opens, symbol, idx, required=True),
260
+ "high": _series_for_symbol(highs, symbol, idx, required=True),
261
+ "low": _series_for_symbol(lows, symbol, idx, required=True),
262
+ "close": _align_exact(closes[symbol], idx, "close"),
263
+ "volume": _series_for_symbol(volumes, symbol, idx, required=False),
264
+ },
265
+ index=idx,
266
+ )
267
+ return frames, symbol_list
268
+
269
+
270
+ def _standard_frame(data: pd.DataFrame, datetime_index=None, *, source_timezone: Optional[str] = None) -> pd.DataFrame:
271
+ frame = data.copy().rename(
272
+ columns={
273
+ "Datetime": "timestamp",
274
+ "Date": "timestamp",
275
+ "Timestamp": "timestamp",
276
+ "Open": "open",
277
+ "High": "high",
278
+ "Low": "low",
279
+ "Close": "close",
280
+ "Volume": "volume",
281
+ }
282
+ )
283
+ if datetime_index is not None:
284
+ frame.index = _strict_index(datetime_index, name="datetime_index", source_timezone=source_timezone)
285
+ elif "timestamp" in frame.columns:
286
+ frame = frame.set_index(_strict_index(frame["timestamp"], name="timestamp", source_timezone=source_timezone))
287
+ else:
288
+ frame.index = _strict_index(frame.index, name="data", source_timezone=source_timezone)
289
+ required = {"open", "high", "low", "close"}
290
+ missing = sorted(required - set(frame.columns))
291
+ if missing:
292
+ raise ValueError(f"market data is missing required columns {missing}")
293
+ if "volume" not in frame.columns:
294
+ frame["volume"] = 0.0
295
+ frame = frame[["open", "high", "low", "close", "volume"]].copy()
296
+ frame.index = _strict_index(frame.index, name="data", source_timezone=source_timezone)
297
+ return frame
298
+
299
+
300
+ def _strict_index(value, *, name: str, source_timezone: Optional[str] = None) -> pd.DatetimeIndex:
301
+ raw = pd.DatetimeIndex(pd.to_datetime(value, errors="raise"))
302
+ if raw.tz is None:
303
+ if source_timezone is None:
304
+ raise ValueError(f"{name} index is timezone-naive; pass source_timezone for strict market tape")
305
+ raw = raw.tz_localize(source_timezone)
306
+ idx = raw.tz_convert("UTC")
307
+ _validate_index(idx, name=name)
308
+ return idx
309
+
310
+
311
+ def _validate_index(idx: pd.DatetimeIndex, *, name: str) -> None:
312
+ if len(idx) == 0:
313
+ raise ValueError(f"{name} index is empty")
314
+ values = idx.view("int64")
315
+ if not bool(np.all(values[1:] > values[:-1])):
316
+ if bool(pd.Index(values).duplicated().any()):
317
+ raise ValueError(f"{name} index contains duplicate timestamps")
318
+ raise ValueError(f"{name} index must be strictly increasing")
319
+ if idx.tz is None:
320
+ raise ValueError(f"{name} index must be timezone-aware")
321
+
322
+
323
+ def _validate_ohlcv_frame(frame: pd.DataFrame, symbol: str) -> None:
324
+ if len(frame) == 0:
325
+ raise ValueError(f"{symbol} market data is empty")
326
+ missing = [col for col in ("open", "high", "low", "close", "volume") if col not in frame]
327
+ if missing:
328
+ raise ValueError(f"{symbol} market data is missing columns {missing}")
329
+
330
+
331
+ def _series_for_symbol(data, symbol: str, idx: pd.DatetimeIndex, *, required: bool) -> pd.Series:
332
+ if data is None:
333
+ if required:
334
+ raise ValueError(f"{symbol} requires explicit open/high/low/close for strict market tape")
335
+ return pd.Series(0.0, index=idx, name="volume")
336
+ if isinstance(data, pd.Series):
337
+ series = data
338
+ else:
339
+ if symbol not in data:
340
+ if required:
341
+ raise KeyError(f"{symbol!r} missing from strict market tape input")
342
+ return pd.Series(0.0, index=idx, name="volume")
343
+ series = data[symbol]
344
+ return _align_exact(series, idx, symbol)
345
+
346
+
347
+ def _align_exact(series: pd.Series, idx: pd.DatetimeIndex, name: str) -> pd.Series:
348
+ s = series.copy()
349
+ s.index = _strict_index(s.index, name=name)
350
+ if not s.index.equals(idx):
351
+ raise ValueError(f"{name} series index is not exactly aligned")
352
+ return pd.to_numeric(s, errors="raise").astype(float)
353
+
354
+
355
+ def _prepare_funding_matrix(
356
+ *,
357
+ funding_rate,
358
+ funding_event_timestamps,
359
+ funding_event_rates,
360
+ use_funding: bool,
361
+ symbols: list[str],
362
+ idx: pd.DatetimeIndex,
363
+ missing_funding_policy: str,
364
+ source_timezone: Optional[str],
365
+ ) -> tuple[np.ndarray, np.ndarray]:
366
+ n = len(idx)
367
+ m = len(symbols)
368
+ funding = np.zeros((n, m), dtype=np.float64)
369
+ mask = np.zeros(n, dtype=np.bool_)
370
+ if not use_funding:
371
+ return funding, mask
372
+ policy = str(missing_funding_policy or "raise").lower().strip()
373
+ if policy not in {"raise", "zero"}:
374
+ raise ValueError("missing_funding_policy must be raise or zero")
375
+ if funding_event_timestamps is not None or funding_event_rates is not None:
376
+ if funding_event_timestamps is None or funding_event_rates is None:
377
+ raise ValueError("funding_event_timestamps and funding_event_rates must be provided together")
378
+ return _funding_from_events(
379
+ event_timestamps=funding_event_timestamps,
380
+ event_rates=funding_event_rates,
381
+ symbols=symbols,
382
+ idx=idx,
383
+ source_timezone=source_timezone,
384
+ )
385
+ if isinstance(funding_rate, dict):
386
+ for j, symbol in enumerate(symbols):
387
+ if symbol not in funding_rate:
388
+ if policy == "zero":
389
+ continue
390
+ raise KeyError(f"funding_rate dict is missing symbol {symbol!r}")
391
+ value = funding_rate[symbol]
392
+ if isinstance(value, pd.Series):
393
+ funding[:, j] = _align_exact(value, idx, f"funding:{symbol}").to_numpy(dtype=np.float64)
394
+ else:
395
+ scalar = float(value)
396
+ if policy != "zero":
397
+ raise ValueError("strict funding requires event timestamps/rates or an aligned Series; scalar funding is not event-causal")
398
+ funding[:, j] = scalar
399
+ elif isinstance(funding_rate, pd.Series):
400
+ series = _align_exact(funding_rate, idx, "funding")
401
+ funding[:, :] = series.to_numpy(dtype=np.float64)[:, None]
402
+ else:
403
+ scalar = float(funding_rate)
404
+ if scalar != 0.0 or policy != "zero":
405
+ raise ValueError("strict funding requires funding events or an aligned Series; use_funding=False or missing_funding_policy='zero' for no funding")
406
+ funding[:, :] = 0.0
407
+ mask[1:] = funding[1:].any(axis=1)
408
+ return funding, mask
409
+
410
+
411
+ def _funding_from_events(
412
+ *,
413
+ event_timestamps,
414
+ event_rates,
415
+ symbols: list[str],
416
+ idx: pd.DatetimeIndex,
417
+ source_timezone: Optional[str],
418
+ ) -> tuple[np.ndarray, np.ndarray]:
419
+ event_idx = _strict_index(event_timestamps, name="funding_events", source_timezone=source_timezone)
420
+ if len(event_idx) == 0:
421
+ return np.zeros((len(idx), len(symbols)), dtype=np.float64), np.zeros(len(idx), dtype=np.bool_)
422
+ event_ns = event_idx.view("int64")
423
+ if isinstance(event_rates, dict):
424
+ rates_by_symbol = {}
425
+ for symbol in symbols:
426
+ if symbol not in event_rates:
427
+ raise KeyError(f"funding_event_rates dict is missing symbol {symbol!r}")
428
+ rates_by_symbol[symbol] = _event_rate_values(event_rates[symbol], event_idx, symbol)
429
+ else:
430
+ values = _event_rate_values(event_rates, event_idx, "funding_events")
431
+ rates_by_symbol = {symbol: values for symbol in symbols}
432
+
433
+ funding = np.zeros((len(idx), len(symbols)), dtype=np.float64)
434
+ mask = np.zeros(len(idx), dtype=np.bool_)
435
+ idx_ns = idx.view("int64")
436
+ for k, ts_ns in enumerate(event_ns):
437
+ bar = int(np.searchsorted(idx_ns, ts_ns, side="left"))
438
+ if bar >= len(idx_ns) or idx_ns[bar] != ts_ns:
439
+ raise ValueError("funding events must align exactly to a market bar timestamp for POSITION_AT_EVENT certification")
440
+ if bar == 0:
441
+ raise ValueError("funding event at the first bar cannot be certified because no prior position interval exists")
442
+ for j, symbol in enumerate(symbols):
443
+ rate = float(rates_by_symbol[symbol][k])
444
+ if rate != 0.0:
445
+ funding[bar, j] += rate
446
+ mask[bar] = True
447
+ return funding, mask
448
+
449
+
450
+ def _event_rate_values(value, event_idx: pd.DatetimeIndex, name: str) -> np.ndarray:
451
+ if isinstance(value, pd.Series):
452
+ series = value.copy()
453
+ series.index = _strict_index(series.index, name=f"funding_event_rates:{name}")
454
+ if not series.index.equals(event_idx):
455
+ raise ValueError(f"funding event rates for {name} must align exactly to funding_event_timestamps")
456
+ return pd.to_numeric(series, errors="raise").to_numpy(dtype=np.float64)
457
+ arr = np.asarray(value, dtype=np.float64)
458
+ if arr.ndim == 0:
459
+ raise ValueError("funding_event_rates scalar is not valid; pass one rate per funding event")
460
+ if len(arr) != len(event_idx):
461
+ raise ValueError("funding_event_rates length must match funding_event_timestamps")
462
+ return np.ascontiguousarray(arr, dtype=np.float64)
463
+
464
+
465
+ def _normalize_bar_timestamp_semantics(value: str) -> str:
466
+ semantics = str(value or "close").lower().strip()
467
+ if semantics not in {"open", "close"}:
468
+ raise ValueError("bar_timestamp_semantics must be 'open' or 'close'")
469
+ return semantics
470
+
471
+
472
+ def _signature(timestamps_ns: np.ndarray, symbols: list[str], *arrays: np.ndarray, metadata: str = "") -> str:
473
+ h = hashlib.sha256()
474
+ h.update(np.ascontiguousarray(timestamps_ns).view(np.uint8))
475
+ h.update("|".join(symbols).encode("utf-8"))
476
+ if metadata:
477
+ h.update(str(metadata).encode("utf-8"))
478
+ for arr in arrays:
479
+ h.update(np.ascontiguousarray(arr).view(np.uint8))
480
+ return h.hexdigest()