quantbt-engine 1.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. quantbt/__init__.py +862 -0
  2. quantbt/adapters/__init__.py +5 -0
  3. quantbt/adapters/nautilus/__init__.py +42 -0
  4. quantbt/adapters/nautilus/_dependency.py +53 -0
  5. quantbt/adapters/nautilus/backend.py +639 -0
  6. quantbt/adapters/nautilus/instruments.py +275 -0
  7. quantbt/adapters/nautilus/options.py +465 -0
  8. quantbt/adapters/nautilus/reports.py +231 -0
  9. quantbt/backends/__init__.py +29 -0
  10. quantbt/backends/_native_event_rust.py +2168 -0
  11. quantbt/backends/native_event.py +5048 -0
  12. quantbt/backends/native_option.py +796 -0
  13. quantbt/backends/native_portfolio.py +928 -0
  14. quantbt/backends/native_vectorized.py +1299 -0
  15. quantbt/backtester.py +485 -0
  16. quantbt/benchmarks/__init__.py +1 -0
  17. quantbt/benchmarks/compare_phase9_parity.py +302 -0
  18. quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
  19. quantbt/benchmarks/profile_phase7.py +415 -0
  20. quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
  21. quantbt/benchmarks/run_optimization_overhead.py +195 -0
  22. quantbt/benchmarks/run_options_engine.py +272 -0
  23. quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
  24. quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
  25. quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
  26. quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
  27. quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
  28. quantbt/benchmarks/run_phase14_service_loop.py +694 -0
  29. quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
  30. quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
  31. quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
  32. quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
  33. quantbt/benchmarks/run_phase31_intrabar.py +441 -0
  34. quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
  35. quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
  36. quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
  37. quantbt/benchmarks/run_phase7.py +654 -0
  38. quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
  39. quantbt/core/__init__.py +323 -0
  40. quantbt/core/arbitrage.py +767 -0
  41. quantbt/core/basket.py +234 -0
  42. quantbt/core/certification.py +274 -0
  43. quantbt/core/constraints.py +155 -0
  44. quantbt/core/engine.py +1144 -0
  45. quantbt/core/event.py +869 -0
  46. quantbt/core/execution_contract.py +197 -0
  47. quantbt/core/execution_depth.py +613 -0
  48. quantbt/core/intrabar_kernel.py +1906 -0
  49. quantbt/core/intrabar_reference.py +907 -0
  50. quantbt/core/intrabar_session.py +156 -0
  51. quantbt/core/market_tape.py +480 -0
  52. quantbt/core/native_event_capabilities.py +121 -0
  53. quantbt/core/native_event_parity.py +336 -0
  54. quantbt/core/order_compiler.py +422 -0
  55. quantbt/core/orders.py +319 -0
  56. quantbt/core/portfolio.py +392 -0
  57. quantbt/core/preprocessor.py +256 -0
  58. quantbt/core/reactive.py +173 -0
  59. quantbt/core/results.py +341 -0
  60. quantbt/core/schema.py +211 -0
  61. quantbt/core/scopes.py +96 -0
  62. quantbt/core/structured_orders.py +378 -0
  63. quantbt/core/types.py +87 -0
  64. quantbt/core/vectorized.py +203 -0
  65. quantbt/endpoint.py +4465 -0
  66. quantbt/engines.py +1014 -0
  67. quantbt/metrics/__init__.py +45 -0
  68. quantbt/metrics/options_analytics.py +46 -0
  69. quantbt/metrics/performance.py +541 -0
  70. quantbt/optimization/__init__.py +94 -0
  71. quantbt/optimization/callbacks.py +116 -0
  72. quantbt/optimization/candidate_selection.py +397 -0
  73. quantbt/optimization/config.py +84 -0
  74. quantbt/optimization/constraints.py +22 -0
  75. quantbt/optimization/evaluator.py +21 -0
  76. quantbt/optimization/evaluators/__init__.py +32 -0
  77. quantbt/optimization/evaluators/arbitrage.py +22 -0
  78. quantbt/optimization/evaluators/generic.py +34 -0
  79. quantbt/optimization/evaluators/grid_dca.py +22 -0
  80. quantbt/optimization/evaluators/intrabar.py +54 -0
  81. quantbt/optimization/evaluators/native_event.py +51 -0
  82. quantbt/optimization/evaluators/options.py +22 -0
  83. quantbt/optimization/evaluators/portfolio.py +42 -0
  84. quantbt/optimization/evaluators/signal.py +43 -0
  85. quantbt/optimization/multiseed.py +176 -0
  86. quantbt/optimization/objectives.py +221 -0
  87. quantbt/optimization/optimizer.py +500 -0
  88. quantbt/optimization/result.py +80 -0
  89. quantbt/optimization/samplers.py +84 -0
  90. quantbt/optimization/space.py +223 -0
  91. quantbt/options/__init__.py +215 -0
  92. quantbt/options/cache.py +116 -0
  93. quantbt/options/conventions.py +158 -0
  94. quantbt/options/data.py +177 -0
  95. quantbt/options/execution.py +600 -0
  96. quantbt/options/fees.py +135 -0
  97. quantbt/options/greeks.py +173 -0
  98. quantbt/options/hedging.py +232 -0
  99. quantbt/options/iv.py +188 -0
  100. quantbt/options/ledger.py +263 -0
  101. quantbt/options/lifecycle.py +94 -0
  102. quantbt/options/margin.py +311 -0
  103. quantbt/options/packages.py +147 -0
  104. quantbt/options/pricing.py +191 -0
  105. quantbt/options/schema.py +227 -0
  106. quantbt/options/selectors.py +281 -0
  107. quantbt/options/strategy.py +249 -0
  108. quantbt/options/surface.py +142 -0
  109. quantbt/options/tape.py +226 -0
  110. quantbt/options/templates/__init__.py +33 -0
  111. quantbt/options/templates/packages.py +354 -0
  112. quantbt/portfolio.py +606 -0
  113. quantbt/py.typed +1 -0
  114. quantbt/reporting/__init__.py +38 -0
  115. quantbt/reporting/arbitrage_audit.py +211 -0
  116. quantbt/reporting/nautilus_bundle.py +758 -0
  117. quantbt/reporting/nautilus_certification.py +226 -0
  118. quantbt/reporting/nautilus_diagnostics.py +225 -0
  119. quantbt/reporting/parity.py +496 -0
  120. quantbt/reporting/portfolio_audit.py +206 -0
  121. quantbt/reporting/portfolio_nautilus.py +140 -0
  122. quantbt/sizing/__init__.py +3 -0
  123. quantbt/sizing/fast.py +67 -0
  124. quantbt/sizing/modes.py +162 -0
  125. quantbt/viz/__init__.py +4 -0
  126. quantbt/viz/plots.py +310 -0
  127. quantbt/viz/themes.py +102 -0
  128. quantbt/walkforward.py +3136 -0
  129. quantbt_engine-1.0.7.dist-info/METADATA +891 -0
  130. quantbt_engine-1.0.7.dist-info/RECORD +133 -0
  131. quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
  132. quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
  133. quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
quantbt/core/event.py ADDED
@@ -0,0 +1,869 @@
1
+ """
2
+ quantbt.core.event
3
+ ------------------
4
+ Numba kernels for the native event-driven backend.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ from numba import njit
11
+
12
+
13
+ ORDER_STATUS_PENDING = 0
14
+ ORDER_STATUS_FILLED = 1
15
+ ORDER_STATUS_CANCELED = 2
16
+ ORDER_STATUS_REJECTED = 3
17
+
18
+ ORDER_TYPE_MARKET = 0
19
+ ORDER_TYPE_LIMIT = 1
20
+ ORDER_TYPE_STOP_MARKET = 2
21
+ ORDER_TYPE_STOP_LIMIT = 3
22
+
23
+ TIF_GTC = 0
24
+ TIF_IOC = 1
25
+ TIF_FOK = 2
26
+ TIF_GTD = 3
27
+
28
+ SIDE_BUY = 1
29
+ SIDE_SELL = -1
30
+
31
+ REJECT_NONE = 0
32
+ REJECT_INSUFFICIENT_MARGIN = 1
33
+ REJECT_UNSUPPORTED_ORDER_TYPE = 2
34
+ REJECT_UNKNOWN_ORDER = 3
35
+ REJECT_INVALID_AMEND = 4
36
+ REJECT_REDUCE_ONLY_NO_POSITION = 5
37
+ REJECT_UNSUPPORTED_ACTION = 6
38
+
39
+ LIQ_NONE = 0
40
+ LIQ_INTRABAR = 1
41
+ LIQ_AFTER_FUNDING = 2
42
+ LIQ_AFTER_ORDER = 3
43
+
44
+ COMMAND_ACTION_PLACE = 0
45
+ COMMAND_ACTION_CANCEL = 1
46
+ COMMAND_ACTION_REPLACE = 2
47
+ COMMAND_ACTION_AMEND = 3
48
+ COMMAND_ACTION_CANCEL_ALL = 4
49
+
50
+ ACTIVATION_IMMEDIATE = 0
51
+ ACTIVATION_ON_PARENT_FIRST_FILL = 1
52
+ ACTIVATION_ON_PARENT_FULL_FILL = 2
53
+
54
+ ORDER_EVENT_PLACE = 0
55
+ ORDER_EVENT_CANCEL = 1
56
+ ORDER_EVENT_REPLACE = 2
57
+ ORDER_EVENT_AMEND = 3
58
+ ORDER_EVENT_FILL = 4
59
+ ORDER_EVENT_EXPIRE = 5
60
+ ORDER_EVENT_ACTIVATE = 6
61
+ ORDER_EVENT_REJECT = 7
62
+
63
+
64
+ @njit(cache=True)
65
+ def _event_close_margin(
66
+ n_syms: int,
67
+ current_pos: np.ndarray,
68
+ closes: np.ndarray,
69
+ contract_sizes: np.ndarray,
70
+ leverages: np.ndarray,
71
+ maint_ratio: float,
72
+ i: int,
73
+ ):
74
+ init_margin = 0.0
75
+ maint_margin = 0.0
76
+ for s in range(n_syms):
77
+ p = current_pos[s]
78
+ if p != 0.0:
79
+ notional = abs(p) * closes[i, s] * contract_sizes[s]
80
+ init_margin += notional / leverages[s]
81
+ maint_margin += notional * maint_ratio
82
+ return init_margin, maint_margin
83
+
84
+
85
+ @njit(cache=True)
86
+ def _event_liquidated(
87
+ n_syms: int,
88
+ equity: float,
89
+ current_pos: np.ndarray,
90
+ highs: np.ndarray,
91
+ lows: np.ndarray,
92
+ closes: np.ndarray,
93
+ contract_sizes: np.ndarray,
94
+ maint_ratio: float,
95
+ i: int,
96
+ ):
97
+ worst_equity = equity
98
+ worst_mm = 0.0
99
+ for s in range(n_syms):
100
+ p = current_pos[s]
101
+ if p == 0.0:
102
+ continue
103
+ worst_p = lows[i, s] if p > 0.0 else highs[i, s]
104
+ worst_equity += p * (worst_p - closes[i, s]) * contract_sizes[s]
105
+ worst_mm += abs(p) * worst_p * contract_sizes[s] * maint_ratio
106
+ return worst_mm > 0.0 and worst_equity <= worst_mm
107
+
108
+
109
+ @njit(cache=True)
110
+ def _engine_event_v1(
111
+ n_bars: int,
112
+ n_syms: int,
113
+ n_orders: int,
114
+ order_ptr: np.ndarray,
115
+ order_symbol: np.ndarray,
116
+ order_side: np.ndarray,
117
+ order_type: np.ndarray,
118
+ order_qty: np.ndarray,
119
+ order_price: np.ndarray,
120
+ order_tif: np.ndarray,
121
+ highs: np.ndarray,
122
+ lows: np.ndarray,
123
+ closes: np.ndarray,
124
+ funding_rates: np.ndarray,
125
+ is_funding_bar: np.ndarray,
126
+ init_capital: float,
127
+ leverages: np.ndarray,
128
+ maint_ratio: float,
129
+ fee_rates: np.ndarray,
130
+ contract_sizes: np.ndarray,
131
+ slippage: float,
132
+ use_funding: bool,
133
+ ):
134
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
135
+ pos_out = np.zeros((n_bars, n_syms), dtype=np.float64)
136
+ fee_arr = np.zeros(n_bars, dtype=np.float64)
137
+ turnover_arr = np.zeros(n_bars, dtype=np.float64)
138
+ funding_arr = np.zeros(n_bars, dtype=np.float64)
139
+ init_margin = np.zeros(n_bars, dtype=np.float64)
140
+ maint_margin = np.zeros(n_bars, dtype=np.float64)
141
+ rejected_bar = np.zeros(n_bars, dtype=np.int64)
142
+ canceled_bar = np.zeros(n_bars, dtype=np.int64)
143
+
144
+ order_status = np.full(n_orders, ORDER_STATUS_PENDING, dtype=np.int64)
145
+ reject_code = np.zeros(n_orders, dtype=np.int64)
146
+ fill_bar = np.full(n_orders, -1, dtype=np.int64)
147
+ fill_qty = np.zeros(n_orders, dtype=np.float64)
148
+ fill_price = np.zeros(n_orders, dtype=np.float64)
149
+ fill_fee = np.zeros(n_orders, dtype=np.float64)
150
+
151
+ pending_ids = np.zeros(n_orders, dtype=np.int64)
152
+ pending_count = 0
153
+
154
+ current_pos = np.zeros(n_syms, dtype=np.float64)
155
+ equity = init_capital
156
+ liq_flag = False
157
+ liq_idx = -1
158
+ liq_reason = LIQ_NONE
159
+
160
+ equity_curve[0] = equity
161
+
162
+ for i in range(1, n_bars):
163
+ if liq_flag:
164
+ equity_curve[i] = 0.0
165
+ for s in range(n_syms):
166
+ pos_out[i, s] = 0.0
167
+ continue
168
+
169
+ # Mark carried positions to close.
170
+ for s in range(n_syms):
171
+ p = current_pos[s]
172
+ if p != 0.0:
173
+ equity += p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
174
+
175
+ if _event_liquidated(
176
+ n_syms, equity, current_pos, highs, lows, closes,
177
+ contract_sizes, maint_ratio, i
178
+ ):
179
+ liq_flag = True
180
+ liq_idx = i
181
+ liq_reason = LIQ_INTRABAR
182
+ equity = 0.0
183
+ for s in range(n_syms):
184
+ current_pos[s] = 0.0
185
+ pos_out[i, s] = 0.0
186
+ equity_curve[i] = 0.0
187
+ continue
188
+
189
+ if is_funding_bar[i] and use_funding:
190
+ for s in range(n_syms):
191
+ p = current_pos[s]
192
+ if p != 0.0:
193
+ cost = p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
194
+ equity -= cost
195
+ funding_arr[i] += cost
196
+
197
+ _, close_mm = _event_close_margin(
198
+ n_syms, current_pos, closes, contract_sizes, leverages, maint_ratio, i
199
+ )
200
+ if close_mm > 0.0 and equity <= close_mm:
201
+ liq_flag = True
202
+ liq_idx = i
203
+ liq_reason = LIQ_AFTER_FUNDING
204
+ equity = 0.0
205
+ for s in range(n_syms):
206
+ current_pos[s] = 0.0
207
+ pos_out[i, s] = 0.0
208
+ equity_curve[i] = 0.0
209
+ continue
210
+
211
+ # Activate orders submitted for this bar.
212
+ for k in range(order_ptr[i], order_ptr[i + 1]):
213
+ pending_ids[pending_count] = k
214
+ pending_count += 1
215
+
216
+ write_count = 0
217
+ for pidx in range(pending_count):
218
+ oid = pending_ids[pidx]
219
+ if order_status[oid] != ORDER_STATUS_PENDING:
220
+ continue
221
+
222
+ sym = order_symbol[oid]
223
+ side = order_side[oid]
224
+ otype = order_type[oid]
225
+ tif = order_tif[oid]
226
+
227
+ touched = False
228
+ exec_price = closes[i, sym]
229
+
230
+ if otype == ORDER_TYPE_MARKET:
231
+ touched = True
232
+ exec_price = closes[i, sym] * (1.0 + slippage if side > 0 else 1.0 - slippage)
233
+ elif otype == ORDER_TYPE_LIMIT:
234
+ limit_p = order_price[oid]
235
+ if side > 0 and lows[i, sym] <= limit_p:
236
+ touched = True
237
+ exec_price = limit_p
238
+ elif side < 0 and highs[i, sym] >= limit_p:
239
+ touched = True
240
+ exec_price = limit_p
241
+ else:
242
+ order_status[oid] = ORDER_STATUS_REJECTED
243
+ reject_code[oid] = REJECT_UNSUPPORTED_ORDER_TYPE
244
+ rejected_bar[i] += 1
245
+ continue
246
+
247
+ if not touched:
248
+ if tif == TIF_GTC:
249
+ pending_ids[write_count] = oid
250
+ write_count += 1
251
+ else:
252
+ order_status[oid] = ORDER_STATUS_CANCELED
253
+ canceled_bar[i] += 1
254
+ continue
255
+
256
+ qty = order_qty[oid]
257
+ delta = qty * side
258
+ cs = contract_sizes[sym]
259
+ c = closes[i, sym]
260
+ trade_notional = abs(delta) * exec_price * cs
261
+ fee_cost = trade_notional * fee_rates[sym]
262
+
263
+ cur_im, _ = _event_close_margin(
264
+ n_syms, current_pos, closes, contract_sizes, leverages, maint_ratio, i
265
+ )
266
+ old_im = abs(current_pos[sym]) * c * cs / leverages[sym]
267
+ new_im = abs(current_pos[sym] + delta) * exec_price * cs / leverages[sym]
268
+ margin_delta = new_im - old_im
269
+ required = fee_cost
270
+ if margin_delta > 0.0:
271
+ required += margin_delta
272
+
273
+ if required > equity - cur_im:
274
+ order_status[oid] = ORDER_STATUS_REJECTED
275
+ reject_code[oid] = REJECT_INSUFFICIENT_MARGIN
276
+ rejected_bar[i] += 1
277
+ continue
278
+
279
+ # Equity is marked at close; fill price creates same-bar PnL.
280
+ equity += delta * (c - exec_price) * cs - fee_cost
281
+ current_pos[sym] += delta
282
+
283
+ order_status[oid] = ORDER_STATUS_FILLED
284
+ fill_bar[oid] = i
285
+ fill_qty[oid] = qty
286
+ fill_price[oid] = exec_price
287
+ fill_fee[oid] = fee_cost
288
+ fee_arr[i] += fee_cost
289
+ turnover_arr[i] += trade_notional
290
+
291
+ pending_count = write_count
292
+
293
+ close_im, close_mm = _event_close_margin(
294
+ n_syms, current_pos, closes, contract_sizes, leverages, maint_ratio, i
295
+ )
296
+
297
+ if close_mm > 0.0 and equity <= close_mm:
298
+ liq_flag = True
299
+ liq_idx = i
300
+ liq_reason = LIQ_AFTER_ORDER
301
+ equity = 0.0
302
+ for s in range(n_syms):
303
+ current_pos[s] = 0.0
304
+ pos_out[i, s] = 0.0
305
+ equity_curve[i] = 0.0
306
+ continue
307
+
308
+ for s in range(n_syms):
309
+ pos_out[i, s] = current_pos[s]
310
+ init_margin[i] = close_im
311
+ maint_margin[i] = close_mm
312
+ equity_curve[i] = equity
313
+
314
+ return (
315
+ equity_curve,
316
+ pos_out,
317
+ fee_arr,
318
+ turnover_arr,
319
+ funding_arr,
320
+ init_margin,
321
+ maint_margin,
322
+ rejected_bar,
323
+ canceled_bar,
324
+ order_status,
325
+ reject_code,
326
+ fill_bar,
327
+ fill_qty,
328
+ fill_price,
329
+ fill_fee,
330
+ liq_flag,
331
+ liq_idx,
332
+ liq_reason,
333
+ )
334
+
335
+
336
+ @njit(cache=True)
337
+ def _record_order_event(
338
+ event_count: int,
339
+ event_bar: np.ndarray,
340
+ event_command: np.ndarray,
341
+ event_type: np.ndarray,
342
+ event_status: np.ndarray,
343
+ event_related_command: np.ndarray,
344
+ bar: int,
345
+ command_idx: int,
346
+ event_code: int,
347
+ status: int,
348
+ related_command_idx: int,
349
+ ):
350
+ if event_count < event_bar.shape[0]:
351
+ event_bar[event_count] = bar
352
+ event_command[event_count] = command_idx
353
+ event_type[event_count] = event_code
354
+ event_status[event_count] = status
355
+ event_related_command[event_count] = related_command_idx
356
+ return event_count + 1
357
+ return event_count
358
+
359
+
360
+ @njit(cache=True)
361
+ def _event_margin_required(
362
+ n_syms: int,
363
+ current_pos: np.ndarray,
364
+ closes: np.ndarray,
365
+ contract_sizes: np.ndarray,
366
+ leverages: np.ndarray,
367
+ maint_ratio: float,
368
+ i: int,
369
+ sym: int,
370
+ delta: float,
371
+ exec_price: float,
372
+ fee_cost: float,
373
+ ):
374
+ cur_im, _ = _event_close_margin(
375
+ n_syms, current_pos, closes, contract_sizes, leverages, maint_ratio, i
376
+ )
377
+ cs = contract_sizes[sym]
378
+ c = closes[i, sym]
379
+ old_im = abs(current_pos[sym]) * c * cs / leverages[sym]
380
+ new_im = abs(current_pos[sym] + delta) * exec_price * cs / leverages[sym]
381
+ margin_delta = new_im - old_im
382
+ required = fee_cost
383
+ if margin_delta > 0.0:
384
+ required += margin_delta
385
+ return required, cur_im
386
+
387
+
388
+ @njit(cache=True)
389
+ def _event_v2_touched_price(
390
+ otype: int,
391
+ side: int,
392
+ price: float,
393
+ trigger_price: float,
394
+ high: float,
395
+ low: float,
396
+ close: float,
397
+ slippage: float,
398
+ ):
399
+ touched = False
400
+ exec_price = close
401
+ if otype == ORDER_TYPE_MARKET:
402
+ touched = True
403
+ exec_price = close * (1.0 + slippage if side > 0 else 1.0 - slippage)
404
+ elif otype == ORDER_TYPE_LIMIT:
405
+ if side > 0 and low <= price:
406
+ touched = True
407
+ exec_price = price
408
+ elif side < 0 and high >= price:
409
+ touched = True
410
+ exec_price = price
411
+ elif otype == ORDER_TYPE_STOP_MARKET:
412
+ if side > 0 and high >= trigger_price:
413
+ touched = True
414
+ exec_price = trigger_price * (1.0 + slippage)
415
+ elif side < 0 and low <= trigger_price:
416
+ touched = True
417
+ exec_price = trigger_price * (1.0 - slippage)
418
+ elif otype == ORDER_TYPE_STOP_LIMIT:
419
+ if side > 0 and high >= trigger_price and low <= price:
420
+ touched = True
421
+ exec_price = price
422
+ elif side < 0 and low <= trigger_price and high >= price:
423
+ touched = True
424
+ exec_price = price
425
+ return touched, exec_price
426
+
427
+
428
+ @njit(cache=True)
429
+ def _engine_event_v2(
430
+ n_bars: int,
431
+ n_syms: int,
432
+ n_commands: int,
433
+ n_ids: int,
434
+ command_ptr: np.ndarray,
435
+ command_action: np.ndarray,
436
+ command_symbol: np.ndarray,
437
+ command_side: np.ndarray,
438
+ command_type: np.ndarray,
439
+ command_qty: np.ndarray,
440
+ command_price: np.ndarray,
441
+ command_trigger_price: np.ndarray,
442
+ command_tif: np.ndarray,
443
+ command_reduce_only: np.ndarray,
444
+ command_order_id: np.ndarray,
445
+ command_target_order_id: np.ndarray,
446
+ command_parent_order_id: np.ndarray,
447
+ command_group_id: np.ndarray,
448
+ command_oco_group_id: np.ndarray,
449
+ command_activation: np.ndarray,
450
+ command_expires_bar: np.ndarray,
451
+ highs: np.ndarray,
452
+ lows: np.ndarray,
453
+ closes: np.ndarray,
454
+ funding_rates: np.ndarray,
455
+ is_funding_bar: np.ndarray,
456
+ init_capital: float,
457
+ leverages: np.ndarray,
458
+ maint_ratio: float,
459
+ fee_rates: np.ndarray,
460
+ contract_sizes: np.ndarray,
461
+ slippage: float,
462
+ use_funding: bool,
463
+ ):
464
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
465
+ pos_out = np.zeros((n_bars, n_syms), dtype=np.float64)
466
+ fee_arr = np.zeros(n_bars, dtype=np.float64)
467
+ turnover_arr = np.zeros(n_bars, dtype=np.float64)
468
+ funding_arr = np.zeros(n_bars, dtype=np.float64)
469
+ init_margin = np.zeros(n_bars, dtype=np.float64)
470
+ maint_margin = np.zeros(n_bars, dtype=np.float64)
471
+ rejected_bar = np.zeros(n_bars, dtype=np.int64)
472
+ canceled_bar = np.zeros(n_bars, dtype=np.int64)
473
+
474
+ command_status = np.full(n_commands, ORDER_STATUS_PENDING, dtype=np.int64)
475
+ reject_code = np.zeros(n_commands, dtype=np.int64)
476
+ fill_bar = np.full(n_commands, -1, dtype=np.int64)
477
+ fill_qty = np.zeros(n_commands, dtype=np.float64)
478
+ fill_price = np.zeros(n_commands, dtype=np.float64)
479
+ fill_fee = np.zeros(n_commands, dtype=np.float64)
480
+
481
+ active = np.zeros(n_commands, dtype=np.int64)
482
+ waiting_parent = np.zeros(n_commands, dtype=np.int64)
483
+ working_qty = np.copy(command_qty)
484
+ working_price = np.copy(command_price)
485
+ working_trigger = np.copy(command_trigger_price)
486
+ id_to_slot = np.full(n_ids, -1, dtype=np.int64)
487
+
488
+ max_events = n_commands * 8 + n_bars
489
+ event_bar = np.full(max_events, -1, dtype=np.int64)
490
+ event_command = np.full(max_events, -1, dtype=np.int64)
491
+ event_type = np.full(max_events, -1, dtype=np.int64)
492
+ event_status = np.full(max_events, -1, dtype=np.int64)
493
+ event_related_command = np.full(max_events, -1, dtype=np.int64)
494
+ event_count = 0
495
+
496
+ current_pos = np.zeros(n_syms, dtype=np.float64)
497
+ equity = init_capital
498
+ liq_flag = False
499
+ liq_idx = -1
500
+ liq_reason = LIQ_NONE
501
+
502
+ equity_curve[0] = equity
503
+
504
+ for i in range(1, n_bars):
505
+ if liq_flag:
506
+ equity_curve[i] = 0.0
507
+ for s in range(n_syms):
508
+ pos_out[i, s] = 0.0
509
+ continue
510
+
511
+ for s in range(n_syms):
512
+ p = current_pos[s]
513
+ if p != 0.0:
514
+ equity += p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
515
+
516
+ if _event_liquidated(
517
+ n_syms, equity, current_pos, highs, lows, closes,
518
+ contract_sizes, maint_ratio, i
519
+ ):
520
+ liq_flag = True
521
+ liq_idx = i
522
+ liq_reason = LIQ_INTRABAR
523
+ equity = 0.0
524
+ for s in range(n_syms):
525
+ current_pos[s] = 0.0
526
+ pos_out[i, s] = 0.0
527
+ equity_curve[i] = 0.0
528
+ continue
529
+
530
+ if is_funding_bar[i] and use_funding:
531
+ for s in range(n_syms):
532
+ p = current_pos[s]
533
+ if p != 0.0:
534
+ cost = p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
535
+ equity -= cost
536
+ funding_arr[i] += cost
537
+
538
+ _, close_mm = _event_close_margin(
539
+ n_syms, current_pos, closes, contract_sizes, leverages, maint_ratio, i
540
+ )
541
+ if close_mm > 0.0 and equity <= close_mm:
542
+ liq_flag = True
543
+ liq_idx = i
544
+ liq_reason = LIQ_AFTER_FUNDING
545
+ equity = 0.0
546
+ for s in range(n_syms):
547
+ current_pos[s] = 0.0
548
+ pos_out[i, s] = 0.0
549
+ equity_curve[i] = 0.0
550
+ continue
551
+
552
+ # Expire active GTD orders before processing the current bar.
553
+ for oid in range(n_commands):
554
+ if active[oid] == 1 and command_status[oid] == ORDER_STATUS_PENDING:
555
+ exp_bar = command_expires_bar[oid]
556
+ if exp_bar >= 0 and i >= exp_bar:
557
+ active[oid] = 0
558
+ command_status[oid] = ORDER_STATUS_CANCELED
559
+ canceled_bar[i] += 1
560
+ event_count = _record_order_event(
561
+ event_count, event_bar, event_command, event_type,
562
+ event_status, event_related_command, i, oid,
563
+ ORDER_EVENT_EXPIRE, ORDER_STATUS_CANCELED, -1,
564
+ )
565
+
566
+ # Apply lifecycle commands submitted for this bar.
567
+ for k in range(command_ptr[i], command_ptr[i + 1]):
568
+ action = command_action[k]
569
+ if action == COMMAND_ACTION_PLACE:
570
+ oid_code = command_order_id[k]
571
+ if oid_code >= 0 and oid_code < n_ids:
572
+ id_to_slot[oid_code] = k
573
+ if command_activation[k] == ACTIVATION_IMMEDIATE:
574
+ active[k] = 1
575
+ else:
576
+ waiting_parent[k] = 1
577
+ event_count = _record_order_event(
578
+ event_count, event_bar, event_command, event_type,
579
+ event_status, event_related_command, i, k,
580
+ ORDER_EVENT_PLACE, ORDER_STATUS_PENDING, -1,
581
+ )
582
+ elif action == COMMAND_ACTION_REPLACE:
583
+ target_code = command_target_order_id[k]
584
+ target = -1
585
+ if target_code >= 0 and target_code < n_ids:
586
+ target = id_to_slot[target_code]
587
+ if target < 0 or command_status[target] != ORDER_STATUS_PENDING:
588
+ command_status[k] = ORDER_STATUS_REJECTED
589
+ reject_code[k] = REJECT_UNKNOWN_ORDER
590
+ rejected_bar[i] += 1
591
+ event_count = _record_order_event(
592
+ event_count, event_bar, event_command, event_type,
593
+ event_status, event_related_command, i, k,
594
+ ORDER_EVENT_REJECT, ORDER_STATUS_REJECTED, target,
595
+ )
596
+ else:
597
+ active[target] = 0
598
+ waiting_parent[target] = 0
599
+ command_status[target] = ORDER_STATUS_CANCELED
600
+ canceled_bar[i] += 1
601
+ oid_code = command_order_id[k]
602
+ if oid_code >= 0 and oid_code < n_ids:
603
+ id_to_slot[oid_code] = k
604
+ if target_code >= 0 and target_code < n_ids:
605
+ id_to_slot[target_code] = k
606
+ active[k] = 1
607
+ event_count = _record_order_event(
608
+ event_count, event_bar, event_command, event_type,
609
+ event_status, event_related_command, i, k,
610
+ ORDER_EVENT_REPLACE, ORDER_STATUS_PENDING, target,
611
+ )
612
+ elif action == COMMAND_ACTION_CANCEL:
613
+ target_code = command_target_order_id[k]
614
+ target = -1
615
+ if target_code >= 0 and target_code < n_ids:
616
+ target = id_to_slot[target_code]
617
+ if target < 0 or command_status[target] != ORDER_STATUS_PENDING:
618
+ command_status[k] = ORDER_STATUS_REJECTED
619
+ reject_code[k] = REJECT_UNKNOWN_ORDER
620
+ rejected_bar[i] += 1
621
+ event_count = _record_order_event(
622
+ event_count, event_bar, event_command, event_type,
623
+ event_status, event_related_command, i, k,
624
+ ORDER_EVENT_REJECT, ORDER_STATUS_REJECTED, target,
625
+ )
626
+ else:
627
+ active[target] = 0
628
+ waiting_parent[target] = 0
629
+ command_status[target] = ORDER_STATUS_CANCELED
630
+ command_status[k] = ORDER_STATUS_FILLED
631
+ canceled_bar[i] += 1
632
+ event_count = _record_order_event(
633
+ event_count, event_bar, event_command, event_type,
634
+ event_status, event_related_command, i, k,
635
+ ORDER_EVENT_CANCEL, ORDER_STATUS_FILLED, target,
636
+ )
637
+ elif action == COMMAND_ACTION_AMEND:
638
+ target_code = command_target_order_id[k]
639
+ target = -1
640
+ if target_code >= 0 and target_code < n_ids:
641
+ target = id_to_slot[target_code]
642
+ if target < 0 or command_status[target] != ORDER_STATUS_PENDING:
643
+ command_status[k] = ORDER_STATUS_REJECTED
644
+ reject_code[k] = REJECT_UNKNOWN_ORDER
645
+ rejected_bar[i] += 1
646
+ event_count = _record_order_event(
647
+ event_count, event_bar, event_command, event_type,
648
+ event_status, event_related_command, i, k,
649
+ ORDER_EVENT_REJECT, ORDER_STATUS_REJECTED, target,
650
+ )
651
+ else:
652
+ if command_qty[k] > 0.0:
653
+ working_qty[target] = command_qty[k]
654
+ if command_price[k] > 0.0:
655
+ working_price[target] = command_price[k]
656
+ if command_trigger_price[k] > 0.0:
657
+ working_trigger[target] = command_trigger_price[k]
658
+ command_status[k] = ORDER_STATUS_FILLED
659
+ event_count = _record_order_event(
660
+ event_count, event_bar, event_command, event_type,
661
+ event_status, event_related_command, i, k,
662
+ ORDER_EVENT_AMEND, ORDER_STATUS_FILLED, target,
663
+ )
664
+ elif action == COMMAND_ACTION_CANCEL_ALL:
665
+ for target in range(n_commands):
666
+ if (
667
+ (active[target] == 1 or waiting_parent[target] == 1)
668
+ and command_status[target] == ORDER_STATUS_PENDING
669
+ ):
670
+ if (
671
+ (command_symbol[k] < 0 or command_symbol[k] == command_symbol[target])
672
+ and (command_side[k] == 0 or command_side[k] == command_side[target])
673
+ and (command_type[k] < 0 or command_type[k] == command_type[target])
674
+ and (
675
+ command_parent_order_id[k] < 0
676
+ or command_parent_order_id[k] == command_parent_order_id[target]
677
+ )
678
+ and (command_group_id[k] < 0 or command_group_id[k] == command_group_id[target])
679
+ and (
680
+ command_oco_group_id[k] < 0
681
+ or command_oco_group_id[k] == command_oco_group_id[target]
682
+ )
683
+ ):
684
+ active[target] = 0
685
+ waiting_parent[target] = 0
686
+ command_status[target] = ORDER_STATUS_CANCELED
687
+ canceled_bar[i] += 1
688
+ command_status[k] = ORDER_STATUS_FILLED
689
+ event_count = _record_order_event(
690
+ event_count, event_bar, event_command, event_type,
691
+ event_status, event_related_command, i, k,
692
+ ORDER_EVENT_CANCEL, ORDER_STATUS_FILLED, -1,
693
+ )
694
+ else:
695
+ command_status[k] = ORDER_STATUS_REJECTED
696
+ reject_code[k] = REJECT_UNSUPPORTED_ACTION
697
+ rejected_bar[i] += 1
698
+
699
+ # Match active order slots. Children activated by an earlier parent fill
700
+ # can fill in the same bar if they appear later in command order.
701
+ for oid in range(n_commands):
702
+ if active[oid] != 1 or command_status[oid] != ORDER_STATUS_PENDING:
703
+ continue
704
+ action = command_action[oid]
705
+ if action != COMMAND_ACTION_PLACE and action != COMMAND_ACTION_REPLACE:
706
+ continue
707
+
708
+ sym = command_symbol[oid]
709
+ side = command_side[oid]
710
+ otype = command_type[oid]
711
+ tif = command_tif[oid]
712
+
713
+ touched, exec_price = _event_v2_touched_price(
714
+ otype, side, working_price[oid], working_trigger[oid],
715
+ highs[i, sym], lows[i, sym], closes[i, sym], slippage,
716
+ )
717
+
718
+ if not touched:
719
+ if tif == TIF_GTC or tif == TIF_GTD:
720
+ continue
721
+ active[oid] = 0
722
+ command_status[oid] = ORDER_STATUS_CANCELED
723
+ canceled_bar[i] += 1
724
+ event_count = _record_order_event(
725
+ event_count, event_bar, event_command, event_type,
726
+ event_status, event_related_command, i, oid,
727
+ ORDER_EVENT_CANCEL, ORDER_STATUS_CANCELED, -1,
728
+ )
729
+ continue
730
+
731
+ qty = working_qty[oid]
732
+ if command_reduce_only[oid] == 1:
733
+ current = current_pos[sym]
734
+ if current == 0.0 or (current > 0.0 and side > 0) or (current < 0.0 and side < 0):
735
+ active[oid] = 0
736
+ command_status[oid] = ORDER_STATUS_CANCELED
737
+ reject_code[oid] = REJECT_REDUCE_ONLY_NO_POSITION
738
+ canceled_bar[i] += 1
739
+ event_count = _record_order_event(
740
+ event_count, event_bar, event_command, event_type,
741
+ event_status, event_related_command, i, oid,
742
+ ORDER_EVENT_CANCEL, ORDER_STATUS_CANCELED, -1,
743
+ )
744
+ continue
745
+ max_reduce = abs(current)
746
+ if qty > max_reduce:
747
+ qty = max_reduce
748
+
749
+ delta = qty * side
750
+ cs = contract_sizes[sym]
751
+ c = closes[i, sym]
752
+ trade_notional = abs(delta) * exec_price * cs
753
+ fee_cost = trade_notional * fee_rates[sym]
754
+
755
+ required, cur_im = _event_margin_required(
756
+ n_syms, current_pos, closes, contract_sizes, leverages,
757
+ maint_ratio, i, sym, delta, exec_price, fee_cost,
758
+ )
759
+ if required > equity - cur_im:
760
+ active[oid] = 0
761
+ command_status[oid] = ORDER_STATUS_REJECTED
762
+ reject_code[oid] = REJECT_INSUFFICIENT_MARGIN
763
+ rejected_bar[i] += 1
764
+ event_count = _record_order_event(
765
+ event_count, event_bar, event_command, event_type,
766
+ event_status, event_related_command, i, oid,
767
+ ORDER_EVENT_REJECT, ORDER_STATUS_REJECTED, -1,
768
+ )
769
+ continue
770
+
771
+ equity += delta * (c - exec_price) * cs - fee_cost
772
+ current_pos[sym] += delta
773
+
774
+ active[oid] = 0
775
+ command_status[oid] = ORDER_STATUS_FILLED
776
+ fill_bar[oid] = i
777
+ fill_qty[oid] = qty
778
+ fill_price[oid] = exec_price
779
+ fill_fee[oid] = fee_cost
780
+ fee_arr[i] += fee_cost
781
+ turnover_arr[i] += trade_notional
782
+ event_count = _record_order_event(
783
+ event_count, event_bar, event_command, event_type,
784
+ event_status, event_related_command, i, oid,
785
+ ORDER_EVENT_FILL, ORDER_STATUS_FILLED, -1,
786
+ )
787
+
788
+ order_id = command_order_id[oid]
789
+ for child in range(n_commands):
790
+ if waiting_parent[child] == 1 and command_parent_order_id[child] == order_id:
791
+ if (
792
+ command_activation[child] == ACTIVATION_ON_PARENT_FIRST_FILL
793
+ or command_activation[child] == ACTIVATION_ON_PARENT_FULL_FILL
794
+ ):
795
+ waiting_parent[child] = 0
796
+ active[child] = 1
797
+ event_count = _record_order_event(
798
+ event_count, event_bar, event_command, event_type,
799
+ event_status, event_related_command, i, child,
800
+ ORDER_EVENT_ACTIVATE, ORDER_STATUS_PENDING, oid,
801
+ )
802
+
803
+ oco_group = command_oco_group_id[oid]
804
+ if oco_group >= 0:
805
+ for sibling in range(n_commands):
806
+ if sibling != oid and active[sibling] == 1 and command_status[sibling] == ORDER_STATUS_PENDING:
807
+ if command_oco_group_id[sibling] == oco_group:
808
+ active[sibling] = 0
809
+ waiting_parent[sibling] = 0
810
+ command_status[sibling] = ORDER_STATUS_CANCELED
811
+ canceled_bar[i] += 1
812
+ event_count = _record_order_event(
813
+ event_count, event_bar, event_command, event_type,
814
+ event_status, event_related_command, i, sibling,
815
+ ORDER_EVENT_CANCEL, ORDER_STATUS_CANCELED, oid,
816
+ )
817
+
818
+ close_im, close_mm = _event_close_margin(
819
+ n_syms, current_pos, closes, contract_sizes, leverages, maint_ratio, i
820
+ )
821
+
822
+ if close_mm > 0.0 and equity <= close_mm:
823
+ liq_flag = True
824
+ liq_idx = i
825
+ liq_reason = LIQ_AFTER_ORDER
826
+ equity = 0.0
827
+ for s in range(n_syms):
828
+ current_pos[s] = 0.0
829
+ pos_out[i, s] = 0.0
830
+ equity_curve[i] = 0.0
831
+ continue
832
+
833
+ for s in range(n_syms):
834
+ pos_out[i, s] = current_pos[s]
835
+ init_margin[i] = close_im
836
+ maint_margin[i] = close_mm
837
+ equity_curve[i] = equity
838
+
839
+ return (
840
+ equity_curve,
841
+ pos_out,
842
+ fee_arr,
843
+ turnover_arr,
844
+ funding_arr,
845
+ init_margin,
846
+ maint_margin,
847
+ rejected_bar,
848
+ canceled_bar,
849
+ command_status,
850
+ reject_code,
851
+ fill_bar,
852
+ fill_qty,
853
+ fill_price,
854
+ fill_fee,
855
+ active,
856
+ waiting_parent,
857
+ working_qty,
858
+ working_price,
859
+ working_trigger,
860
+ event_count,
861
+ event_bar,
862
+ event_command,
863
+ event_type,
864
+ event_status,
865
+ event_related_command,
866
+ liq_flag,
867
+ liq_idx,
868
+ liq_reason,
869
+ )