quantbt-engine 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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")
|