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,907 @@
1
+ """
2
+ Readable Python oracle for the Phase 31 intrabar execution contract.
3
+
4
+ This is not a performance engine. It is the reference state machine used to
5
+ prove the later Numba kernel. Strategy output is intentionally compact:
6
+ entry side/size plus optional stop, take-profit, trailing distance, and
7
+ technical-exit arrays.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum, IntFlag
14
+ from typing import Dict, Optional, Sequence
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ from .execution_contract import ExecutionContract, IntrabarSameBarPolicy, TakeProfitGapPolicy
20
+ from .constraints import quantize_signed_quantity
21
+ from .intrabar_session import (
22
+ EntryPositionPolicy,
23
+ IntrabarSessionTape,
24
+ ProtectiveExitReentryPolicy,
25
+ SessionCounterBasis,
26
+ SessionExecutionPolicy,
27
+ )
28
+ from .market_tape import PreparedMarketTape
29
+ from .schema import AccountConfig
30
+
31
+
32
+ class IntrabarLevelMode(str, Enum):
33
+ ABSOLUTE_PRICE = "absolute_price"
34
+ PRICE_DISTANCE = "price_distance"
35
+ PERCENT_DISTANCE = "percent_distance"
36
+
37
+
38
+ class IntrabarSizingMode(str, Enum):
39
+ UNITS = "units"
40
+ FIXED_NOTIONAL = "fixed_notional"
41
+ PCT_EQUITY = "pct_equity"
42
+ RISK_PER_TRADE = "risk_per_trade"
43
+
44
+
45
+ class IntrabarFillReason(str, Enum):
46
+ ENTRY = "entry"
47
+ TECHNICAL_EXIT = "technical_exit"
48
+ REVERSAL_EXIT = "reversal_exit"
49
+ REVERSAL_ENTRY = "reversal_entry"
50
+ STOP_LOSS = "stop_loss"
51
+ TAKE_PROFIT = "take_profit"
52
+ LIQUIDATION = "liquidation"
53
+ FINAL_CLOSE = "final_close"
54
+ SESSION_FORCED_EXIT = "session_forced_exit"
55
+
56
+
57
+ class IntrabarEventFlag(IntFlag):
58
+ NONE = 0
59
+ ENTRY_FILLED = 1 << 0
60
+ EXIT_FILLED = 1 << 1
61
+ STOP_FILLED = 1 << 2
62
+ TP_FILLED = 1 << 3
63
+ TECH_EXIT = 1 << 4
64
+ REVERSAL = 1 << 5
65
+ AMBIGUOUS = 1 << 6
66
+ FUNDING = 1 << 7
67
+ LIQUIDATION = 1 << 8
68
+ REJECTED = 1 << 9
69
+ ENTRY_SUPPRESSED = 1 << 10
70
+ SESSION_RESET = 1 << 11
71
+ SESSION_FORCED_EXIT = 1 << 12
72
+ ENTRY_WINDOW_BLOCKED = 1 << 13
73
+ ENTRY_QUOTA_BLOCKED = 1 << 14
74
+ FLAT_ONLY_BLOCKED = 1 << 15
75
+ STALE_SESSION_SIGNAL = 1 << 16
76
+ PROTECTIVE_REENTRY_BLOCKED = 1 << 17
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class IntrabarIntentTape:
81
+ entry_side: np.ndarray
82
+ entry_size: np.ndarray
83
+ stop_value: Optional[np.ndarray] = None
84
+ take_profit_value: Optional[np.ndarray] = None
85
+ trailing_value: Optional[np.ndarray] = None
86
+ technical_exit: Optional[np.ndarray] = None
87
+ exit_long: Optional[np.ndarray] = None
88
+ exit_short: Optional[np.ndarray] = None
89
+ level_mode: IntrabarLevelMode = IntrabarLevelMode.PERCENT_DISTANCE
90
+
91
+ def __post_init__(self) -> None:
92
+ n = len(self.entry_side)
93
+ if len(self.entry_size) != n:
94
+ raise ValueError("entry_size must have the same length as entry_side")
95
+ for name in ("stop_value", "take_profit_value", "trailing_value", "technical_exit", "exit_long", "exit_short"):
96
+ value = getattr(self, name)
97
+ if value is not None and len(value) != n:
98
+ raise ValueError(f"{name} must have the same length as entry_side")
99
+
100
+ @classmethod
101
+ def from_arrays(
102
+ cls,
103
+ *,
104
+ entry_side: Sequence,
105
+ entry_size: Sequence,
106
+ stop_value: Optional[Sequence] = None,
107
+ take_profit_value: Optional[Sequence] = None,
108
+ trailing_value: Optional[Sequence] = None,
109
+ technical_exit: Optional[Sequence] = None,
110
+ exit_long: Optional[Sequence] = None,
111
+ exit_short: Optional[Sequence] = None,
112
+ level_mode: IntrabarLevelMode = IntrabarLevelMode.PERCENT_DISTANCE,
113
+ ) -> "IntrabarIntentTape":
114
+ legacy_exit = None if technical_exit is None else np.ascontiguousarray(technical_exit, dtype=np.bool_)
115
+ return cls(
116
+ entry_side=np.ascontiguousarray(entry_side, dtype=np.int8),
117
+ entry_size=np.ascontiguousarray(entry_size, dtype=np.float64),
118
+ stop_value=_optional_float_array(stop_value),
119
+ take_profit_value=_optional_float_array(take_profit_value),
120
+ trailing_value=_optional_float_array(trailing_value),
121
+ technical_exit=legacy_exit,
122
+ exit_long=legacy_exit if exit_long is None and legacy_exit is not None else _optional_bool_array(exit_long),
123
+ exit_short=legacy_exit if exit_short is None and legacy_exit is not None else _optional_bool_array(exit_short),
124
+ level_mode=level_mode,
125
+ )
126
+
127
+ @classmethod
128
+ def from_frame(
129
+ cls,
130
+ frame: pd.DataFrame,
131
+ *,
132
+ entry_side_col: str = "entry_side",
133
+ signal_col: Optional[str] = None,
134
+ entry_size_col: str = "entry_size",
135
+ stop_col: str = "stop_value",
136
+ take_profit_col: str = "take_profit_value",
137
+ trailing_col: str = "trailing_value",
138
+ technical_exit_col: str = "technical_exit",
139
+ exit_long_col: str = "exit_long",
140
+ exit_short_col: str = "exit_short",
141
+ level_mode: IntrabarLevelMode = IntrabarLevelMode.PERCENT_DISTANCE,
142
+ ) -> "IntrabarIntentTape":
143
+ """Build intrabar intents from an alpha output frame.
144
+
145
+ This is an adapter convenience only. Strategy code still owns signal
146
+ causality; the intrabar kernel still owns fills, SL/TP/trailing, fee,
147
+ funding, margin, and liquidation semantics.
148
+ """
149
+
150
+ if not isinstance(frame, pd.DataFrame):
151
+ raise TypeError("frame must be a pandas DataFrame")
152
+ if entry_side_col in frame:
153
+ side = np.sign(frame[entry_side_col].fillna(0.0).to_numpy(dtype=float)).astype(np.int8)
154
+ else:
155
+ raw_col = signal_col or ("signal" if "signal" in frame else "entry")
156
+ if raw_col not in frame:
157
+ raise ValueError(f"frame must contain {entry_side_col!r}, {raw_col!r}, or provide signal_col")
158
+ raw = frame[raw_col].fillna(0.0).to_numpy(dtype=float)
159
+ side = np.sign(raw).astype(np.int8)
160
+ if entry_size_col in frame:
161
+ size = np.abs(frame[entry_size_col].fillna(0.0).to_numpy(dtype=float))
162
+ else:
163
+ size = np.abs(side.astype(np.float64))
164
+
165
+ def optional(name: str):
166
+ return frame[name].to_numpy() if name in frame else None
167
+
168
+ return cls.from_arrays(
169
+ entry_side=side,
170
+ entry_size=size,
171
+ stop_value=optional(stop_col),
172
+ take_profit_value=optional(take_profit_col),
173
+ trailing_value=optional(trailing_col),
174
+ technical_exit=optional(technical_exit_col),
175
+ exit_long=optional(exit_long_col),
176
+ exit_short=optional(exit_short_col),
177
+ level_mode=level_mode,
178
+ )
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class IntrabarFill:
183
+ bar_index: int
184
+ sequence: int
185
+ timestamp: pd.Timestamp
186
+ side: int
187
+ qty: float
188
+ price: float
189
+ fee: float
190
+ reason: IntrabarFillReason
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class IntrabarReferenceResult:
195
+ equity: pd.Series
196
+ position: pd.Series
197
+ average_entry: pd.Series
198
+ active_stop: pd.Series
199
+ active_take_profit: pd.Series
200
+ fees: pd.Series
201
+ funding: pd.Series
202
+ event_flags: pd.Series
203
+ fills: tuple[IntrabarFill, ...]
204
+ ambiguity_count: int
205
+ rejected_count: int = 0
206
+ liquidated: bool = False
207
+ liquidation_bar: int = -1
208
+ metadata: Dict = field(default_factory=dict)
209
+
210
+
211
+ def run_intrabar_reference(
212
+ *,
213
+ tape: PreparedMarketTape,
214
+ intent: IntrabarIntentTape,
215
+ account: AccountConfig,
216
+ contract: Optional[ExecutionContract] = None,
217
+ fee_rate: float = 0.0,
218
+ slippage_rate: float = 0.0,
219
+ contract_size: float = 1.0,
220
+ sizing_mode: IntrabarSizingMode | str = IntrabarSizingMode.UNITS,
221
+ fixed_notional: float = 0.0,
222
+ equity_fraction: float = 0.0,
223
+ risk_fraction: float = 0.0,
224
+ qty_step: float = 0.0,
225
+ min_qty: float = 0.0,
226
+ min_notional: float = 0.0,
227
+ tick_size: float = 0.0,
228
+ session_policy: Optional[SessionExecutionPolicy] = None,
229
+ session_tape: Optional[IntrabarSessionTape] = None,
230
+ ) -> IntrabarReferenceResult:
231
+ """
232
+ Execute a single-symbol intrabar bracket tape with causal next-open timing.
233
+
234
+ Decision arrays at index `t-1` become executable at `open[t]`.
235
+ """
236
+ if tape.n_symbols != 1:
237
+ raise NotImplementedError("Phase 31B intrabar oracle certifies single-symbol tapes only")
238
+ if len(intent.entry_side) != tape.n_bars:
239
+ raise ValueError("intent length must match market tape length")
240
+ if account.initial_capital <= 0.0:
241
+ raise ValueError("initial_capital must be > 0")
242
+ if fee_rate < 0.0 or slippage_rate < 0.0:
243
+ raise ValueError("fee_rate and slippage_rate must be >= 0")
244
+ if (session_policy is None) != (session_tape is None):
245
+ raise ValueError("session_policy and session_tape must be provided together")
246
+ session_enabled = session_policy is not None
247
+ if session_enabled and len(session_tape.session_id) != tape.n_bars:
248
+ raise ValueError("session_tape length must match market tape length")
249
+ contract = contract or ExecutionContract.intrabar_bracket()
250
+ if contract.engine_id != "intrabar_bracket_v1":
251
+ raise ValueError("run_intrabar_reference requires intrabar_bracket_v1 contract")
252
+ _validate_intrabar_contract_supported(contract)
253
+ sizing_code = IntrabarSizingMode(sizing_mode)
254
+
255
+ idx = pd.DatetimeIndex(pd.to_datetime(tape.timestamps_ns, utc=True))
256
+ opens = tape.opens[:, 0]
257
+ highs = tape.highs[:, 0]
258
+ lows = tape.lows[:, 0]
259
+ closes = tape.closes[:, 0]
260
+ funding_rates = tape.funding_rates[:, 0]
261
+ funding_mask = tape.funding_event_mask
262
+ timestamp_semantics = str(getattr(tape, "bar_timestamp_semantics", "close")).lower().strip()
263
+ if timestamp_semantics not in {"open", "close"}:
264
+ raise ValueError("bar_timestamp_semantics must be 'open' or 'close'")
265
+ funding_at_open = timestamp_semantics == "open"
266
+
267
+ n = tape.n_bars
268
+ equity_arr = np.zeros(n, dtype=np.float64)
269
+ pos_arr = np.zeros(n, dtype=np.float64)
270
+ avg_arr = np.zeros(n, dtype=np.float64)
271
+ stop_arr = np.zeros(n, dtype=np.float64)
272
+ tp_arr = np.zeros(n, dtype=np.float64)
273
+ fee_arr = np.zeros(n, dtype=np.float64)
274
+ funding_arr = np.zeros(n, dtype=np.float64)
275
+ flags_arr = np.zeros(n, dtype=np.uint32)
276
+
277
+ equity = float(account.initial_capital)
278
+ position = 0.0
279
+ avg_entry = 0.0
280
+ active_stop = np.nan
281
+ active_tp = np.nan
282
+ fills: list[IntrabarFill] = []
283
+ ambiguity_count = 0
284
+ rejected_count = 0
285
+ liquidated = False
286
+ liquidation_bar = -1
287
+ current_session_id = int(session_tape.session_id[0]) if session_enabled and n else 0
288
+ long_entry_count = 0
289
+ short_entry_count = 0
290
+ protective_exit_on_previous_bar = False
291
+ session_reset_count = 0
292
+ session_forced_exit_count = 0
293
+ entry_window_blocked_count = 0
294
+ long_quota_blocked_count = 0
295
+ short_quota_blocked_count = 0
296
+ flat_only_blocked_count = 0
297
+ stale_session_signal_count = 0
298
+ reentry_suppressed_count = 0
299
+
300
+ equity_arr[0] = equity
301
+ for t in range(1, n):
302
+ if liquidated:
303
+ equity_arr[t] = 0.0
304
+ pos_arr[t] = 0.0
305
+ avg_arr[t] = 0.0
306
+ stop_arr[t] = 0.0
307
+ tp_arr[t] = 0.0
308
+ continue
309
+
310
+ seq = 0
311
+ open_ref = float(opens[t])
312
+ close_ref = float(closes[t])
313
+ last_ref = open_ref
314
+ if position != 0.0:
315
+ equity += position * (open_ref - float(closes[t - 1])) * contract_size
316
+
317
+ reentry_block_from_previous_bar = False
318
+ if session_enabled:
319
+ bar_session_id = int(session_tape.session_id[t])
320
+ if bar_session_id != current_session_id:
321
+ current_session_id = bar_session_id
322
+ long_entry_count = 0
323
+ short_entry_count = 0
324
+ protective_exit_on_previous_bar = False
325
+ flags_arr[t] |= int(IntrabarEventFlag.SESSION_RESET)
326
+ session_reset_count += 1
327
+ reentry_block_from_previous_bar = bool(protective_exit_on_previous_bar)
328
+ protective_exit_on_previous_bar = False
329
+
330
+ if position != 0.0 and _maintenance_breached(equity, position, open_ref, contract_size, account.maintenance_ratio):
331
+ side = -1 if position > 0.0 else 1
332
+ price = _market_price(open_ref, side, slippage_rate, tick_size=tick_size)
333
+ fee = abs(position) * price * contract_size * fee_rate
334
+ equity += position * (price - open_ref) * contract_size - fee
335
+ fee_arr[t] += fee
336
+ fills.append(_fill(t, seq, idx[t], side, abs(position), price, fee, IntrabarFillReason.LIQUIDATION))
337
+ flags_arr[t] |= int(IntrabarEventFlag.EXIT_FILLED | IntrabarEventFlag.LIQUIDATION)
338
+ liquidated = True
339
+ liquidation_bar = t
340
+ equity = 0.0
341
+ position = 0.0
342
+ avg_entry = 0.0
343
+ active_stop = np.nan
344
+ active_tp = np.nan
345
+ equity_arr[t] = 0.0
346
+ pos_arr[t] = 0.0
347
+ avg_arr[t] = 0.0
348
+ stop_arr[t] = 0.0
349
+ tp_arr[t] = 0.0
350
+ continue
351
+
352
+ if funding_at_open and position != 0.0 and funding_mask[t]:
353
+ funding_cost = position * open_ref * contract_size * funding_rates[t]
354
+ equity -= funding_cost
355
+ funding_arr[t] = funding_cost
356
+ flags_arr[t] |= int(IntrabarEventFlag.FUNDING)
357
+
358
+ force_flat_bar = bool(session_enabled and session_tape.force_flat_at_open[t])
359
+ if force_flat_bar and position != 0.0:
360
+ side = -1 if position > 0.0 else 1
361
+ price = _market_price(open_ref, side, slippage_rate, tick_size=tick_size)
362
+ fee = abs(position) * price * contract_size * fee_rate
363
+ equity += position * (price - open_ref) * contract_size - fee
364
+ fee_arr[t] += fee
365
+ fills.append(_fill(t, seq, idx[t], side, abs(position), price, fee, IntrabarFillReason.SESSION_FORCED_EXIT))
366
+ seq += 1
367
+ flags_arr[t] |= int(IntrabarEventFlag.EXIT_FILLED | IntrabarEventFlag.SESSION_FORCED_EXIT)
368
+ session_forced_exit_count += 1
369
+ position = 0.0
370
+ avg_entry = 0.0
371
+ active_stop = np.nan
372
+ active_tp = np.nan
373
+
374
+ pending_side = int(intent.entry_side[t - 1])
375
+ pending_size = float(intent.entry_size[t - 1])
376
+ pending_exit = _pending_exit(intent, t - 1, position)
377
+ stale_session_signal = bool(
378
+ session_enabled
379
+ and session_policy.cancel_pending_on_session_change
380
+ and pending_side != 0
381
+ and int(session_tape.session_id[t - 1]) != int(session_tape.session_id[t])
382
+ )
383
+ if stale_session_signal:
384
+ pending_side = 0
385
+ pending_size = 0.0
386
+ flags_arr[t] |= int(IntrabarEventFlag.STALE_SESSION_SIGNAL | IntrabarEventFlag.ENTRY_SUPPRESSED)
387
+ stale_session_signal_count += 1
388
+ if (
389
+ session_enabled
390
+ and pending_side != 0
391
+ and position != 0.0
392
+ and session_policy.entry_position_policy is EntryPositionPolicy.FLAT_ONLY
393
+ ):
394
+ pending_side = 0
395
+ pending_size = 0.0
396
+ flags_arr[t] |= int(IntrabarEventFlag.FLAT_ONLY_BLOCKED | IntrabarEventFlag.ENTRY_SUPPRESSED)
397
+ flat_only_blocked_count += 1
398
+ exit_same_side_conflict = bool(
399
+ pending_exit and pending_side != 0 and position != 0.0 and np.sign(position) == pending_side
400
+ )
401
+ reversal_allowed = not (
402
+ session_enabled and session_policy.entry_position_policy is EntryPositionPolicy.FLAT_ONLY
403
+ )
404
+
405
+ if position != 0.0 and (pending_exit or (reversal_allowed and pending_side != 0 and np.sign(position) != pending_side)):
406
+ reason = IntrabarFillReason.REVERSAL_EXIT if pending_side != 0 and np.sign(position) != pending_side else IntrabarFillReason.TECHNICAL_EXIT
407
+ side = -1 if position > 0.0 else 1
408
+ price = _market_price(open_ref, side, slippage_rate, tick_size=tick_size)
409
+ fee = abs(position) * price * contract_size * fee_rate
410
+ equity += position * (price - open_ref) * contract_size - fee
411
+ fee_arr[t] += fee
412
+ fills.append(_fill(t, seq, idx[t], side, abs(position), price, fee, reason))
413
+ seq += 1
414
+ flags_arr[t] |= int(IntrabarEventFlag.EXIT_FILLED)
415
+ if reason is IntrabarFillReason.TECHNICAL_EXIT:
416
+ flags_arr[t] |= int(IntrabarEventFlag.TECH_EXIT)
417
+ else:
418
+ flags_arr[t] |= int(IntrabarEventFlag.REVERSAL)
419
+ position = 0.0
420
+ avg_entry = 0.0
421
+ active_stop = np.nan
422
+ active_tp = np.nan
423
+
424
+ if pending_side != 0 and pending_size > 0.0 and position == 0.0:
425
+ side = 1 if pending_side > 0 else -1
426
+ price = _market_price(open_ref, side, slippage_rate, tick_size=tick_size)
427
+ entry_blocked = False
428
+ if session_enabled:
429
+ if force_flat_bar and session_policy.suppress_entry_on_force_flat_bar:
430
+ entry_blocked = True
431
+ flags_arr[t] |= int(IntrabarEventFlag.SESSION_FORCED_EXIT | IntrabarEventFlag.ENTRY_SUPPRESSED)
432
+ elif not bool(session_tape.entry_allowed_at_open[t]):
433
+ entry_blocked = True
434
+ entry_window_blocked_count += 1
435
+ flags_arr[t] |= int(IntrabarEventFlag.ENTRY_WINDOW_BLOCKED | IntrabarEventFlag.ENTRY_SUPPRESSED)
436
+ elif (
437
+ session_policy.protective_exit_reentry_policy is ProtectiveExitReentryPolicy.SUPPRESS_SIGNAL_BAR
438
+ and reentry_block_from_previous_bar
439
+ ):
440
+ entry_blocked = True
441
+ reentry_suppressed_count += 1
442
+ flags_arr[t] |= int(IntrabarEventFlag.PROTECTIVE_REENTRY_BLOCKED | IntrabarEventFlag.ENTRY_SUPPRESSED)
443
+ elif side > 0 and session_policy.max_long_entries_per_session is not None and long_entry_count >= session_policy.max_long_entries_per_session:
444
+ entry_blocked = True
445
+ long_quota_blocked_count += 1
446
+ flags_arr[t] |= int(IntrabarEventFlag.ENTRY_QUOTA_BLOCKED | IntrabarEventFlag.ENTRY_SUPPRESSED)
447
+ elif side < 0 and session_policy.max_short_entries_per_session is not None and short_entry_count >= session_policy.max_short_entries_per_session:
448
+ entry_blocked = True
449
+ short_quota_blocked_count += 1
450
+ flags_arr[t] |= int(IntrabarEventFlag.ENTRY_QUOTA_BLOCKED | IntrabarEventFlag.ENTRY_SUPPRESSED)
451
+ if exit_same_side_conflict or entry_blocked:
452
+ qty = 0.0
453
+ else:
454
+ qty = _compile_entry_quantity(
455
+ size_weight=float(pending_size),
456
+ fill_price=price,
457
+ equity=equity,
458
+ contract_size=contract_size,
459
+ sizing_mode=sizing_code,
460
+ fixed_notional=fixed_notional,
461
+ equity_fraction=equity_fraction,
462
+ risk_fraction=risk_fraction,
463
+ stop_value=None if intent.stop_value is None else float(intent.stop_value[t - 1]),
464
+ level_mode=intent.level_mode,
465
+ side=side,
466
+ tick_size=tick_size,
467
+ )
468
+ qty = abs(
469
+ quantize_signed_quantity(
470
+ qty,
471
+ price,
472
+ contract_size=contract_size,
473
+ qty_step=qty_step,
474
+ min_qty=min_qty,
475
+ min_notional=min_notional,
476
+ )
477
+ )
478
+ if exit_same_side_conflict or entry_blocked:
479
+ flags_arr[t] |= int(IntrabarEventFlag.ENTRY_SUPPRESSED)
480
+ equity_arr[t] = equity
481
+ pos_arr[t] = position
482
+ avg_arr[t] = avg_entry
483
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
484
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
485
+ continue
486
+ if qty <= 0.0:
487
+ flags_arr[t] |= int(IntrabarEventFlag.REJECTED)
488
+ rejected_count += 1
489
+ equity_arr[t] = equity
490
+ pos_arr[t] = position
491
+ avg_arr[t] = avg_entry
492
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
493
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
494
+ continue
495
+ if not _has_initial_margin(equity, qty, price, contract_size, account.leverage, account.margin_buffer):
496
+ flags_arr[t] |= int(IntrabarEventFlag.REJECTED)
497
+ rejected_count += 1
498
+ equity_arr[t] = equity
499
+ pos_arr[t] = position
500
+ avg_arr[t] = avg_entry
501
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
502
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
503
+ continue
504
+ fee = qty * price * contract_size * fee_rate
505
+ equity -= fee
506
+ fee_arr[t] += fee
507
+ position = qty * side
508
+ avg_entry = price
509
+ last_ref = price
510
+ active_stop, active_tp = _initial_bracket(intent, t - 1, side, price, tick_size=tick_size)
511
+ reason = IntrabarFillReason.REVERSAL_ENTRY if flags_arr[t] & int(IntrabarEventFlag.REVERSAL) else IntrabarFillReason.ENTRY
512
+ fills.append(_fill(t, seq, idx[t], side, qty, price, fee, reason))
513
+ seq += 1
514
+ flags_arr[t] |= int(IntrabarEventFlag.ENTRY_FILLED)
515
+ if session_enabled and session_policy.counter_basis in {SessionCounterBasis.FILLED_ENTRY, SessionCounterBasis.ACCEPTED_ENTRY}:
516
+ if side > 0:
517
+ long_entry_count += 1
518
+ else:
519
+ short_entry_count += 1
520
+
521
+ if position != 0.0:
522
+ exit_info = _resolve_intrabar_exit(
523
+ side=1 if position > 0.0 else -1,
524
+ open_price=open_ref,
525
+ high=float(highs[t]),
526
+ low=float(lows[t]),
527
+ stop_price=active_stop,
528
+ tp_price=active_tp,
529
+ same_bar_policy=contract.same_bar_policy,
530
+ take_profit_gap_policy=contract.take_profit_gap_policy,
531
+ slippage_rate=slippage_rate,
532
+ tick_size=tick_size,
533
+ )
534
+ if exit_info is not None:
535
+ exit_side, exit_price, reason, ambiguous = exit_info
536
+ if ambiguous:
537
+ flags_arr[t] |= int(IntrabarEventFlag.AMBIGUOUS)
538
+ ambiguity_count += 1
539
+ qty = abs(position)
540
+ fee = qty * exit_price * contract_size * fee_rate
541
+ equity += position * (exit_price - last_ref) * contract_size - fee
542
+ fee_arr[t] += fee
543
+ fills.append(_fill(t, seq, idx[t], exit_side, qty, exit_price, fee, reason))
544
+ seq += 1
545
+ flags_arr[t] |= int(IntrabarEventFlag.EXIT_FILLED)
546
+ if reason is IntrabarFillReason.STOP_LOSS:
547
+ flags_arr[t] |= int(IntrabarEventFlag.STOP_FILLED)
548
+ if session_enabled:
549
+ protective_exit_on_previous_bar = True
550
+ else:
551
+ flags_arr[t] |= int(IntrabarEventFlag.TP_FILLED)
552
+ if session_enabled:
553
+ protective_exit_on_previous_bar = True
554
+ position = 0.0
555
+ avg_entry = 0.0
556
+ active_stop = np.nan
557
+ active_tp = np.nan
558
+
559
+ if position != 0.0:
560
+ if _maintenance_breached_at_worst(
561
+ equity,
562
+ position,
563
+ last_ref,
564
+ high=float(highs[t]),
565
+ low=float(lows[t]),
566
+ contract_size=contract_size,
567
+ maintenance_ratio=account.maintenance_ratio,
568
+ ):
569
+ side = -1 if position > 0.0 else 1
570
+ worst = float(lows[t]) if position > 0.0 else float(highs[t])
571
+ price = _market_price(worst, side, slippage_rate, tick_size=tick_size)
572
+ fee = abs(position) * price * contract_size * fee_rate
573
+ equity += position * (price - last_ref) * contract_size - fee
574
+ fee_arr[t] += fee
575
+ fills.append(_fill(t, seq, idx[t], side, abs(position), price, fee, IntrabarFillReason.LIQUIDATION))
576
+ flags_arr[t] |= int(IntrabarEventFlag.EXIT_FILLED | IntrabarEventFlag.LIQUIDATION)
577
+ liquidated = True
578
+ liquidation_bar = t
579
+ equity = 0.0
580
+ position = 0.0
581
+ avg_entry = 0.0
582
+ active_stop = np.nan
583
+ active_tp = np.nan
584
+ else:
585
+ equity += position * (close_ref - last_ref) * contract_size
586
+ active_stop = _update_trailing(intent, t, position, close_ref, active_stop, tick_size=tick_size)
587
+
588
+ if liquidated:
589
+ equity_arr[t] = 0.0
590
+ pos_arr[t] = 0.0
591
+ avg_arr[t] = 0.0
592
+ stop_arr[t] = 0.0
593
+ tp_arr[t] = 0.0
594
+ continue
595
+
596
+ if not funding_at_open and position != 0.0 and funding_mask[t]:
597
+ funding_cost = position * close_ref * contract_size * funding_rates[t]
598
+ equity -= funding_cost
599
+ funding_arr[t] = funding_cost
600
+ flags_arr[t] |= int(IntrabarEventFlag.FUNDING)
601
+
602
+ equity_arr[t] = equity
603
+ pos_arr[t] = position
604
+ avg_arr[t] = avg_entry
605
+ stop_arr[t] = 0.0 if not np.isfinite(active_stop) else active_stop
606
+ tp_arr[t] = 0.0 if not np.isfinite(active_tp) else active_tp
607
+
608
+ if contract.close_on_last_bar and position != 0.0:
609
+ t = n - 1
610
+ side = -1 if position > 0.0 else 1
611
+ price = _market_price(float(closes[t]), side, slippage_rate, tick_size=tick_size)
612
+ fee = abs(position) * price * contract_size * fee_rate
613
+ equity += position * (price - float(closes[t])) * contract_size - fee
614
+ fee_arr[t] += fee
615
+ fills.append(_fill(t, 99, idx[t], side, abs(position), price, fee, IntrabarFillReason.FINAL_CLOSE))
616
+ position = 0.0
617
+ equity_arr[t] = equity
618
+ pos_arr[t] = 0.0
619
+ avg_arr[t] = 0.0
620
+ stop_arr[t] = 0.0
621
+ tp_arr[t] = 0.0
622
+
623
+ return IntrabarReferenceResult(
624
+ equity=pd.Series(equity_arr, index=idx, name="equity"),
625
+ position=pd.Series(pos_arr, index=idx, name=f"Position_{tape.symbols[0]}"),
626
+ average_entry=pd.Series(avg_arr, index=idx, name="average_entry"),
627
+ active_stop=pd.Series(stop_arr, index=idx, name="active_stop"),
628
+ active_take_profit=pd.Series(tp_arr, index=idx, name="active_take_profit"),
629
+ fees=pd.Series(fee_arr, index=idx, name="fees"),
630
+ funding=pd.Series(funding_arr, index=idx, name="funding"),
631
+ event_flags=pd.Series(flags_arr, index=idx, name="event_flags"),
632
+ fills=tuple(fills),
633
+ ambiguity_count=int(ambiguity_count),
634
+ rejected_count=int(rejected_count),
635
+ liquidated=bool(liquidated),
636
+ liquidation_bar=int(liquidation_bar),
637
+ metadata={
638
+ "engine": "intrabar_reference_v1",
639
+ "engine_id": "intrabar_reference_v1",
640
+ "execution_contract": contract.to_metadata(),
641
+ "data_signature": tape.signature,
642
+ "fill_count": len(fills),
643
+ "ambiguity_count": int(ambiguity_count),
644
+ "rejected_count": int(rejected_count),
645
+ "liquidated": bool(liquidated),
646
+ "liquidation_bar": int(liquidation_bar),
647
+ "oracle": True,
648
+ "funding_timing_certified": True,
649
+ "funding_event_alignment": "exact_bar_timestamp",
650
+ "bar_timestamp_semantics": timestamp_semantics,
651
+ "funding_event_price_reference": "open" if funding_at_open else "close",
652
+ "sizing_mode": sizing_code.value,
653
+ "quantity_constraints": {
654
+ "qty_step": float(qty_step),
655
+ "min_qty": float(min_qty),
656
+ "min_notional": float(min_notional),
657
+ "tick_size": float(tick_size),
658
+ },
659
+ **(
660
+ {
661
+ "session_execution_enabled": True,
662
+ "session_policy": session_policy.to_metadata(),
663
+ "session_tape_signature": session_tape.signature,
664
+ "session_reset_count": int(session_reset_count),
665
+ "session_forced_exit_count": int(session_forced_exit_count),
666
+ "entry_window_blocked_count": int(entry_window_blocked_count),
667
+ "long_quota_blocked_count": int(long_quota_blocked_count),
668
+ "short_quota_blocked_count": int(short_quota_blocked_count),
669
+ "flat_only_blocked_count": int(flat_only_blocked_count),
670
+ "stale_session_signal_count": int(stale_session_signal_count),
671
+ "reentry_suppressed_count": int(reentry_suppressed_count),
672
+ }
673
+ if session_enabled
674
+ else {"session_execution_enabled": False}
675
+ ),
676
+ },
677
+ )
678
+
679
+
680
+ def _optional_float_array(value) -> Optional[np.ndarray]:
681
+ if value is None:
682
+ return None
683
+ return np.ascontiguousarray(value, dtype=np.float64)
684
+
685
+
686
+ def _optional_bool_array(value) -> Optional[np.ndarray]:
687
+ if value is None:
688
+ return None
689
+ return np.ascontiguousarray(value, dtype=np.bool_)
690
+
691
+
692
+ def _fill(bar, seq, ts, side, qty, price, fee, reason) -> IntrabarFill:
693
+ return IntrabarFill(
694
+ bar_index=int(bar),
695
+ sequence=int(seq),
696
+ timestamp=pd.Timestamp(ts),
697
+ side=int(side),
698
+ qty=float(qty),
699
+ price=float(price),
700
+ fee=float(fee),
701
+ reason=reason,
702
+ )
703
+
704
+
705
+ def _market_price(open_price: float, side: int, slippage_rate: float, *, tick_size: float = 0.0) -> float:
706
+ raw = float(open_price * (1.0 + slippage_rate if side > 0 else 1.0 - slippage_rate))
707
+ return _quantize_price(raw, side, tick_size)
708
+
709
+
710
+ def _has_initial_margin(equity: float, qty: float, price: float, contract_size: float, leverage: float, margin_buffer: float) -> bool:
711
+ required = abs(qty) * price * contract_size / leverage
712
+ return bool(equity >= required * (1.0 + margin_buffer))
713
+
714
+
715
+ def _maintenance_breached(equity: float, position: float, price: float, contract_size: float, maintenance_ratio: float) -> bool:
716
+ maintenance = abs(position) * price * contract_size * maintenance_ratio
717
+ return bool(maintenance > 0.0 and equity <= maintenance)
718
+
719
+
720
+ def _maintenance_breached_at_worst(
721
+ equity: float,
722
+ position: float,
723
+ reference_price: float,
724
+ *,
725
+ high: float,
726
+ low: float,
727
+ contract_size: float,
728
+ maintenance_ratio: float,
729
+ ) -> bool:
730
+ worst = low if position > 0.0 else high
731
+ worst_equity = equity + position * (worst - reference_price) * contract_size
732
+ maintenance = abs(position) * worst * contract_size * maintenance_ratio
733
+ return bool(maintenance > 0.0 and worst_equity <= maintenance)
734
+
735
+
736
+ def _initial_bracket(intent: IntrabarIntentTape, signal_bar: int, side: int, fill_price: float, *, tick_size: float = 0.0) -> tuple[float, float]:
737
+ stop = np.nan
738
+ tp = np.nan
739
+ if intent.stop_value is not None and np.isfinite(intent.stop_value[signal_bar]) and intent.stop_value[signal_bar] > 0.0:
740
+ stop = _level_price(fill_price, side, float(intent.stop_value[signal_bar]), intent.level_mode, is_stop=True, tick_size=tick_size)
741
+ if (
742
+ intent.take_profit_value is not None
743
+ and np.isfinite(intent.take_profit_value[signal_bar])
744
+ and intent.take_profit_value[signal_bar] > 0.0
745
+ ):
746
+ tp = _level_price(fill_price, side, float(intent.take_profit_value[signal_bar]), intent.level_mode, is_stop=False, tick_size=tick_size)
747
+ if intent.trailing_value is not None and np.isfinite(intent.trailing_value[signal_bar]) and intent.trailing_value[signal_bar] > 0.0:
748
+ trailing_stop = _level_price(fill_price, side, float(intent.trailing_value[signal_bar]), intent.level_mode, is_stop=True, tick_size=tick_size)
749
+ stop = trailing_stop if not np.isfinite(stop) else (max(stop, trailing_stop) if side > 0 else min(stop, trailing_stop))
750
+ return stop, tp
751
+
752
+
753
+ def _level_price(price: float, side: int, value: float, mode: IntrabarLevelMode, *, is_stop: bool, tick_size: float = 0.0) -> float:
754
+ direction = -1.0 if (side > 0 and is_stop) or (side < 0 and not is_stop) else 1.0
755
+ if mode is IntrabarLevelMode.ABSOLUTE_PRICE:
756
+ return _quantize_price(float(value), -side, tick_size)
757
+ if mode is IntrabarLevelMode.PRICE_DISTANCE:
758
+ return _quantize_price(float(price + direction * value), -side, tick_size)
759
+ if mode is IntrabarLevelMode.PERCENT_DISTANCE:
760
+ return _quantize_price(float(price * (1.0 + direction * value)), -side, tick_size)
761
+ raise NotImplementedError(f"unsupported level mode={mode!r}")
762
+
763
+
764
+ def _resolve_intrabar_exit(
765
+ *,
766
+ side: int,
767
+ open_price: float,
768
+ high: float,
769
+ low: float,
770
+ stop_price: float,
771
+ tp_price: float,
772
+ same_bar_policy: IntrabarSameBarPolicy,
773
+ take_profit_gap_policy: TakeProfitGapPolicy,
774
+ slippage_rate: float,
775
+ tick_size: float = 0.0,
776
+ ):
777
+ has_stop = np.isfinite(stop_price) and stop_price > 0.0
778
+ has_tp = np.isfinite(tp_price) and tp_price > 0.0
779
+ if side > 0:
780
+ stop_hit = has_stop and low <= stop_price
781
+ tp_hit = has_tp and high >= tp_price
782
+ stop_gap = has_stop and open_price <= stop_price
783
+ tp_gap = has_tp and open_price >= tp_price
784
+ exit_side = -1
785
+ else:
786
+ stop_hit = has_stop and high >= stop_price
787
+ tp_hit = has_tp and low <= tp_price
788
+ stop_gap = has_stop and open_price >= stop_price
789
+ tp_gap = has_tp and open_price <= tp_price
790
+ exit_side = 1
791
+ if not stop_hit and not tp_hit:
792
+ return None
793
+ ambiguous = bool(stop_hit and tp_hit)
794
+ if ambiguous and same_bar_policy is IntrabarSameBarPolicy.REJECT_AMBIGUOUS:
795
+ raise ValueError("same bar stop/take-profit ambiguity requires lower timeframe or explicit policy")
796
+ stop_first = same_bar_policy in {
797
+ IntrabarSameBarPolicy.CONSERVATIVE,
798
+ IntrabarSameBarPolicy.STOP_FIRST,
799
+ IntrabarSameBarPolicy.OLHC_PATH if side > 0 else IntrabarSameBarPolicy.OHLC_PATH,
800
+ }
801
+ if stop_hit and (not tp_hit or stop_first):
802
+ price = open_price if stop_gap else stop_price
803
+ price = _market_price(float(price), exit_side, slippage_rate, tick_size=tick_size)
804
+ return exit_side, price, IntrabarFillReason.STOP_LOSS, ambiguous
805
+ if tp_hit:
806
+ if tp_gap and take_profit_gap_policy is TakeProfitGapPolicy.OPEN_PRICE_IMPROVEMENT:
807
+ price = open_price
808
+ else:
809
+ price = tp_price
810
+ return exit_side, _quantize_price(float(price), exit_side, tick_size), IntrabarFillReason.TAKE_PROFIT, ambiguous
811
+ return None
812
+
813
+
814
+ def _update_trailing(intent: IntrabarIntentTape, signal_bar: int, position: float, close_price: float, current_stop: float, *, tick_size: float = 0.0) -> float:
815
+ if intent.trailing_value is None:
816
+ return current_stop
817
+ value = float(intent.trailing_value[signal_bar])
818
+ if not np.isfinite(value) or value <= 0.0:
819
+ return current_stop
820
+ side = 1 if position > 0.0 else -1
821
+ candidate = _level_price(close_price, side, value, intent.level_mode, is_stop=True, tick_size=tick_size)
822
+ if not np.isfinite(current_stop):
823
+ return candidate
824
+ return max(current_stop, candidate) if side > 0 else min(current_stop, candidate)
825
+
826
+
827
+ def _pending_exit(intent: IntrabarIntentTape, signal_bar: int, position: float) -> bool:
828
+ if position > 0.0 and intent.exit_long is not None:
829
+ return bool(intent.exit_long[signal_bar])
830
+ if position < 0.0 and intent.exit_short is not None:
831
+ return bool(intent.exit_short[signal_bar])
832
+ if intent.technical_exit is not None:
833
+ return bool(intent.technical_exit[signal_bar])
834
+ return False
835
+
836
+
837
+ def _compile_entry_quantity(
838
+ *,
839
+ size_weight: float,
840
+ fill_price: float,
841
+ equity: float,
842
+ contract_size: float,
843
+ sizing_mode: IntrabarSizingMode,
844
+ fixed_notional: float,
845
+ equity_fraction: float,
846
+ risk_fraction: float,
847
+ stop_value: Optional[float],
848
+ level_mode: IntrabarLevelMode,
849
+ side: int,
850
+ tick_size: float = 0.0,
851
+ ) -> float:
852
+ weight = abs(float(size_weight))
853
+ if sizing_mode is IntrabarSizingMode.UNITS:
854
+ return weight
855
+ if sizing_mode is IntrabarSizingMode.FIXED_NOTIONAL:
856
+ notional = float(fixed_notional) * weight
857
+ return notional / (fill_price * contract_size) if fill_price > 0.0 and contract_size > 0.0 else 0.0
858
+ if sizing_mode is IntrabarSizingMode.PCT_EQUITY:
859
+ notional = float(equity) * float(equity_fraction) * weight
860
+ return notional / (fill_price * contract_size) if fill_price > 0.0 and contract_size > 0.0 else 0.0
861
+ if sizing_mode is IntrabarSizingMode.RISK_PER_TRADE:
862
+ if stop_value is None or not np.isfinite(stop_value) or stop_value <= 0.0:
863
+ return 0.0
864
+ stop_price = _level_price(fill_price, side, float(stop_value), level_mode, is_stop=True, tick_size=tick_size)
865
+ stop_distance = abs(fill_price - stop_price)
866
+ risk_budget = float(equity) * float(risk_fraction) * weight
867
+ return risk_budget / (stop_distance * contract_size) if stop_distance > 0.0 and contract_size > 0.0 else 0.0
868
+ raise NotImplementedError(f"unsupported intrabar sizing_mode={sizing_mode!r}")
869
+
870
+
871
+ def _quantize_price(price: float, side: int, tick_size: float) -> float:
872
+ tick = float(tick_size)
873
+ if tick <= 0.0 or not np.isfinite(price):
874
+ return float(price)
875
+ if side > 0:
876
+ return float(np.ceil((float(price) / tick) - 1e-12) * tick)
877
+ return float(np.floor((float(price) / tick) + 1e-12) * tick)
878
+
879
+
880
+ def _validate_intrabar_contract_supported(contract: ExecutionContract) -> None:
881
+ from .execution_contract import (
882
+ AmbiguityPolicy,
883
+ FillPhase,
884
+ FundingPhase,
885
+ LiquidationPriority,
886
+ MarketFillPolicy,
887
+ SignalPhase,
888
+ StopGapPolicy,
889
+ TrailingUpdatePhase,
890
+ )
891
+
892
+ if contract.signal_phase is not SignalPhase.BAR_CLOSE:
893
+ raise NotImplementedError("intrabar_bracket_v1 supports signal_phase=bar_close only")
894
+ if contract.entry_fill_phase is not FillPhase.NEXT_OPEN:
895
+ raise NotImplementedError("intrabar_bracket_v1 supports entry_fill_phase=next_open only")
896
+ if contract.market_fill_policy is not MarketFillPolicy.NEXT_OPEN:
897
+ raise NotImplementedError("intrabar_bracket_v1 supports market_fill_policy=next_open only")
898
+ if contract.stop_gap_policy is not StopGapPolicy.OPEN_WORSE_THAN_TRIGGER:
899
+ raise NotImplementedError("intrabar_bracket_v1 supports stop_gap_policy=open_worse_than_trigger only")
900
+ if contract.trailing_update_phase is not TrailingUpdatePhase.NEXT_BAR:
901
+ raise NotImplementedError("intrabar_bracket_v1 supports trailing_update_phase=next_bar only")
902
+ if contract.funding_phase is not FundingPhase.POSITION_AT_EVENT:
903
+ raise NotImplementedError("intrabar_bracket_v1 supports funding_phase=position_at_event only")
904
+ if contract.liquidation_priority is not LiquidationPriority.LIQUIDATION_FIRST_AT_GAP:
905
+ raise NotImplementedError("intrabar_bracket_v1 supports liquidation_priority=liquidation_first_at_gap only")
906
+ if contract.ambiguity_policy not in {AmbiguityPolicy.FLAG_AND_CONSERVATIVE, AmbiguityPolicy.REJECT}:
907
+ raise NotImplementedError("intrabar_bracket_v1 supports ambiguity_policy flag_and_conservative or reject only")