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