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,1906 @@
1
+ """
2
+ Fast Numba kernels for Phase 31 intrabar execution contracts.
3
+
4
+ The public Python reference oracle remains the readability source of truth.
5
+ This module mirrors that state machine with primitive arrays only: no Python
6
+ objects are created inside hot loops, and sparse fills are generated only by an
7
+ optional deterministic second pass.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Dict, Optional, Sequence
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+ from numba import njit
18
+
19
+ from .execution_contract import ExecutionContract, IntrabarSameBarPolicy, TakeProfitGapPolicy
20
+ from .intrabar_reference import IntrabarFill, IntrabarFillReason, IntrabarIntentTape, IntrabarLevelMode, IntrabarSizingMode, _validate_intrabar_contract_supported
21
+ from .intrabar_session import EntryPositionPolicy, IntrabarSessionTape, ProtectiveExitReentryPolicy, SessionCounterBasis, SessionExecutionPolicy
22
+ from .market_tape import PreparedMarketTape
23
+ from .schema import AccountConfig
24
+
25
+
26
+ LEVEL_ABSOLUTE_PRICE = 1
27
+ LEVEL_PRICE_DISTANCE = 2
28
+ LEVEL_PERCENT_DISTANCE = 3
29
+
30
+ SAME_BAR_CONSERVATIVE = 1
31
+ SAME_BAR_STOP_FIRST = 2
32
+ SAME_BAR_TP_FIRST = 3
33
+ SAME_BAR_OHLC_PATH = 4
34
+ SAME_BAR_OLHC_PATH = 5
35
+ SAME_BAR_REJECT_AMBIGUOUS = 6
36
+
37
+ TP_LIMIT_CONSERVATIVE = 1
38
+ TP_OPEN_PRICE_IMPROVEMENT = 2
39
+
40
+ FILL_ENTRY = 1
41
+ FILL_TECHNICAL_EXIT = 2
42
+ FILL_REVERSAL_EXIT = 3
43
+ FILL_REVERSAL_ENTRY = 4
44
+ FILL_STOP_LOSS = 5
45
+ FILL_TAKE_PROFIT = 6
46
+ FILL_LIQUIDATION = 7
47
+ FILL_FINAL_CLOSE = 8
48
+ FILL_SESSION_FORCED_EXIT = 9
49
+
50
+ FLAG_ENTRY_FILLED = 1 << 0
51
+ FLAG_EXIT_FILLED = 1 << 1
52
+ FLAG_STOP_FILLED = 1 << 2
53
+ FLAG_TP_FILLED = 1 << 3
54
+ FLAG_TECH_EXIT = 1 << 4
55
+ FLAG_REVERSAL = 1 << 5
56
+ FLAG_AMBIGUOUS = 1 << 6
57
+ FLAG_FUNDING = 1 << 7
58
+ FLAG_LIQUIDATION = 1 << 8
59
+ FLAG_REJECTED = 1 << 9
60
+ FLAG_ENTRY_SUPPRESSED = 1 << 10
61
+ FLAG_SESSION_RESET = 1 << 11
62
+ FLAG_SESSION_FORCED_EXIT = 1 << 12
63
+ FLAG_ENTRY_WINDOW_BLOCKED = 1 << 13
64
+ FLAG_ENTRY_QUOTA_BLOCKED = 1 << 14
65
+ FLAG_FLAT_ONLY_BLOCKED = 1 << 15
66
+ FLAG_STALE_SESSION_SIGNAL = 1 << 16
67
+ FLAG_PROTECTIVE_REENTRY_BLOCKED = 1 << 17
68
+
69
+ SIZING_UNITS = 1
70
+ SIZING_FIXED_NOTIONAL = 2
71
+ SIZING_PCT_EQUITY = 3
72
+ SIZING_RISK_PER_TRADE = 4
73
+
74
+ BAR_TS_CLOSE = 1
75
+ BAR_TS_OPEN = 2
76
+
77
+ SESSION_ENTRY_CURRENT = 1
78
+ SESSION_ENTRY_FLAT_ONLY = 2
79
+ SESSION_ENTRY_REVERSE = 3
80
+
81
+ SESSION_COUNTER_FILLED = 1
82
+ SESSION_COUNTER_ACCEPTED = 2
83
+
84
+ SESSION_REENTRY_ALLOW = 1
85
+ SESSION_REENTRY_SUPPRESS_SIGNAL_BAR = 2
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class NativeIntrabarKernelResult:
90
+ equity: pd.Series
91
+ position: pd.Series
92
+ average_entry: pd.Series
93
+ active_stop: pd.Series
94
+ active_take_profit: pd.Series
95
+ fees: pd.Series
96
+ funding: pd.Series
97
+ event_flags: pd.Series
98
+ initial_margin: pd.Series
99
+ maintenance_margin: pd.Series
100
+ fills: tuple[IntrabarFill, ...] = ()
101
+ fills_report: pd.DataFrame = field(default_factory=pd.DataFrame)
102
+ ambiguity_count: int = 0
103
+ rejected_count: int = 0
104
+ fill_count: int = 0
105
+ liquidated: bool = False
106
+ liquidation_bar: int = -1
107
+ report_level: str = "standard"
108
+ metadata: Dict = field(default_factory=dict)
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class FillReplayTape:
113
+ bar_index: np.ndarray
114
+ sequence: np.ndarray
115
+ side: np.ndarray
116
+ qty: np.ndarray
117
+ price: np.ndarray
118
+ fee: np.ndarray
119
+ reason: np.ndarray
120
+
121
+ @classmethod
122
+ def from_frame(cls, frame: pd.DataFrame, *, fee_rate: float = 0.0, contract_size: float = 1.0) -> "FillReplayTape":
123
+ required = {"bar_index", "side", "qty", "price"}
124
+ missing = sorted(required - set(frame.columns))
125
+ if missing:
126
+ raise ValueError(f"fill replay frame is missing columns {missing}")
127
+ sequence = frame["sequence"] if "sequence" in frame else pd.Series(np.arange(len(frame)), index=frame.index)
128
+ price = pd.to_numeric(frame["price"], errors="raise").to_numpy(dtype=np.float64)
129
+ qty = pd.to_numeric(frame["qty"], errors="raise").to_numpy(dtype=np.float64)
130
+ if "fee" in frame:
131
+ fee = pd.to_numeric(frame["fee"], errors="raise").to_numpy(dtype=np.float64)
132
+ else:
133
+ fee = np.abs(qty) * price * float(contract_size) * float(fee_rate)
134
+ reason = _reason_series_to_codes(frame["reason"]) if "reason" in frame else np.zeros(len(frame), dtype=np.int16)
135
+ return cls(
136
+ bar_index=np.ascontiguousarray(pd.to_numeric(frame["bar_index"], errors="raise").to_numpy(dtype=np.int64)),
137
+ sequence=np.ascontiguousarray(pd.to_numeric(sequence, errors="raise").to_numpy(dtype=np.int64)),
138
+ side=np.ascontiguousarray(np.sign(pd.to_numeric(frame["side"], errors="raise").to_numpy(dtype=np.float64)).astype(np.int8)),
139
+ qty=np.ascontiguousarray(qty, dtype=np.float64),
140
+ price=np.ascontiguousarray(price, dtype=np.float64),
141
+ fee=np.ascontiguousarray(fee, dtype=np.float64),
142
+ reason=np.ascontiguousarray(reason, dtype=np.int16),
143
+ )
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class NativeFillReplayResult:
148
+ equity: pd.Series
149
+ position: pd.Series
150
+ fees: pd.Series
151
+ event_flags: pd.Series
152
+ fill_count: int
153
+ metadata: Dict = field(default_factory=dict)
154
+
155
+
156
+ def run_intrabar_kernel(
157
+ *,
158
+ tape: PreparedMarketTape,
159
+ intent: IntrabarIntentTape,
160
+ account: AccountConfig,
161
+ contract: Optional[ExecutionContract] = None,
162
+ fee_rate: float = 0.0,
163
+ slippage_rate: float = 0.0,
164
+ contract_size: float = 1.0,
165
+ sizing_mode: IntrabarSizingMode | str = IntrabarSizingMode.UNITS,
166
+ fixed_notional: float = 0.0,
167
+ equity_fraction: float = 0.0,
168
+ risk_fraction: float = 0.0,
169
+ qty_step: float = 0.0,
170
+ min_qty: float = 0.0,
171
+ min_notional: float = 0.0,
172
+ tick_size: float = 0.0,
173
+ report_level: str = "standard",
174
+ ) -> NativeIntrabarKernelResult:
175
+ """
176
+ Run the fast single-symbol `intrabar_bracket_v1` Numba kernel.
177
+
178
+ `report_level="audit"` triggers the second pass and materializes sparse
179
+ fills. `minimal` and `standard` keep fill accounting as counters/flags only.
180
+ """
181
+ if tape.n_symbols != 1:
182
+ raise NotImplementedError("intrabar fast kernel v1 supports exactly one symbol")
183
+ if len(intent.entry_side) != tape.n_bars:
184
+ raise ValueError("intent length must match market tape length")
185
+ if fee_rate < 0.0 or slippage_rate < 0.0:
186
+ raise ValueError("fee_rate and slippage_rate must be >= 0")
187
+ level = _normalize_report_level(report_level)
188
+ contract = contract or ExecutionContract.intrabar_bracket()
189
+ if contract.engine_id != "intrabar_bracket_v1":
190
+ raise ValueError("run_intrabar_kernel requires intrabar_bracket_v1 contract")
191
+ _validate_intrabar_contract_supported(contract)
192
+ if contract.same_bar_policy is IntrabarSameBarPolicy.REJECT_AMBIGUOUS:
193
+ raise NotImplementedError("fast intrabar kernel v1 does not support REJECT_AMBIGUOUS; use the reference oracle for debug rejection")
194
+ sizing_mode_value = IntrabarSizingMode(sizing_mode)
195
+
196
+ arrays = _run_intrabar_pass(
197
+ record_fills=False,
198
+ fill_capacity=1,
199
+ tape=tape,
200
+ intent=intent,
201
+ account=account,
202
+ contract=contract,
203
+ fee_rate=fee_rate,
204
+ slippage_rate=slippage_rate,
205
+ contract_size=contract_size,
206
+ sizing_mode=sizing_mode_value,
207
+ fixed_notional=fixed_notional,
208
+ equity_fraction=equity_fraction,
209
+ risk_fraction=risk_fraction,
210
+ qty_step=qty_step,
211
+ min_qty=min_qty,
212
+ min_notional=min_notional,
213
+ tick_size=tick_size,
214
+ )
215
+ (
216
+ equity,
217
+ position,
218
+ avg_entry,
219
+ active_stop,
220
+ active_tp,
221
+ fees,
222
+ funding,
223
+ flags,
224
+ initial_margin,
225
+ maintenance_margin,
226
+ fill_count,
227
+ ambiguity_count,
228
+ rejected_count,
229
+ liquidated,
230
+ liquidation_bar,
231
+ _fill_bar,
232
+ _fill_seq,
233
+ _fill_side,
234
+ _fill_qty,
235
+ _fill_price,
236
+ _fill_fee,
237
+ _fill_reason,
238
+ ) = arrays
239
+
240
+ fills: tuple[IntrabarFill, ...] = ()
241
+ fills_report = pd.DataFrame()
242
+ if level == "audit":
243
+ audit = _run_intrabar_pass(
244
+ record_fills=True,
245
+ fill_capacity=int(fill_count),
246
+ tape=tape,
247
+ intent=intent,
248
+ account=account,
249
+ contract=contract,
250
+ fee_rate=fee_rate,
251
+ slippage_rate=slippage_rate,
252
+ contract_size=contract_size,
253
+ sizing_mode=sizing_mode_value,
254
+ fixed_notional=fixed_notional,
255
+ equity_fraction=equity_fraction,
256
+ risk_fraction=risk_fraction,
257
+ qty_step=qty_step,
258
+ min_qty=min_qty,
259
+ min_notional=min_notional,
260
+ tick_size=tick_size,
261
+ )
262
+ _assert_intrabar_audit_parity(arrays, audit)
263
+ fills = _materialize_intrabar_fills(
264
+ timestamps_ns=tape.timestamps_ns,
265
+ fill_bar=audit[15],
266
+ fill_seq=audit[16],
267
+ fill_side=audit[17],
268
+ fill_qty=audit[18],
269
+ fill_price=audit[19],
270
+ fill_fee=audit[20],
271
+ fill_reason=audit[21],
272
+ fill_count=int(fill_count),
273
+ )
274
+ fills_report = _fills_to_report(fills)
275
+
276
+ idx = pd.DatetimeIndex(pd.to_datetime(tape.timestamps_ns, utc=True))
277
+ symbol = tape.symbols[0]
278
+ metadata = {
279
+ "engine": "intrabar_bracket_v1",
280
+ "engine_id": "intrabar_bracket_v1",
281
+ "backend": "native_intrabar",
282
+ "backend_alias": "native_intrabar",
283
+ "kernel_version": "intrabar_numba_v1",
284
+ "execution_contract": contract.to_metadata(),
285
+ "data_signature": tape.signature,
286
+ "validation_certificate": tape.validation_certificate.__dict__.copy(),
287
+ "report_level": level,
288
+ "two_pass_audit": level == "audit",
289
+ "fill_count": int(fill_count),
290
+ "ambiguity_count": int(ambiguity_count),
291
+ "rejected_count": int(rejected_count),
292
+ "liquidated": bool(liquidated),
293
+ "liquidation_bar": int(liquidation_bar),
294
+ "funding_timing_certified": True,
295
+ "funding_event_alignment": "exact_bar_timestamp",
296
+ "bar_timestamp_semantics": tape.bar_timestamp_semantics,
297
+ "funding_event_price_reference": "open" if tape.bar_timestamp_semantics == "open" else "close",
298
+ "sizing_mode": sizing_mode_value.value,
299
+ "sizing": {
300
+ "fixed_notional": float(fixed_notional),
301
+ "equity_fraction": float(equity_fraction),
302
+ "risk_fraction": float(risk_fraction),
303
+ },
304
+ "quantity_constraints": {
305
+ "qty_step": float(qty_step),
306
+ "min_qty": float(min_qty),
307
+ "min_notional": float(min_notional),
308
+ "tick_size": float(tick_size),
309
+ },
310
+ }
311
+ return NativeIntrabarKernelResult(
312
+ equity=pd.Series(equity, index=idx, name="equity"),
313
+ position=pd.Series(position, index=idx, name=f"Position_{symbol}"),
314
+ average_entry=pd.Series(avg_entry, index=idx, name="average_entry"),
315
+ active_stop=pd.Series(active_stop, index=idx, name="active_stop"),
316
+ active_take_profit=pd.Series(active_tp, index=idx, name="active_take_profit"),
317
+ fees=pd.Series(fees, index=idx, name="fees"),
318
+ funding=pd.Series(funding, index=idx, name="funding"),
319
+ event_flags=pd.Series(flags, index=idx, name="event_flags"),
320
+ initial_margin=pd.Series(initial_margin, index=idx, name="initial_margin"),
321
+ maintenance_margin=pd.Series(maintenance_margin, index=idx, name="maintenance_margin"),
322
+ fills=fills,
323
+ fills_report=fills_report,
324
+ ambiguity_count=int(ambiguity_count),
325
+ rejected_count=int(rejected_count),
326
+ fill_count=int(fill_count),
327
+ liquidated=bool(liquidated),
328
+ liquidation_bar=int(liquidation_bar),
329
+ report_level=level,
330
+ metadata=metadata,
331
+ )
332
+
333
+
334
+ def run_intrabar_session_kernel(
335
+ *,
336
+ tape: PreparedMarketTape,
337
+ intent: IntrabarIntentTape,
338
+ account: AccountConfig,
339
+ session_policy: SessionExecutionPolicy,
340
+ session_tape: IntrabarSessionTape,
341
+ contract: Optional[ExecutionContract] = None,
342
+ fee_rate: float = 0.0,
343
+ slippage_rate: float = 0.0,
344
+ contract_size: float = 1.0,
345
+ sizing_mode: IntrabarSizingMode | str = IntrabarSizingMode.UNITS,
346
+ fixed_notional: float = 0.0,
347
+ equity_fraction: float = 0.0,
348
+ risk_fraction: float = 0.0,
349
+ qty_step: float = 0.0,
350
+ min_qty: float = 0.0,
351
+ min_notional: float = 0.0,
352
+ tick_size: float = 0.0,
353
+ report_level: str = "standard",
354
+ ) -> NativeIntrabarKernelResult:
355
+ """Run the fast session-aware single-symbol intrabar kernel."""
356
+ if tape.n_symbols != 1:
357
+ raise NotImplementedError("session intrabar fast kernel v1 supports exactly one symbol")
358
+ if len(intent.entry_side) != tape.n_bars:
359
+ raise ValueError("intent length must match market tape length")
360
+ if len(session_tape.session_id) != tape.n_bars:
361
+ raise ValueError("session_tape length must match market tape length")
362
+ if fee_rate < 0.0 or slippage_rate < 0.0:
363
+ raise ValueError("fee_rate and slippage_rate must be >= 0")
364
+ level = _normalize_report_level(report_level)
365
+ contract = contract or ExecutionContract.intrabar_bracket()
366
+ if contract.engine_id != "intrabar_bracket_v1":
367
+ raise ValueError("run_intrabar_session_kernel requires intrabar_bracket_v1 contract")
368
+ _validate_intrabar_contract_supported(contract)
369
+ if contract.same_bar_policy is IntrabarSameBarPolicy.REJECT_AMBIGUOUS:
370
+ raise NotImplementedError("fast session intrabar kernel v1 does not support REJECT_AMBIGUOUS")
371
+ sizing_mode_value = IntrabarSizingMode(sizing_mode)
372
+ policy = SessionExecutionPolicy.from_metadata(session_policy.to_metadata())
373
+
374
+ arrays = _run_intrabar_session_pass(
375
+ record_fills=False,
376
+ fill_capacity=1,
377
+ tape=tape,
378
+ intent=intent,
379
+ account=account,
380
+ contract=contract,
381
+ fee_rate=fee_rate,
382
+ slippage_rate=slippage_rate,
383
+ contract_size=contract_size,
384
+ sizing_mode=sizing_mode_value,
385
+ fixed_notional=fixed_notional,
386
+ equity_fraction=equity_fraction,
387
+ risk_fraction=risk_fraction,
388
+ qty_step=qty_step,
389
+ min_qty=min_qty,
390
+ min_notional=min_notional,
391
+ tick_size=tick_size,
392
+ session_policy=policy,
393
+ session_tape=session_tape,
394
+ )
395
+ (
396
+ equity,
397
+ position,
398
+ avg_entry,
399
+ active_stop,
400
+ active_tp,
401
+ fees,
402
+ funding,
403
+ flags,
404
+ initial_margin,
405
+ maintenance_margin,
406
+ fill_count,
407
+ ambiguity_count,
408
+ rejected_count,
409
+ liquidated,
410
+ liquidation_bar,
411
+ _fill_bar,
412
+ _fill_seq,
413
+ _fill_side,
414
+ _fill_qty,
415
+ _fill_price,
416
+ _fill_fee,
417
+ _fill_reason,
418
+ session_reset_count,
419
+ session_forced_exit_count,
420
+ entry_window_blocked_count,
421
+ long_quota_blocked_count,
422
+ short_quota_blocked_count,
423
+ flat_only_blocked_count,
424
+ stale_session_signal_count,
425
+ reentry_suppressed_count,
426
+ ) = arrays
427
+
428
+ fills: tuple[IntrabarFill, ...] = ()
429
+ fills_report = pd.DataFrame()
430
+ if level == "audit":
431
+ audit = _run_intrabar_session_pass(
432
+ record_fills=True,
433
+ fill_capacity=int(fill_count),
434
+ tape=tape,
435
+ intent=intent,
436
+ account=account,
437
+ contract=contract,
438
+ fee_rate=fee_rate,
439
+ slippage_rate=slippage_rate,
440
+ contract_size=contract_size,
441
+ sizing_mode=sizing_mode_value,
442
+ fixed_notional=fixed_notional,
443
+ equity_fraction=equity_fraction,
444
+ risk_fraction=risk_fraction,
445
+ qty_step=qty_step,
446
+ min_qty=min_qty,
447
+ min_notional=min_notional,
448
+ tick_size=tick_size,
449
+ session_policy=policy,
450
+ session_tape=session_tape,
451
+ )
452
+ _assert_intrabar_session_audit_parity(arrays, audit)
453
+ fills = _materialize_intrabar_fills(
454
+ timestamps_ns=tape.timestamps_ns,
455
+ fill_bar=audit[15],
456
+ fill_seq=audit[16],
457
+ fill_side=audit[17],
458
+ fill_qty=audit[18],
459
+ fill_price=audit[19],
460
+ fill_fee=audit[20],
461
+ fill_reason=audit[21],
462
+ fill_count=int(fill_count),
463
+ )
464
+ fills_report = _fills_to_report(fills)
465
+
466
+ idx = pd.DatetimeIndex(pd.to_datetime(tape.timestamps_ns, utc=True))
467
+ symbol = tape.symbols[0]
468
+ metadata = {
469
+ "engine": "intrabar_session_bracket_v1",
470
+ "engine_id": "intrabar_session_bracket_v1",
471
+ "backend": "native_intrabar",
472
+ "backend_alias": "native_intrabar_session",
473
+ "kernel_version": "intrabar_session_numba_v1",
474
+ "execution_contract": contract.to_metadata(),
475
+ "data_signature": tape.signature,
476
+ "session_execution_enabled": True,
477
+ "session_policy": policy.to_metadata(),
478
+ "session_tape_signature": session_tape.signature,
479
+ "validation_certificate": tape.validation_certificate.__dict__.copy(),
480
+ "report_level": level,
481
+ "two_pass_audit": level == "audit",
482
+ "fill_count": int(fill_count),
483
+ "ambiguity_count": int(ambiguity_count),
484
+ "rejected_count": int(rejected_count),
485
+ "liquidated": bool(liquidated),
486
+ "liquidation_bar": int(liquidation_bar),
487
+ "session_reset_count": int(session_reset_count),
488
+ "session_forced_exit_count": int(session_forced_exit_count),
489
+ "entry_window_blocked_count": int(entry_window_blocked_count),
490
+ "long_quota_blocked_count": int(long_quota_blocked_count),
491
+ "short_quota_blocked_count": int(short_quota_blocked_count),
492
+ "flat_only_blocked_count": int(flat_only_blocked_count),
493
+ "stale_session_signal_count": int(stale_session_signal_count),
494
+ "reentry_suppressed_count": int(reentry_suppressed_count),
495
+ "funding_timing_certified": True,
496
+ "funding_event_alignment": "exact_bar_timestamp",
497
+ "bar_timestamp_semantics": tape.bar_timestamp_semantics,
498
+ "funding_event_price_reference": "open" if tape.bar_timestamp_semantics == "open" else "close",
499
+ "sizing_mode": sizing_mode_value.value,
500
+ "sizing": {
501
+ "fixed_notional": float(fixed_notional),
502
+ "equity_fraction": float(equity_fraction),
503
+ "risk_fraction": float(risk_fraction),
504
+ },
505
+ "quantity_constraints": {
506
+ "qty_step": float(qty_step),
507
+ "min_qty": float(min_qty),
508
+ "min_notional": float(min_notional),
509
+ "tick_size": float(tick_size),
510
+ },
511
+ }
512
+ return NativeIntrabarKernelResult(
513
+ equity=pd.Series(equity, index=idx, name="equity"),
514
+ position=pd.Series(position, index=idx, name=f"Position_{symbol}"),
515
+ average_entry=pd.Series(avg_entry, index=idx, name="average_entry"),
516
+ active_stop=pd.Series(active_stop, index=idx, name="active_stop"),
517
+ active_take_profit=pd.Series(active_tp, index=idx, name="active_take_profit"),
518
+ fees=pd.Series(fees, index=idx, name="fees"),
519
+ funding=pd.Series(funding, index=idx, name="funding"),
520
+ event_flags=pd.Series(flags, index=idx, name="event_flags"),
521
+ initial_margin=pd.Series(initial_margin, index=idx, name="initial_margin"),
522
+ maintenance_margin=pd.Series(maintenance_margin, index=idx, name="maintenance_margin"),
523
+ fills=fills,
524
+ fills_report=fills_report,
525
+ ambiguity_count=int(ambiguity_count),
526
+ rejected_count=int(rejected_count),
527
+ fill_count=int(fill_count),
528
+ liquidated=bool(liquidated),
529
+ liquidation_bar=int(liquidation_bar),
530
+ report_level=level,
531
+ metadata=metadata,
532
+ )
533
+
534
+
535
+ def run_fill_replay_kernel(
536
+ *,
537
+ tape: PreparedMarketTape,
538
+ fill_tape: FillReplayTape,
539
+ account: AccountConfig,
540
+ contract_size: float = 1.0,
541
+ ) -> NativeFillReplayResult:
542
+ """Replay explicit fills through fast accounting without certifying signal generation."""
543
+ if tape.n_symbols != 1:
544
+ raise NotImplementedError("fill replay v1 supports exactly one symbol")
545
+ _validate_fill_replay_tape(fill_tape, tape.n_bars)
546
+ equity, position, fees, flags = _engine_fill_replay_v1(
547
+ tape.opens[:, 0],
548
+ tape.closes[:, 0],
549
+ fill_tape.bar_index,
550
+ fill_tape.sequence,
551
+ fill_tape.side,
552
+ fill_tape.qty,
553
+ fill_tape.price,
554
+ fill_tape.fee,
555
+ account.initial_capital,
556
+ float(contract_size),
557
+ )
558
+ idx = pd.DatetimeIndex(pd.to_datetime(tape.timestamps_ns, utc=True))
559
+ metadata = {
560
+ "engine": "fill_replay_v1",
561
+ "engine_id": "fill_replay_v1",
562
+ "backend": "native_intrabar",
563
+ "accounting_certified": True,
564
+ "price_accounting_certified": True,
565
+ "fee_accounting_certified": True,
566
+ "funding_certified": False,
567
+ "margin_certified": False,
568
+ "liquidation_certified": False,
569
+ "execution_generation_certified": False,
570
+ "causality_certified": False,
571
+ "data_signature": tape.signature,
572
+ "fill_count": int(len(fill_tape.bar_index)),
573
+ }
574
+ return NativeFillReplayResult(
575
+ equity=pd.Series(equity, index=idx, name="equity"),
576
+ position=pd.Series(position, index=idx, name=f"Position_{tape.symbols[0]}"),
577
+ fees=pd.Series(fees, index=idx, name="fees"),
578
+ event_flags=pd.Series(flags, index=idx, name="event_flags"),
579
+ fill_count=int(len(fill_tape.bar_index)),
580
+ metadata=metadata,
581
+ )
582
+
583
+
584
+ def _run_intrabar_pass(
585
+ *,
586
+ record_fills: bool,
587
+ fill_capacity: int,
588
+ tape,
589
+ intent,
590
+ account,
591
+ contract,
592
+ fee_rate,
593
+ slippage_rate,
594
+ contract_size,
595
+ sizing_mode,
596
+ fixed_notional,
597
+ equity_fraction,
598
+ risk_fraction,
599
+ qty_step,
600
+ min_qty,
601
+ min_notional,
602
+ tick_size,
603
+ ):
604
+ stop_value = _optional_float_array(intent.stop_value, tape.n_bars)
605
+ tp_value = _optional_float_array(intent.take_profit_value, tape.n_bars)
606
+ trailing_value = _optional_float_array(intent.trailing_value, tape.n_bars)
607
+ exit_long = _optional_bool_array(intent.exit_long if intent.exit_long is not None else intent.technical_exit, tape.n_bars)
608
+ exit_short = _optional_bool_array(intent.exit_short if intent.exit_short is not None else intent.technical_exit, tape.n_bars)
609
+ fill_bar = np.zeros(max(1, int(fill_capacity)), dtype=np.int64)
610
+ fill_seq = np.zeros(max(1, int(fill_capacity)), dtype=np.int16)
611
+ fill_side = np.zeros(max(1, int(fill_capacity)), dtype=np.int8)
612
+ fill_qty = np.zeros(max(1, int(fill_capacity)), dtype=np.float64)
613
+ fill_price = np.zeros(max(1, int(fill_capacity)), dtype=np.float64)
614
+ fill_fee = np.zeros(max(1, int(fill_capacity)), dtype=np.float64)
615
+ fill_reason = np.zeros(max(1, int(fill_capacity)), dtype=np.int16)
616
+ return _engine_intrabar_bracket_v1(
617
+ tape.opens[:, 0],
618
+ tape.highs[:, 0],
619
+ tape.lows[:, 0],
620
+ tape.closes[:, 0],
621
+ np.ascontiguousarray(intent.entry_side, dtype=np.int8),
622
+ np.ascontiguousarray(intent.entry_size, dtype=np.float64),
623
+ stop_value,
624
+ tp_value,
625
+ trailing_value,
626
+ exit_long,
627
+ exit_short,
628
+ tape.funding_rates[:, 0],
629
+ tape.funding_event_mask,
630
+ _bar_timestamp_semantics_code(tape.bar_timestamp_semantics),
631
+ float(account.initial_capital),
632
+ float(account.leverage),
633
+ float(account.maintenance_ratio),
634
+ float(account.margin_buffer),
635
+ float(contract_size),
636
+ float(fee_rate),
637
+ float(slippage_rate),
638
+ _sizing_mode_code(sizing_mode),
639
+ float(fixed_notional),
640
+ float(equity_fraction),
641
+ float(risk_fraction),
642
+ float(qty_step),
643
+ float(min_qty),
644
+ float(min_notional),
645
+ float(tick_size),
646
+ _level_mode_code(intent.level_mode),
647
+ _same_bar_policy_code(contract.same_bar_policy),
648
+ _tp_policy_code(contract.take_profit_gap_policy),
649
+ bool(contract.close_on_last_bar),
650
+ bool(record_fills),
651
+ fill_bar,
652
+ fill_seq,
653
+ fill_side,
654
+ fill_qty,
655
+ fill_price,
656
+ fill_fee,
657
+ fill_reason,
658
+ )
659
+
660
+
661
+ def _run_intrabar_session_pass(
662
+ *,
663
+ record_fills: bool,
664
+ fill_capacity: int,
665
+ tape,
666
+ intent,
667
+ account,
668
+ contract,
669
+ fee_rate,
670
+ slippage_rate,
671
+ contract_size,
672
+ sizing_mode,
673
+ fixed_notional,
674
+ equity_fraction,
675
+ risk_fraction,
676
+ qty_step,
677
+ min_qty,
678
+ min_notional,
679
+ tick_size,
680
+ session_policy,
681
+ session_tape,
682
+ ):
683
+ stop_value = _optional_float_array(intent.stop_value, tape.n_bars)
684
+ tp_value = _optional_float_array(intent.take_profit_value, tape.n_bars)
685
+ trailing_value = _optional_float_array(intent.trailing_value, tape.n_bars)
686
+ exit_long = _optional_bool_array(intent.exit_long if intent.exit_long is not None else intent.technical_exit, tape.n_bars)
687
+ exit_short = _optional_bool_array(intent.exit_short if intent.exit_short is not None else intent.technical_exit, tape.n_bars)
688
+ fill_bar = np.zeros(max(1, int(fill_capacity)), dtype=np.int64)
689
+ fill_seq = np.zeros(max(1, int(fill_capacity)), dtype=np.int16)
690
+ fill_side = np.zeros(max(1, int(fill_capacity)), dtype=np.int8)
691
+ fill_qty = np.zeros(max(1, int(fill_capacity)), dtype=np.float64)
692
+ fill_price = np.zeros(max(1, int(fill_capacity)), dtype=np.float64)
693
+ fill_fee = np.zeros(max(1, int(fill_capacity)), dtype=np.float64)
694
+ fill_reason = np.zeros(max(1, int(fill_capacity)), dtype=np.int16)
695
+ return _engine_intrabar_session_bracket_v1(
696
+ tape.opens[:, 0],
697
+ tape.highs[:, 0],
698
+ tape.lows[:, 0],
699
+ tape.closes[:, 0],
700
+ np.ascontiguousarray(intent.entry_side, dtype=np.int8),
701
+ np.ascontiguousarray(intent.entry_size, dtype=np.float64),
702
+ stop_value,
703
+ tp_value,
704
+ trailing_value,
705
+ exit_long,
706
+ exit_short,
707
+ tape.funding_rates[:, 0],
708
+ tape.funding_event_mask,
709
+ _bar_timestamp_semantics_code(tape.bar_timestamp_semantics),
710
+ np.ascontiguousarray(session_tape.session_id, dtype=np.int64),
711
+ np.ascontiguousarray(session_tape.entry_allowed_at_open, dtype=np.bool_),
712
+ np.ascontiguousarray(session_tape.force_flat_at_open, dtype=np.bool_),
713
+ _session_entry_policy_code(session_policy.entry_position_policy),
714
+ _session_counter_basis_code(session_policy.counter_basis),
715
+ _session_reentry_policy_code(session_policy.protective_exit_reentry_policy),
716
+ -1 if session_policy.max_long_entries_per_session is None else int(session_policy.max_long_entries_per_session),
717
+ -1 if session_policy.max_short_entries_per_session is None else int(session_policy.max_short_entries_per_session),
718
+ bool(session_policy.cancel_pending_on_session_change),
719
+ bool(session_policy.suppress_entry_on_force_flat_bar),
720
+ float(account.initial_capital),
721
+ float(account.leverage),
722
+ float(account.maintenance_ratio),
723
+ float(account.margin_buffer),
724
+ float(contract_size),
725
+ float(fee_rate),
726
+ float(slippage_rate),
727
+ _sizing_mode_code(sizing_mode),
728
+ float(fixed_notional),
729
+ float(equity_fraction),
730
+ float(risk_fraction),
731
+ float(qty_step),
732
+ float(min_qty),
733
+ float(min_notional),
734
+ float(tick_size),
735
+ _level_mode_code(intent.level_mode),
736
+ _same_bar_policy_code(contract.same_bar_policy),
737
+ _tp_policy_code(contract.take_profit_gap_policy),
738
+ bool(contract.close_on_last_bar),
739
+ bool(record_fills),
740
+ fill_bar,
741
+ fill_seq,
742
+ fill_side,
743
+ fill_qty,
744
+ fill_price,
745
+ fill_fee,
746
+ fill_reason,
747
+ )
748
+
749
+
750
+ @njit(cache=True, nogil=True)
751
+ def _engine_intrabar_bracket_v1(
752
+ opens,
753
+ highs,
754
+ lows,
755
+ closes,
756
+ entry_side,
757
+ entry_size,
758
+ stop_value,
759
+ tp_value,
760
+ trailing_value,
761
+ exit_long,
762
+ exit_short,
763
+ funding_rates,
764
+ funding_mask,
765
+ bar_timestamp_semantics,
766
+ initial_capital,
767
+ leverage,
768
+ maintenance_ratio,
769
+ margin_buffer,
770
+ contract_size,
771
+ fee_rate,
772
+ slippage_rate,
773
+ sizing_mode,
774
+ fixed_notional,
775
+ equity_fraction,
776
+ risk_fraction,
777
+ qty_step,
778
+ min_qty,
779
+ min_notional,
780
+ tick_size,
781
+ level_mode,
782
+ same_bar_policy,
783
+ tp_gap_policy,
784
+ close_on_last_bar,
785
+ record_fills,
786
+ fill_bar,
787
+ fill_seq,
788
+ fill_side,
789
+ fill_qty,
790
+ fill_price,
791
+ fill_fee,
792
+ fill_reason,
793
+ ):
794
+ n = closes.shape[0]
795
+ equity_arr = np.zeros(n, dtype=np.float64)
796
+ pos_arr = np.zeros(n, dtype=np.float64)
797
+ avg_arr = np.zeros(n, dtype=np.float64)
798
+ stop_arr = np.zeros(n, dtype=np.float64)
799
+ tp_arr = np.zeros(n, dtype=np.float64)
800
+ fee_arr = np.zeros(n, dtype=np.float64)
801
+ funding_arr = np.zeros(n, dtype=np.float64)
802
+ flags_arr = np.zeros(n, dtype=np.uint16)
803
+ init_margin = np.zeros(n, dtype=np.float64)
804
+ maint_margin = np.zeros(n, dtype=np.float64)
805
+
806
+ equity = initial_capital
807
+ position = 0.0
808
+ avg_entry = 0.0
809
+ active_stop = np.nan
810
+ active_tp = np.nan
811
+ fill_count = 0
812
+ ambiguity_count = 0
813
+ rejected_count = 0
814
+ liquidated = False
815
+ liquidation_bar = -1
816
+
817
+ equity_arr[0] = equity
818
+ for t in range(1, n):
819
+ if liquidated:
820
+ equity_arr[t] = 0.0
821
+ continue
822
+
823
+ seq = 0
824
+ open_ref = opens[t]
825
+ close_ref = closes[t]
826
+ last_ref = open_ref
827
+
828
+ if position != 0.0:
829
+ equity += position * (open_ref - closes[t - 1]) * contract_size
830
+
831
+ if position != 0.0 and _maintenance_breached_numba(equity, position, open_ref, contract_size, maintenance_ratio):
832
+ side = -1 if position > 0.0 else 1
833
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
834
+ qty = abs(position)
835
+ fee = qty * price * contract_size * fee_rate
836
+ equity += position * (price - open_ref) * contract_size - fee
837
+ fee_arr[t] += fee
838
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, FILL_LIQUIDATION, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
839
+ flags_arr[t] |= FLAG_EXIT_FILLED | FLAG_LIQUIDATION
840
+ liquidated = True
841
+ liquidation_bar = t
842
+ equity = 0.0
843
+ equity_arr[t] = 0.0
844
+ continue
845
+
846
+ if bar_timestamp_semantics == BAR_TS_OPEN and position != 0.0 and funding_mask[t]:
847
+ funding_cost = position * open_ref * contract_size * funding_rates[t]
848
+ equity -= funding_cost
849
+ funding_arr[t] = funding_cost
850
+ flags_arr[t] |= FLAG_FUNDING
851
+
852
+ pending_side = entry_side[t - 1]
853
+ pending_size = entry_size[t - 1]
854
+ pending_exit = (position > 0.0 and exit_long[t - 1]) or (position < 0.0 and exit_short[t - 1])
855
+ exit_same_side_conflict = pending_exit and pending_side != 0 and position != 0.0 and _sign_numba(position) == pending_side
856
+
857
+ if position != 0.0 and (pending_exit or (pending_side != 0 and _sign_numba(position) != pending_side)):
858
+ reason = FILL_REVERSAL_EXIT if pending_side != 0 and _sign_numba(position) != pending_side else FILL_TECHNICAL_EXIT
859
+ side = -1 if position > 0.0 else 1
860
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
861
+ qty = abs(position)
862
+ fee = qty * price * contract_size * fee_rate
863
+ equity += position * (price - open_ref) * contract_size - fee
864
+ fee_arr[t] += fee
865
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
866
+ seq += 1
867
+ flags_arr[t] |= FLAG_EXIT_FILLED
868
+ if reason == FILL_TECHNICAL_EXIT:
869
+ flags_arr[t] |= FLAG_TECH_EXIT
870
+ else:
871
+ flags_arr[t] |= FLAG_REVERSAL
872
+ position = 0.0
873
+ avg_entry = 0.0
874
+ active_stop = np.nan
875
+ active_tp = np.nan
876
+
877
+ if pending_side != 0 and pending_size > 0.0 and position == 0.0:
878
+ side = 1 if pending_side > 0 else -1
879
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
880
+ if exit_same_side_conflict:
881
+ qty = 0.0
882
+ else:
883
+ qty = _compile_entry_quantity_numba(
884
+ pending_size,
885
+ price,
886
+ equity,
887
+ contract_size,
888
+ sizing_mode,
889
+ fixed_notional,
890
+ equity_fraction,
891
+ risk_fraction,
892
+ stop_value[t - 1],
893
+ level_mode,
894
+ side,
895
+ tick_size,
896
+ )
897
+ qty = abs(_quantize_signed_quantity_numba(qty, price, contract_size, qty_step, min_qty, min_notional))
898
+ if exit_same_side_conflict:
899
+ flags_arr[t] |= FLAG_ENTRY_SUPPRESSED
900
+ equity_arr[t] = equity
901
+ pos_arr[t] = position
902
+ avg_arr[t] = avg_entry
903
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
904
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
905
+ continue
906
+ if qty <= 0.0:
907
+ flags_arr[t] |= FLAG_REJECTED
908
+ rejected_count += 1
909
+ equity_arr[t] = equity
910
+ pos_arr[t] = position
911
+ avg_arr[t] = avg_entry
912
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
913
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
914
+ continue
915
+ if not _has_initial_margin_numba(equity, qty, price, contract_size, leverage, margin_buffer):
916
+ flags_arr[t] |= FLAG_REJECTED
917
+ rejected_count += 1
918
+ equity_arr[t] = equity
919
+ pos_arr[t] = position
920
+ avg_arr[t] = avg_entry
921
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
922
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
923
+ continue
924
+ fee = qty * price * contract_size * fee_rate
925
+ equity -= fee
926
+ fee_arr[t] += fee
927
+ position = qty * side
928
+ avg_entry = price
929
+ last_ref = price
930
+ active_stop, active_tp = _initial_bracket_numba(stop_value[t - 1], tp_value[t - 1], trailing_value[t - 1], side, price, level_mode, tick_size)
931
+ reason = FILL_REVERSAL_ENTRY if (flags_arr[t] & FLAG_REVERSAL) != 0 else FILL_ENTRY
932
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
933
+ seq += 1
934
+ flags_arr[t] |= FLAG_ENTRY_FILLED
935
+
936
+ if position != 0.0:
937
+ exit_side, exit_price, exit_reason, ambiguous = _resolve_intrabar_exit_numba(
938
+ 1 if position > 0.0 else -1,
939
+ open_ref,
940
+ highs[t],
941
+ lows[t],
942
+ active_stop,
943
+ active_tp,
944
+ same_bar_policy,
945
+ tp_gap_policy,
946
+ slippage_rate,
947
+ tick_size,
948
+ )
949
+ if exit_reason != 0:
950
+ if ambiguous:
951
+ flags_arr[t] |= FLAG_AMBIGUOUS
952
+ ambiguity_count += 1
953
+ qty = abs(position)
954
+ fee = qty * exit_price * contract_size * fee_rate
955
+ equity += position * (exit_price - last_ref) * contract_size - fee
956
+ fee_arr[t] += fee
957
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, exit_side, qty, exit_price, fee, exit_reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
958
+ seq += 1
959
+ flags_arr[t] |= FLAG_EXIT_FILLED
960
+ if exit_reason == FILL_STOP_LOSS:
961
+ flags_arr[t] |= FLAG_STOP_FILLED
962
+ else:
963
+ flags_arr[t] |= FLAG_TP_FILLED
964
+ position = 0.0
965
+ avg_entry = 0.0
966
+ active_stop = np.nan
967
+ active_tp = np.nan
968
+
969
+ if position != 0.0:
970
+ if _maintenance_breached_worst_numba(equity, position, last_ref, highs[t], lows[t], contract_size, maintenance_ratio):
971
+ side = -1 if position > 0.0 else 1
972
+ worst = lows[t] if position > 0.0 else highs[t]
973
+ price = _market_price_numba(worst, side, slippage_rate, tick_size)
974
+ qty = abs(position)
975
+ fee = qty * price * contract_size * fee_rate
976
+ equity += position * (price - last_ref) * contract_size - fee
977
+ fee_arr[t] += fee
978
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, FILL_LIQUIDATION, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
979
+ flags_arr[t] |= FLAG_EXIT_FILLED | FLAG_LIQUIDATION
980
+ liquidated = True
981
+ liquidation_bar = t
982
+ equity = 0.0
983
+ position = 0.0
984
+ avg_entry = 0.0
985
+ active_stop = np.nan
986
+ active_tp = np.nan
987
+ else:
988
+ equity += position * (close_ref - last_ref) * contract_size
989
+ active_stop = _update_trailing_numba(trailing_value[t], position, close_ref, active_stop, level_mode, tick_size)
990
+
991
+ if liquidated:
992
+ equity_arr[t] = 0.0
993
+ pos_arr[t] = 0.0
994
+ avg_arr[t] = 0.0
995
+ stop_arr[t] = 0.0
996
+ tp_arr[t] = 0.0
997
+ continue
998
+
999
+ if bar_timestamp_semantics == BAR_TS_CLOSE and position != 0.0 and funding_mask[t]:
1000
+ funding_cost = position * close_ref * contract_size * funding_rates[t]
1001
+ equity -= funding_cost
1002
+ funding_arr[t] = funding_cost
1003
+ flags_arr[t] |= FLAG_FUNDING
1004
+
1005
+ equity_arr[t] = equity
1006
+ pos_arr[t] = position
1007
+ avg_arr[t] = avg_entry
1008
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
1009
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
1010
+ init_margin[t] = abs(position) * close_ref * contract_size / leverage
1011
+ maint_margin[t] = abs(position) * close_ref * contract_size * maintenance_ratio
1012
+
1013
+ if close_on_last_bar and position != 0.0 and not liquidated:
1014
+ t = n - 1
1015
+ side = -1 if position > 0.0 else 1
1016
+ price = _market_price_numba(closes[t], side, slippage_rate, tick_size)
1017
+ qty = abs(position)
1018
+ fee = qty * price * contract_size * fee_rate
1019
+ equity += position * (price - closes[t]) * contract_size - fee
1020
+ fee_arr[t] += fee
1021
+ fill_count = _record_fill_numba(record_fills, fill_count, t, 99, side, qty, price, fee, FILL_FINAL_CLOSE, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1022
+ position = 0.0
1023
+ equity_arr[t] = equity
1024
+ pos_arr[t] = 0.0
1025
+ avg_arr[t] = 0.0
1026
+ stop_arr[t] = 0.0
1027
+ tp_arr[t] = 0.0
1028
+ init_margin[t] = 0.0
1029
+ maint_margin[t] = 0.0
1030
+
1031
+ return (
1032
+ equity_arr,
1033
+ pos_arr,
1034
+ avg_arr,
1035
+ stop_arr,
1036
+ tp_arr,
1037
+ fee_arr,
1038
+ funding_arr,
1039
+ flags_arr,
1040
+ init_margin,
1041
+ maint_margin,
1042
+ fill_count,
1043
+ ambiguity_count,
1044
+ rejected_count,
1045
+ liquidated,
1046
+ liquidation_bar,
1047
+ fill_bar,
1048
+ fill_seq,
1049
+ fill_side,
1050
+ fill_qty,
1051
+ fill_price,
1052
+ fill_fee,
1053
+ fill_reason,
1054
+ )
1055
+
1056
+
1057
+ @njit(cache=True, nogil=True)
1058
+ def _engine_intrabar_session_bracket_v1(
1059
+ opens,
1060
+ highs,
1061
+ lows,
1062
+ closes,
1063
+ entry_side,
1064
+ entry_size,
1065
+ stop_value,
1066
+ tp_value,
1067
+ trailing_value,
1068
+ exit_long,
1069
+ exit_short,
1070
+ funding_rates,
1071
+ funding_mask,
1072
+ bar_timestamp_semantics,
1073
+ session_id,
1074
+ entry_allowed_at_open,
1075
+ force_flat_at_open,
1076
+ entry_position_policy,
1077
+ counter_basis,
1078
+ protective_reentry_policy,
1079
+ max_long_entries_per_session,
1080
+ max_short_entries_per_session,
1081
+ cancel_pending_on_session_change,
1082
+ suppress_entry_on_force_flat_bar,
1083
+ initial_capital,
1084
+ leverage,
1085
+ maintenance_ratio,
1086
+ margin_buffer,
1087
+ contract_size,
1088
+ fee_rate,
1089
+ slippage_rate,
1090
+ sizing_mode,
1091
+ fixed_notional,
1092
+ equity_fraction,
1093
+ risk_fraction,
1094
+ qty_step,
1095
+ min_qty,
1096
+ min_notional,
1097
+ tick_size,
1098
+ level_mode,
1099
+ same_bar_policy,
1100
+ tp_gap_policy,
1101
+ close_on_last_bar,
1102
+ record_fills,
1103
+ fill_bar,
1104
+ fill_seq,
1105
+ fill_side,
1106
+ fill_qty,
1107
+ fill_price,
1108
+ fill_fee,
1109
+ fill_reason,
1110
+ ):
1111
+ n = closes.shape[0]
1112
+ equity_arr = np.zeros(n, dtype=np.float64)
1113
+ pos_arr = np.zeros(n, dtype=np.float64)
1114
+ avg_arr = np.zeros(n, dtype=np.float64)
1115
+ stop_arr = np.zeros(n, dtype=np.float64)
1116
+ tp_arr = np.zeros(n, dtype=np.float64)
1117
+ fee_arr = np.zeros(n, dtype=np.float64)
1118
+ funding_arr = np.zeros(n, dtype=np.float64)
1119
+ flags_arr = np.zeros(n, dtype=np.uint32)
1120
+ init_margin = np.zeros(n, dtype=np.float64)
1121
+ maint_margin = np.zeros(n, dtype=np.float64)
1122
+
1123
+ equity = initial_capital
1124
+ position = 0.0
1125
+ avg_entry = 0.0
1126
+ active_stop = np.nan
1127
+ active_tp = np.nan
1128
+ fill_count = 0
1129
+ ambiguity_count = 0
1130
+ rejected_count = 0
1131
+ liquidated = False
1132
+ liquidation_bar = -1
1133
+
1134
+ current_session_id = session_id[0] if n > 0 else 0
1135
+ long_entry_count = 0
1136
+ short_entry_count = 0
1137
+ protective_exit_on_previous_bar = False
1138
+ session_reset_count = 0
1139
+ session_forced_exit_count = 0
1140
+ entry_window_blocked_count = 0
1141
+ long_quota_blocked_count = 0
1142
+ short_quota_blocked_count = 0
1143
+ flat_only_blocked_count = 0
1144
+ stale_session_signal_count = 0
1145
+ reentry_suppressed_count = 0
1146
+
1147
+ equity_arr[0] = equity
1148
+ for t in range(1, n):
1149
+ if liquidated:
1150
+ equity_arr[t] = 0.0
1151
+ continue
1152
+
1153
+ seq = 0
1154
+ open_ref = opens[t]
1155
+ close_ref = closes[t]
1156
+ last_ref = open_ref
1157
+
1158
+ if position != 0.0:
1159
+ equity += position * (open_ref - closes[t - 1]) * contract_size
1160
+
1161
+ reentry_block_from_previous_bar = False
1162
+ if session_id[t] != current_session_id:
1163
+ current_session_id = session_id[t]
1164
+ long_entry_count = 0
1165
+ short_entry_count = 0
1166
+ protective_exit_on_previous_bar = False
1167
+ flags_arr[t] |= FLAG_SESSION_RESET
1168
+ session_reset_count += 1
1169
+ reentry_block_from_previous_bar = protective_exit_on_previous_bar
1170
+ protective_exit_on_previous_bar = False
1171
+
1172
+ if position != 0.0 and _maintenance_breached_numba(equity, position, open_ref, contract_size, maintenance_ratio):
1173
+ side = -1 if position > 0.0 else 1
1174
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
1175
+ qty = abs(position)
1176
+ fee = qty * price * contract_size * fee_rate
1177
+ equity += position * (price - open_ref) * contract_size - fee
1178
+ fee_arr[t] += fee
1179
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, FILL_LIQUIDATION, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1180
+ flags_arr[t] |= FLAG_EXIT_FILLED | FLAG_LIQUIDATION
1181
+ liquidated = True
1182
+ liquidation_bar = t
1183
+ equity = 0.0
1184
+ equity_arr[t] = 0.0
1185
+ continue
1186
+
1187
+ if bar_timestamp_semantics == BAR_TS_OPEN and position != 0.0 and funding_mask[t]:
1188
+ funding_cost = position * open_ref * contract_size * funding_rates[t]
1189
+ equity -= funding_cost
1190
+ funding_arr[t] = funding_cost
1191
+ flags_arr[t] |= FLAG_FUNDING
1192
+
1193
+ force_flat_bar = force_flat_at_open[t]
1194
+ if force_flat_bar and position != 0.0:
1195
+ side = -1 if position > 0.0 else 1
1196
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
1197
+ qty = abs(position)
1198
+ fee = qty * price * contract_size * fee_rate
1199
+ equity += position * (price - open_ref) * contract_size - fee
1200
+ fee_arr[t] += fee
1201
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, FILL_SESSION_FORCED_EXIT, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1202
+ seq += 1
1203
+ flags_arr[t] |= FLAG_EXIT_FILLED | FLAG_SESSION_FORCED_EXIT
1204
+ session_forced_exit_count += 1
1205
+ position = 0.0
1206
+ avg_entry = 0.0
1207
+ active_stop = np.nan
1208
+ active_tp = np.nan
1209
+
1210
+ pending_side = entry_side[t - 1]
1211
+ pending_size = entry_size[t - 1]
1212
+ pending_exit = (position > 0.0 and exit_long[t - 1]) or (position < 0.0 and exit_short[t - 1])
1213
+
1214
+ if cancel_pending_on_session_change and pending_side != 0 and session_id[t - 1] != session_id[t]:
1215
+ pending_side = 0
1216
+ pending_size = 0.0
1217
+ flags_arr[t] |= FLAG_STALE_SESSION_SIGNAL | FLAG_ENTRY_SUPPRESSED
1218
+ stale_session_signal_count += 1
1219
+
1220
+ if pending_side != 0 and position != 0.0 and entry_position_policy == SESSION_ENTRY_FLAT_ONLY:
1221
+ pending_side = 0
1222
+ pending_size = 0.0
1223
+ flags_arr[t] |= FLAG_FLAT_ONLY_BLOCKED | FLAG_ENTRY_SUPPRESSED
1224
+ flat_only_blocked_count += 1
1225
+
1226
+ exit_same_side_conflict = pending_exit and pending_side != 0 and position != 0.0 and _sign_numba(position) == pending_side
1227
+ reversal_allowed = entry_position_policy != SESSION_ENTRY_FLAT_ONLY
1228
+
1229
+ if position != 0.0 and (pending_exit or (reversal_allowed and pending_side != 0 and _sign_numba(position) != pending_side)):
1230
+ reason = FILL_REVERSAL_EXIT if pending_side != 0 and _sign_numba(position) != pending_side else FILL_TECHNICAL_EXIT
1231
+ side = -1 if position > 0.0 else 1
1232
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
1233
+ qty = abs(position)
1234
+ fee = qty * price * contract_size * fee_rate
1235
+ equity += position * (price - open_ref) * contract_size - fee
1236
+ fee_arr[t] += fee
1237
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1238
+ seq += 1
1239
+ flags_arr[t] |= FLAG_EXIT_FILLED
1240
+ if reason == FILL_TECHNICAL_EXIT:
1241
+ flags_arr[t] |= FLAG_TECH_EXIT
1242
+ else:
1243
+ flags_arr[t] |= FLAG_REVERSAL
1244
+ position = 0.0
1245
+ avg_entry = 0.0
1246
+ active_stop = np.nan
1247
+ active_tp = np.nan
1248
+
1249
+ if pending_side != 0 and pending_size > 0.0 and position == 0.0:
1250
+ side = 1 if pending_side > 0 else -1
1251
+ price = _market_price_numba(open_ref, side, slippage_rate, tick_size)
1252
+ entry_blocked = False
1253
+ if force_flat_bar and suppress_entry_on_force_flat_bar:
1254
+ entry_blocked = True
1255
+ flags_arr[t] |= FLAG_SESSION_FORCED_EXIT | FLAG_ENTRY_SUPPRESSED
1256
+ elif not entry_allowed_at_open[t]:
1257
+ entry_blocked = True
1258
+ entry_window_blocked_count += 1
1259
+ flags_arr[t] |= FLAG_ENTRY_WINDOW_BLOCKED | FLAG_ENTRY_SUPPRESSED
1260
+ elif protective_reentry_policy == SESSION_REENTRY_SUPPRESS_SIGNAL_BAR and reentry_block_from_previous_bar:
1261
+ entry_blocked = True
1262
+ reentry_suppressed_count += 1
1263
+ flags_arr[t] |= FLAG_PROTECTIVE_REENTRY_BLOCKED | FLAG_ENTRY_SUPPRESSED
1264
+ elif side > 0 and max_long_entries_per_session >= 0 and long_entry_count >= max_long_entries_per_session:
1265
+ entry_blocked = True
1266
+ long_quota_blocked_count += 1
1267
+ flags_arr[t] |= FLAG_ENTRY_QUOTA_BLOCKED | FLAG_ENTRY_SUPPRESSED
1268
+ elif side < 0 and max_short_entries_per_session >= 0 and short_entry_count >= max_short_entries_per_session:
1269
+ entry_blocked = True
1270
+ short_quota_blocked_count += 1
1271
+ flags_arr[t] |= FLAG_ENTRY_QUOTA_BLOCKED | FLAG_ENTRY_SUPPRESSED
1272
+
1273
+ if exit_same_side_conflict or entry_blocked:
1274
+ flags_arr[t] |= FLAG_ENTRY_SUPPRESSED
1275
+ equity_arr[t] = equity
1276
+ pos_arr[t] = position
1277
+ avg_arr[t] = avg_entry
1278
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
1279
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
1280
+ init_margin[t] = abs(position) * close_ref * contract_size / leverage
1281
+ maint_margin[t] = abs(position) * close_ref * contract_size * maintenance_ratio
1282
+ continue
1283
+
1284
+ qty = _compile_entry_quantity_numba(
1285
+ pending_size,
1286
+ price,
1287
+ equity,
1288
+ contract_size,
1289
+ sizing_mode,
1290
+ fixed_notional,
1291
+ equity_fraction,
1292
+ risk_fraction,
1293
+ stop_value[t - 1],
1294
+ level_mode,
1295
+ side,
1296
+ tick_size,
1297
+ )
1298
+ qty = abs(_quantize_signed_quantity_numba(qty, price, contract_size, qty_step, min_qty, min_notional))
1299
+ if qty <= 0.0:
1300
+ flags_arr[t] |= FLAG_REJECTED
1301
+ rejected_count += 1
1302
+ equity_arr[t] = equity
1303
+ pos_arr[t] = position
1304
+ avg_arr[t] = avg_entry
1305
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
1306
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
1307
+ continue
1308
+ if not _has_initial_margin_numba(equity, qty, price, contract_size, leverage, margin_buffer):
1309
+ flags_arr[t] |= FLAG_REJECTED
1310
+ rejected_count += 1
1311
+ equity_arr[t] = equity
1312
+ pos_arr[t] = position
1313
+ avg_arr[t] = avg_entry
1314
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
1315
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
1316
+ continue
1317
+ fee = qty * price * contract_size * fee_rate
1318
+ equity -= fee
1319
+ fee_arr[t] += fee
1320
+ position = qty * side
1321
+ avg_entry = price
1322
+ last_ref = price
1323
+ active_stop, active_tp = _initial_bracket_numba(stop_value[t - 1], tp_value[t - 1], trailing_value[t - 1], side, price, level_mode, tick_size)
1324
+ reason = FILL_REVERSAL_ENTRY if (flags_arr[t] & FLAG_REVERSAL) != 0 else FILL_ENTRY
1325
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1326
+ seq += 1
1327
+ flags_arr[t] |= FLAG_ENTRY_FILLED
1328
+ if side > 0:
1329
+ long_entry_count += 1
1330
+ else:
1331
+ short_entry_count += 1
1332
+
1333
+ if position != 0.0:
1334
+ exit_side, exit_price, exit_reason, ambiguous = _resolve_intrabar_exit_numba(
1335
+ 1 if position > 0.0 else -1,
1336
+ open_ref,
1337
+ highs[t],
1338
+ lows[t],
1339
+ active_stop,
1340
+ active_tp,
1341
+ same_bar_policy,
1342
+ tp_gap_policy,
1343
+ slippage_rate,
1344
+ tick_size,
1345
+ )
1346
+ if exit_reason != 0:
1347
+ if ambiguous:
1348
+ flags_arr[t] |= FLAG_AMBIGUOUS
1349
+ ambiguity_count += 1
1350
+ qty = abs(position)
1351
+ fee = qty * exit_price * contract_size * fee_rate
1352
+ equity += position * (exit_price - last_ref) * contract_size - fee
1353
+ fee_arr[t] += fee
1354
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, exit_side, qty, exit_price, fee, exit_reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1355
+ seq += 1
1356
+ flags_arr[t] |= FLAG_EXIT_FILLED
1357
+ if exit_reason == FILL_STOP_LOSS:
1358
+ flags_arr[t] |= FLAG_STOP_FILLED
1359
+ protective_exit_on_previous_bar = True
1360
+ else:
1361
+ flags_arr[t] |= FLAG_TP_FILLED
1362
+ protective_exit_on_previous_bar = True
1363
+ position = 0.0
1364
+ avg_entry = 0.0
1365
+ active_stop = np.nan
1366
+ active_tp = np.nan
1367
+
1368
+ if position != 0.0:
1369
+ if _maintenance_breached_worst_numba(equity, position, last_ref, highs[t], lows[t], contract_size, maintenance_ratio):
1370
+ side = -1 if position > 0.0 else 1
1371
+ worst = lows[t] if position > 0.0 else highs[t]
1372
+ price = _market_price_numba(worst, side, slippage_rate, tick_size)
1373
+ qty = abs(position)
1374
+ fee = qty * price * contract_size * fee_rate
1375
+ equity += position * (price - last_ref) * contract_size - fee
1376
+ fee_arr[t] += fee
1377
+ fill_count = _record_fill_numba(record_fills, fill_count, t, seq, side, qty, price, fee, FILL_LIQUIDATION, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1378
+ flags_arr[t] |= FLAG_EXIT_FILLED | FLAG_LIQUIDATION
1379
+ liquidated = True
1380
+ liquidation_bar = t
1381
+ equity = 0.0
1382
+ position = 0.0
1383
+ avg_entry = 0.0
1384
+ active_stop = np.nan
1385
+ active_tp = np.nan
1386
+ else:
1387
+ equity += position * (close_ref - last_ref) * contract_size
1388
+ active_stop = _update_trailing_numba(trailing_value[t], position, close_ref, active_stop, level_mode, tick_size)
1389
+
1390
+ if liquidated:
1391
+ equity_arr[t] = 0.0
1392
+ pos_arr[t] = 0.0
1393
+ avg_arr[t] = 0.0
1394
+ stop_arr[t] = 0.0
1395
+ tp_arr[t] = 0.0
1396
+ continue
1397
+
1398
+ if bar_timestamp_semantics == BAR_TS_CLOSE and position != 0.0 and funding_mask[t]:
1399
+ funding_cost = position * close_ref * contract_size * funding_rates[t]
1400
+ equity -= funding_cost
1401
+ funding_arr[t] = funding_cost
1402
+ flags_arr[t] |= FLAG_FUNDING
1403
+
1404
+ equity_arr[t] = equity
1405
+ pos_arr[t] = position
1406
+ avg_arr[t] = avg_entry
1407
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
1408
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
1409
+ init_margin[t] = abs(position) * close_ref * contract_size / leverage
1410
+ maint_margin[t] = abs(position) * close_ref * contract_size * maintenance_ratio
1411
+
1412
+ if close_on_last_bar and position != 0.0 and not liquidated:
1413
+ t = n - 1
1414
+ side = -1 if position > 0.0 else 1
1415
+ price = _market_price_numba(closes[t], side, slippage_rate, tick_size)
1416
+ qty = abs(position)
1417
+ fee = qty * price * contract_size * fee_rate
1418
+ equity += position * (price - closes[t]) * contract_size - fee
1419
+ fee_arr[t] += fee
1420
+ fill_count = _record_fill_numba(record_fills, fill_count, t, 99, side, qty, price, fee, FILL_FINAL_CLOSE, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason)
1421
+ position = 0.0
1422
+ equity_arr[t] = equity
1423
+ pos_arr[t] = 0.0
1424
+ avg_arr[t] = 0.0
1425
+ stop_arr[t] = 0.0
1426
+ tp_arr[t] = 0.0
1427
+ init_margin[t] = 0.0
1428
+ maint_margin[t] = 0.0
1429
+
1430
+ return (
1431
+ equity_arr,
1432
+ pos_arr,
1433
+ avg_arr,
1434
+ stop_arr,
1435
+ tp_arr,
1436
+ fee_arr,
1437
+ funding_arr,
1438
+ flags_arr,
1439
+ init_margin,
1440
+ maint_margin,
1441
+ fill_count,
1442
+ ambiguity_count,
1443
+ rejected_count,
1444
+ liquidated,
1445
+ liquidation_bar,
1446
+ fill_bar,
1447
+ fill_seq,
1448
+ fill_side,
1449
+ fill_qty,
1450
+ fill_price,
1451
+ fill_fee,
1452
+ fill_reason,
1453
+ session_reset_count,
1454
+ session_forced_exit_count,
1455
+ entry_window_blocked_count,
1456
+ long_quota_blocked_count,
1457
+ short_quota_blocked_count,
1458
+ flat_only_blocked_count,
1459
+ stale_session_signal_count,
1460
+ reentry_suppressed_count,
1461
+ )
1462
+
1463
+
1464
+ @njit(cache=True, nogil=True)
1465
+ def _engine_fill_replay_v1(opens, closes, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, initial_capital, contract_size):
1466
+ n = closes.shape[0]
1467
+ equity_arr = np.zeros(n, dtype=np.float64)
1468
+ pos_arr = np.zeros(n, dtype=np.float64)
1469
+ fee_arr = np.zeros(n, dtype=np.float64)
1470
+ flags_arr = np.zeros(n, dtype=np.uint16)
1471
+ equity = initial_capital
1472
+ position = 0.0
1473
+ ptr = 0
1474
+ n_fills = fill_bar.shape[0]
1475
+ prev_close = opens[0]
1476
+ for t in range(n):
1477
+ current_ref = opens[t]
1478
+ if t > 0 and position != 0.0:
1479
+ equity += position * (opens[t] - prev_close) * contract_size
1480
+ while ptr < n_fills and fill_bar[ptr] == t:
1481
+ price = fill_price[ptr]
1482
+ side = fill_side[ptr]
1483
+ qty = fill_qty[ptr]
1484
+ fee = fill_fee[ptr]
1485
+ if position != 0.0:
1486
+ equity += position * (price - current_ref) * contract_size
1487
+ equity -= fee
1488
+ fee_arr[t] += fee
1489
+ position += side * qty
1490
+ current_ref = price
1491
+ flags_arr[t] |= FLAG_ENTRY_FILLED if side > 0 else FLAG_EXIT_FILLED
1492
+ ptr += 1
1493
+ if position != 0.0:
1494
+ equity += position * (closes[t] - current_ref) * contract_size
1495
+ equity_arr[t] = equity
1496
+ pos_arr[t] = position
1497
+ prev_close = closes[t]
1498
+ return equity_arr, pos_arr, fee_arr, flags_arr
1499
+
1500
+
1501
+ @njit(cache=True, nogil=True)
1502
+ def _market_price_numba(price, side, slippage_rate, tick_size):
1503
+ raw = price * (1.0 + slippage_rate if side > 0 else 1.0 - slippage_rate)
1504
+ return _quantize_price_numba(raw, side, tick_size)
1505
+
1506
+
1507
+ @njit(cache=True, nogil=True)
1508
+ def _sign_numba(value):
1509
+ if value > 0.0:
1510
+ return 1
1511
+ if value < 0.0:
1512
+ return -1
1513
+ return 0
1514
+
1515
+
1516
+ @njit(cache=True, nogil=True)
1517
+ def _has_initial_margin_numba(equity, qty, price, contract_size, leverage, margin_buffer):
1518
+ required = abs(qty) * price * contract_size / leverage
1519
+ return equity >= required * (1.0 + margin_buffer)
1520
+
1521
+
1522
+ @njit(cache=True, nogil=True)
1523
+ def _maintenance_breached_numba(equity, position, price, contract_size, maintenance_ratio):
1524
+ maintenance = abs(position) * price * contract_size * maintenance_ratio
1525
+ return maintenance > 0.0 and equity <= maintenance
1526
+
1527
+
1528
+ @njit(cache=True, nogil=True)
1529
+ def _maintenance_breached_worst_numba(equity, position, reference_price, high, low, contract_size, maintenance_ratio):
1530
+ worst = low if position > 0.0 else high
1531
+ worst_equity = equity + position * (worst - reference_price) * contract_size
1532
+ maintenance = abs(position) * worst * contract_size * maintenance_ratio
1533
+ return maintenance > 0.0 and worst_equity <= maintenance
1534
+
1535
+
1536
+ @njit(cache=True, nogil=True)
1537
+ def _initial_bracket_numba(stop_value, tp_value, trailing_value, side, fill_price, level_mode, tick_size):
1538
+ stop = np.nan
1539
+ tp = np.nan
1540
+ if np.isfinite(stop_value) and stop_value > 0.0:
1541
+ stop = _level_price_numba(fill_price, side, stop_value, level_mode, True, tick_size)
1542
+ if np.isfinite(tp_value) and tp_value > 0.0:
1543
+ tp = _level_price_numba(fill_price, side, tp_value, level_mode, False, tick_size)
1544
+ if np.isfinite(trailing_value) and trailing_value > 0.0:
1545
+ trailing_stop = _level_price_numba(fill_price, side, trailing_value, level_mode, True, tick_size)
1546
+ if not np.isfinite(stop):
1547
+ stop = trailing_stop
1548
+ elif side > 0:
1549
+ stop = max(stop, trailing_stop)
1550
+ else:
1551
+ stop = min(stop, trailing_stop)
1552
+ return stop, tp
1553
+
1554
+
1555
+ @njit(cache=True, nogil=True)
1556
+ def _level_price_numba(price, side, value, level_mode, is_stop, tick_size):
1557
+ direction = -1.0 if (side > 0 and is_stop) or (side < 0 and not is_stop) else 1.0
1558
+ if level_mode == LEVEL_ABSOLUTE_PRICE:
1559
+ return _quantize_price_numba(value, -side, tick_size)
1560
+ if level_mode == LEVEL_PRICE_DISTANCE:
1561
+ return _quantize_price_numba(price + direction * value, -side, tick_size)
1562
+ return _quantize_price_numba(price * (1.0 + direction * value), -side, tick_size)
1563
+
1564
+
1565
+ @njit(cache=True, nogil=True)
1566
+ def _resolve_intrabar_exit_numba(side, open_price, high, low, stop_price, tp_price, same_bar_policy, tp_gap_policy, slippage_rate, tick_size):
1567
+ has_stop = np.isfinite(stop_price) and stop_price > 0.0
1568
+ has_tp = np.isfinite(tp_price) and tp_price > 0.0
1569
+ if side > 0:
1570
+ stop_hit = has_stop and low <= stop_price
1571
+ tp_hit = has_tp and high >= tp_price
1572
+ stop_gap = has_stop and open_price <= stop_price
1573
+ tp_gap = has_tp and open_price >= tp_price
1574
+ exit_side = -1
1575
+ else:
1576
+ stop_hit = has_stop and high >= stop_price
1577
+ tp_hit = has_tp and low <= tp_price
1578
+ stop_gap = has_stop and open_price >= stop_price
1579
+ tp_gap = has_tp and open_price <= tp_price
1580
+ exit_side = 1
1581
+ if not stop_hit and not tp_hit:
1582
+ return 0, 0.0, 0, False
1583
+ ambiguous = stop_hit and tp_hit
1584
+ if ambiguous and same_bar_policy == SAME_BAR_REJECT_AMBIGUOUS:
1585
+ return 0, 0.0, -1, True
1586
+ stop_first = (
1587
+ same_bar_policy == SAME_BAR_CONSERVATIVE
1588
+ or same_bar_policy == SAME_BAR_STOP_FIRST
1589
+ or (side > 0 and same_bar_policy == SAME_BAR_OLHC_PATH)
1590
+ or (side < 0 and same_bar_policy == SAME_BAR_OHLC_PATH)
1591
+ )
1592
+ if stop_hit and ((not tp_hit) or stop_first):
1593
+ price = open_price if stop_gap else stop_price
1594
+ return exit_side, _market_price_numba(price, exit_side, slippage_rate, tick_size), FILL_STOP_LOSS, ambiguous
1595
+ if tp_hit:
1596
+ price = open_price if tp_gap and tp_gap_policy == TP_OPEN_PRICE_IMPROVEMENT else tp_price
1597
+ return exit_side, _quantize_price_numba(price, exit_side, tick_size), FILL_TAKE_PROFIT, ambiguous
1598
+ return 0, 0.0, 0, False
1599
+
1600
+
1601
+ @njit(cache=True, nogil=True)
1602
+ def _update_trailing_numba(trailing_value, position, close_price, current_stop, level_mode, tick_size):
1603
+ if not np.isfinite(trailing_value) or trailing_value <= 0.0:
1604
+ return current_stop
1605
+ side = 1 if position > 0.0 else -1
1606
+ candidate = _level_price_numba(close_price, side, trailing_value, level_mode, True, tick_size)
1607
+ if not np.isfinite(current_stop):
1608
+ return candidate
1609
+ return max(current_stop, candidate) if side > 0 else min(current_stop, candidate)
1610
+
1611
+
1612
+ @njit(cache=True, nogil=True)
1613
+ def _quantize_price_numba(price, side, tick_size):
1614
+ if tick_size <= 0.0 or not np.isfinite(price):
1615
+ return price
1616
+ if side > 0:
1617
+ return np.ceil((price / tick_size) - 1e-12) * tick_size
1618
+ return np.floor((price / tick_size) + 1e-12) * tick_size
1619
+
1620
+
1621
+ @njit(cache=True, nogil=True)
1622
+ def _compile_entry_quantity_numba(size_weight, fill_price, equity, contract_size, sizing_mode, fixed_notional, equity_fraction, risk_fraction, stop_value, level_mode, side, tick_size):
1623
+ weight = abs(size_weight)
1624
+ if sizing_mode == SIZING_UNITS:
1625
+ return weight
1626
+ if fill_price <= 0.0 or contract_size <= 0.0:
1627
+ return 0.0
1628
+ if sizing_mode == SIZING_FIXED_NOTIONAL:
1629
+ return fixed_notional * weight / (fill_price * contract_size)
1630
+ if sizing_mode == SIZING_PCT_EQUITY:
1631
+ return equity * equity_fraction * weight / (fill_price * contract_size)
1632
+ if sizing_mode == SIZING_RISK_PER_TRADE:
1633
+ if not np.isfinite(stop_value) or stop_value <= 0.0:
1634
+ return 0.0
1635
+ stop_price = _level_price_numba(fill_price, side, stop_value, level_mode, True, tick_size)
1636
+ stop_distance = abs(fill_price - stop_price)
1637
+ if stop_distance <= 0.0:
1638
+ return 0.0
1639
+ return equity * risk_fraction * weight / (stop_distance * contract_size)
1640
+ return 0.0
1641
+
1642
+
1643
+ @njit(cache=True, nogil=True)
1644
+ def _quantize_signed_quantity_numba(qty, price, contract_size, qty_step, min_qty, min_notional):
1645
+ if qty == 0.0:
1646
+ return 0.0
1647
+ sign = 1.0 if qty > 0.0 else -1.0
1648
+ abs_q = abs(qty)
1649
+ if qty_step > 0.0:
1650
+ abs_q = np.floor((abs_q / qty_step) + 1e-12) * qty_step
1651
+ if abs_q <= 0.0:
1652
+ return 0.0
1653
+ if min_qty > 0.0 and abs_q + 1e-12 < min_qty:
1654
+ return 0.0
1655
+ if min_notional > 0.0 and abs_q * price * contract_size + 1e-12 < min_notional:
1656
+ return 0.0
1657
+ return sign * abs_q
1658
+
1659
+
1660
+ @njit(cache=True, nogil=True)
1661
+ def _record_fill_numba(record, count, bar, seq, side, qty, price, fee, reason, fill_bar, fill_seq, fill_side, fill_qty, fill_price, fill_fee, fill_reason):
1662
+ if record and count < fill_bar.shape[0]:
1663
+ fill_bar[count] = bar
1664
+ fill_seq[count] = seq
1665
+ fill_side[count] = side
1666
+ fill_qty[count] = qty
1667
+ fill_price[count] = price
1668
+ fill_fee[count] = fee
1669
+ fill_reason[count] = reason
1670
+ return count + 1
1671
+
1672
+
1673
+ def _optional_float_array(value, n: int) -> np.ndarray:
1674
+ if value is None:
1675
+ return np.full(n, np.nan, dtype=np.float64)
1676
+ return np.ascontiguousarray(value, dtype=np.float64)
1677
+
1678
+
1679
+ def _optional_bool_array(value, n: int) -> np.ndarray:
1680
+ if value is None:
1681
+ return np.zeros(n, dtype=np.bool_)
1682
+ return np.ascontiguousarray(value, dtype=np.bool_)
1683
+
1684
+
1685
+ def _level_mode_code(mode) -> int:
1686
+ value = mode.value if hasattr(mode, "value") else str(mode)
1687
+ if value == IntrabarLevelMode.ABSOLUTE_PRICE.value:
1688
+ return LEVEL_ABSOLUTE_PRICE
1689
+ if value == IntrabarLevelMode.PRICE_DISTANCE.value:
1690
+ return LEVEL_PRICE_DISTANCE
1691
+ if value == IntrabarLevelMode.PERCENT_DISTANCE.value:
1692
+ return LEVEL_PERCENT_DISTANCE
1693
+ raise NotImplementedError(f"unsupported intrabar level mode={mode!r}")
1694
+
1695
+
1696
+ def _sizing_mode_code(mode) -> int:
1697
+ value = mode.value if hasattr(mode, "value") else str(mode)
1698
+ mapping = {
1699
+ IntrabarSizingMode.UNITS.value: SIZING_UNITS,
1700
+ IntrabarSizingMode.FIXED_NOTIONAL.value: SIZING_FIXED_NOTIONAL,
1701
+ IntrabarSizingMode.PCT_EQUITY.value: SIZING_PCT_EQUITY,
1702
+ IntrabarSizingMode.RISK_PER_TRADE.value: SIZING_RISK_PER_TRADE,
1703
+ }
1704
+ if value not in mapping:
1705
+ raise NotImplementedError(f"unsupported intrabar sizing_mode={mode!r}")
1706
+ return mapping[value]
1707
+
1708
+
1709
+ def _bar_timestamp_semantics_code(value: str) -> int:
1710
+ semantics = str(value or "close").lower().strip()
1711
+ if semantics == "close":
1712
+ return BAR_TS_CLOSE
1713
+ if semantics == "open":
1714
+ return BAR_TS_OPEN
1715
+ raise ValueError("bar_timestamp_semantics must be 'open' or 'close'")
1716
+
1717
+
1718
+ def _session_entry_policy_code(policy) -> int:
1719
+ value = policy.value if hasattr(policy, "value") else str(policy)
1720
+ if value == EntryPositionPolicy.CURRENT_BEHAVIOR.value:
1721
+ return SESSION_ENTRY_CURRENT
1722
+ if value == EntryPositionPolicy.FLAT_ONLY.value:
1723
+ return SESSION_ENTRY_FLAT_ONLY
1724
+ if value == EntryPositionPolicy.REVERSE.value:
1725
+ return SESSION_ENTRY_REVERSE
1726
+ raise NotImplementedError(f"unsupported session entry_position_policy={policy!r}")
1727
+
1728
+
1729
+ def _session_counter_basis_code(policy) -> int:
1730
+ value = policy.value if hasattr(policy, "value") else str(policy)
1731
+ if value == SessionCounterBasis.FILLED_ENTRY.value:
1732
+ return SESSION_COUNTER_FILLED
1733
+ if value == SessionCounterBasis.ACCEPTED_ENTRY.value:
1734
+ return SESSION_COUNTER_ACCEPTED
1735
+ raise NotImplementedError(f"unsupported session counter_basis={policy!r}")
1736
+
1737
+
1738
+ def _session_reentry_policy_code(policy) -> int:
1739
+ value = policy.value if hasattr(policy, "value") else str(policy)
1740
+ if value == ProtectiveExitReentryPolicy.ALLOW.value:
1741
+ return SESSION_REENTRY_ALLOW
1742
+ if value == ProtectiveExitReentryPolicy.SUPPRESS_SIGNAL_BAR.value:
1743
+ return SESSION_REENTRY_SUPPRESS_SIGNAL_BAR
1744
+ raise NotImplementedError(f"unsupported protective_exit_reentry_policy={policy!r}")
1745
+
1746
+
1747
+ def _same_bar_policy_code(policy) -> int:
1748
+ value = policy.value if hasattr(policy, "value") else str(policy)
1749
+ mapping = {
1750
+ IntrabarSameBarPolicy.CONSERVATIVE.value: SAME_BAR_CONSERVATIVE,
1751
+ IntrabarSameBarPolicy.STOP_FIRST.value: SAME_BAR_STOP_FIRST,
1752
+ IntrabarSameBarPolicy.TP_FIRST.value: SAME_BAR_TP_FIRST,
1753
+ IntrabarSameBarPolicy.OHLC_PATH.value: SAME_BAR_OHLC_PATH,
1754
+ IntrabarSameBarPolicy.OLHC_PATH.value: SAME_BAR_OLHC_PATH,
1755
+ IntrabarSameBarPolicy.REJECT_AMBIGUOUS.value: SAME_BAR_REJECT_AMBIGUOUS,
1756
+ }
1757
+ if value not in mapping:
1758
+ raise NotImplementedError(f"unsupported same-bar policy={policy!r}")
1759
+ return mapping[value]
1760
+
1761
+
1762
+ def _tp_policy_code(policy) -> int:
1763
+ value = policy.value if hasattr(policy, "value") else str(policy)
1764
+ if value == TakeProfitGapPolicy.LIMIT_PRICE_CONSERVATIVE.value:
1765
+ return TP_LIMIT_CONSERVATIVE
1766
+ if value == TakeProfitGapPolicy.OPEN_PRICE_IMPROVEMENT.value:
1767
+ return TP_OPEN_PRICE_IMPROVEMENT
1768
+ raise NotImplementedError(f"unsupported take-profit gap policy={policy!r}")
1769
+
1770
+
1771
+ def _normalize_report_level(report_level: str) -> str:
1772
+ level = str(report_level or "standard").lower().strip()
1773
+ aliases = {"full": "audit", "debug": "audit", "optimizer": "minimal", "scoring": "minimal"}
1774
+ level = aliases.get(level, level)
1775
+ if level not in {"minimal", "standard", "audit"}:
1776
+ raise ValueError("report_level must be minimal, standard, or audit")
1777
+ return level
1778
+
1779
+
1780
+ def _assert_intrabar_audit_parity(first, second, atol: float = 1e-9) -> None:
1781
+ for i, name in enumerate(("equity", "position", "average_entry", "active_stop", "active_take_profit", "fees", "funding", "flags")):
1782
+ if not np.allclose(first[i], second[i], atol=atol, rtol=0.0):
1783
+ raise AssertionError(f"intrabar audit replay drifted from pass 1 for {name}")
1784
+ for i, name in ((10, "fill_count"), (11, "ambiguity_count"), (12, "rejected_count"), (13, "liquidated"), (14, "liquidation_bar")):
1785
+ if first[i] != second[i]:
1786
+ raise AssertionError(f"intrabar audit replay drifted from pass 1 for {name}")
1787
+
1788
+
1789
+ def _assert_intrabar_session_audit_parity(first, second, atol: float = 1e-9) -> None:
1790
+ _assert_intrabar_audit_parity(first, second, atol=atol)
1791
+ for i, name in (
1792
+ (22, "session_reset_count"),
1793
+ (23, "session_forced_exit_count"),
1794
+ (24, "entry_window_blocked_count"),
1795
+ (25, "long_quota_blocked_count"),
1796
+ (26, "short_quota_blocked_count"),
1797
+ (27, "flat_only_blocked_count"),
1798
+ (28, "stale_session_signal_count"),
1799
+ (29, "reentry_suppressed_count"),
1800
+ ):
1801
+ if first[i] != second[i]:
1802
+ raise AssertionError(f"intrabar session audit replay drifted from pass 1 for {name}")
1803
+
1804
+
1805
+ def _materialize_intrabar_fills(
1806
+ *,
1807
+ timestamps_ns: np.ndarray,
1808
+ fill_bar: np.ndarray,
1809
+ fill_seq: np.ndarray,
1810
+ fill_side: np.ndarray,
1811
+ fill_qty: np.ndarray,
1812
+ fill_price: np.ndarray,
1813
+ fill_fee: np.ndarray,
1814
+ fill_reason: np.ndarray,
1815
+ fill_count: int,
1816
+ ) -> tuple[IntrabarFill, ...]:
1817
+ idx = pd.DatetimeIndex(pd.to_datetime(timestamps_ns, utc=True))
1818
+ out = []
1819
+ for i in range(fill_count):
1820
+ bar = int(fill_bar[i])
1821
+ out.append(
1822
+ IntrabarFill(
1823
+ bar_index=bar,
1824
+ sequence=int(fill_seq[i]),
1825
+ timestamp=pd.Timestamp(idx[bar]),
1826
+ side=int(fill_side[i]),
1827
+ qty=float(fill_qty[i]),
1828
+ price=float(fill_price[i]),
1829
+ fee=float(fill_fee[i]),
1830
+ reason=_reason_code_to_enum(int(fill_reason[i])),
1831
+ )
1832
+ )
1833
+ return tuple(out)
1834
+
1835
+
1836
+ def _fills_to_report(fills: Sequence[IntrabarFill]) -> pd.DataFrame:
1837
+ return pd.DataFrame(
1838
+ [
1839
+ {
1840
+ "bar_index": fill.bar_index,
1841
+ "sequence": fill.sequence,
1842
+ "timestamp": fill.timestamp,
1843
+ "side": fill.side,
1844
+ "qty": fill.qty,
1845
+ "price": fill.price,
1846
+ "fee": fill.fee,
1847
+ "reason": fill.reason.value,
1848
+ }
1849
+ for fill in fills
1850
+ ]
1851
+ )
1852
+
1853
+
1854
+ def _reason_code_to_enum(code: int) -> IntrabarFillReason:
1855
+ mapping = {
1856
+ FILL_ENTRY: IntrabarFillReason.ENTRY,
1857
+ FILL_TECHNICAL_EXIT: IntrabarFillReason.TECHNICAL_EXIT,
1858
+ FILL_REVERSAL_EXIT: IntrabarFillReason.REVERSAL_EXIT,
1859
+ FILL_REVERSAL_ENTRY: IntrabarFillReason.REVERSAL_ENTRY,
1860
+ FILL_STOP_LOSS: IntrabarFillReason.STOP_LOSS,
1861
+ FILL_TAKE_PROFIT: IntrabarFillReason.TAKE_PROFIT,
1862
+ FILL_LIQUIDATION: IntrabarFillReason.LIQUIDATION,
1863
+ FILL_FINAL_CLOSE: IntrabarFillReason.FINAL_CLOSE,
1864
+ FILL_SESSION_FORCED_EXIT: IntrabarFillReason.SESSION_FORCED_EXIT,
1865
+ }
1866
+ return mapping.get(code, IntrabarFillReason.ENTRY)
1867
+
1868
+
1869
+ def _reason_series_to_codes(series: pd.Series) -> np.ndarray:
1870
+ out = np.zeros(len(series), dtype=np.int16)
1871
+ mapping = {reason.value: code for code, reason in (
1872
+ (FILL_ENTRY, IntrabarFillReason.ENTRY),
1873
+ (FILL_TECHNICAL_EXIT, IntrabarFillReason.TECHNICAL_EXIT),
1874
+ (FILL_REVERSAL_EXIT, IntrabarFillReason.REVERSAL_EXIT),
1875
+ (FILL_REVERSAL_ENTRY, IntrabarFillReason.REVERSAL_ENTRY),
1876
+ (FILL_STOP_LOSS, IntrabarFillReason.STOP_LOSS),
1877
+ (FILL_TAKE_PROFIT, IntrabarFillReason.TAKE_PROFIT),
1878
+ (FILL_LIQUIDATION, IntrabarFillReason.LIQUIDATION),
1879
+ (FILL_FINAL_CLOSE, IntrabarFillReason.FINAL_CLOSE),
1880
+ (FILL_SESSION_FORCED_EXIT, IntrabarFillReason.SESSION_FORCED_EXIT),
1881
+ )}
1882
+ for i, value in enumerate(series.astype(str)):
1883
+ out[i] = mapping.get(value, 0)
1884
+ return out
1885
+
1886
+
1887
+ def _validate_fill_replay_tape(fill_tape: FillReplayTape, n_bars: int) -> None:
1888
+ if not (len(fill_tape.bar_index) == len(fill_tape.sequence) == len(fill_tape.side) == len(fill_tape.qty) == len(fill_tape.price) == len(fill_tape.fee)):
1889
+ raise ValueError("fill replay arrays must have matching lengths")
1890
+ if len(fill_tape.bar_index) == 0:
1891
+ return
1892
+ if np.any(fill_tape.bar_index < 0) or np.any(fill_tape.bar_index >= n_bars):
1893
+ raise ValueError("fill replay bar_index is out of market tape range")
1894
+ if not np.isfinite(fill_tape.qty).all() or not np.isfinite(fill_tape.price).all() or not np.isfinite(fill_tape.fee).all():
1895
+ raise ValueError("fill replay qty/price/fee must be finite")
1896
+ if np.any(fill_tape.qty <= 0.0) or np.any(fill_tape.price <= 0.0) or np.any(fill_tape.fee < 0.0):
1897
+ raise ValueError("fill replay qty/price must be positive and fee non-negative")
1898
+ prev_bar = int(fill_tape.bar_index[0])
1899
+ prev_seq = int(fill_tape.sequence[0])
1900
+ for bar, seq in zip(fill_tape.bar_index[1:], fill_tape.sequence[1:]):
1901
+ bar_i = int(bar)
1902
+ seq_i = int(seq)
1903
+ if bar_i < prev_bar or (bar_i == prev_bar and seq_i < prev_seq):
1904
+ raise ValueError("fill replay tape must be sorted by bar_index then sequence")
1905
+ prev_bar = bar_i
1906
+ prev_seq = seq_i