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/engine.py ADDED
@@ -0,0 +1,1144 @@
1
+ """
2
+ quantbt.core.engine
3
+ -------------------
4
+ Numba-compiled simulation kernels.
5
+
6
+ Kernel entry points
7
+ ~~~~~~~~~~~~~~~~~~~
8
+ _engine_units signals are pre-scaled target units (notional / unit / signal_notional)
9
+ _engine_pct_equity signals are raw weight fractions; units derived from live equity each bar
10
+ _engine_dca_ladder structural DCA/grid level with High/Low limit fills
11
+ _engine_portfolio multi-symbol portfolio loop with cross-margin buying-power gate
12
+
13
+ Simulation contract
14
+ ~~~~~~~~~~~~~~~~~~~
15
+ equity realised + unrealised MTM, updated close-to-close every bar
16
+ liquidation intrabar worst-case: Low for longs, High for shorts
17
+ maintenance_margin abs(pos) × price × cs × mm_rate (Binance notional-based formula)
18
+ funding fires once per is_funding_bar=True bar (caller marks the FIRST bar of each 8h window)
19
+ fee_rate ONE-WAY rate; caller passes fee/2 if fee is round-trip
20
+ slippage fraction applied at execution price; always a cost
21
+ """
22
+
23
+ import numpy as np
24
+ from numba import njit
25
+
26
+
27
+ @njit(cache=True)
28
+ def _quantize_signed_qty(qty: float, price: float, contract_size: float, qty_step: float, min_qty: float, min_notional: float) -> float:
29
+ if qty == 0.0:
30
+ return 0.0
31
+ sign = 1.0
32
+ if qty < 0.0:
33
+ sign = -1.0
34
+ q = abs(qty)
35
+ if qty_step > 0.0:
36
+ q = np.floor((q / qty_step) + 1e-12) * qty_step
37
+ if q <= 0.0:
38
+ return 0.0
39
+ if min_qty > 0.0 and q + 1e-12 < min_qty:
40
+ return 0.0
41
+ if min_notional > 0.0 and q * price * contract_size + 1e-12 < min_notional:
42
+ return 0.0
43
+ return sign * q
44
+
45
+
46
+ @njit(cache=True)
47
+ def _engine_units(
48
+ n_bars: int,
49
+ n_syms: int,
50
+ highs: np.ndarray, # (n_bars, n_syms) float64
51
+ lows: np.ndarray, # (n_bars, n_syms) float64
52
+ closes: np.ndarray, # (n_bars, n_syms) float64
53
+ signals: np.ndarray, # (n_bars, n_syms) float64 pre-scaled target units
54
+ funding_rates: np.ndarray, # (n_bars, n_syms) float64
55
+ is_funding_bar: np.ndarray, # (n_bars,) bool
56
+ init_capital: float,
57
+ leverage: float,
58
+ maint_ratio: float,
59
+ fee_rate: float,
60
+ contract_sizes: np.ndarray, # (n_syms,) float64
61
+ slippage: float,
62
+ ):
63
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
64
+ equity = init_capital
65
+ current_pos = np.zeros(n_syms, dtype=np.float64)
66
+ liq_flag = False
67
+ liq_idx = -1
68
+
69
+ equity_curve[0] = equity
70
+
71
+ for i in range(1, n_bars):
72
+ if liq_flag:
73
+ equity_curve[i] = 0.0
74
+ continue
75
+
76
+ # 1 ── Mark-to-market (close-to-close) ───────────────────────────
77
+ for s in range(n_syms):
78
+ p = current_pos[s]
79
+ if p != 0.0:
80
+ equity += p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
81
+
82
+ # 2 ── Intrabar liquidation check ─────────────────────────────────
83
+ worst_equity = equity
84
+ maint_req = 0.0
85
+ for s in range(n_syms):
86
+ p = current_pos[s]
87
+ if p == 0.0:
88
+ continue
89
+ worst_p = lows[i, s] if p > 0.0 else highs[i, s]
90
+ worst_equity += p * (worst_p - closes[i, s]) * contract_sizes[s]
91
+ # Binance: maintenance_margin = notional × mm_rate
92
+ maint_req += abs(p) * worst_p * contract_sizes[s] * maint_ratio
93
+
94
+ if maint_req > 0.0 and worst_equity <= maint_req:
95
+ liq_flag = True
96
+ liq_idx = i
97
+ equity = 0.0
98
+ for s in range(n_syms):
99
+ current_pos[s] = 0.0
100
+ equity_curve[i] = 0.0
101
+ continue
102
+
103
+ # 3 ── Funding fee ─────────────────────────────────────────────────
104
+ if is_funding_bar[i]:
105
+ for s in range(n_syms):
106
+ p = current_pos[s]
107
+ if p != 0.0:
108
+ # Long pays positive rate; short earns positive rate
109
+ equity -= p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
110
+
111
+ # Funding can push equity below maintenance before new orders.
112
+ close_maint_req = 0.0
113
+ for s in range(n_syms):
114
+ p = current_pos[s]
115
+ if p != 0.0:
116
+ close_maint_req += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
117
+
118
+ if close_maint_req > 0.0 and equity <= close_maint_req:
119
+ liq_flag = True
120
+ liq_idx = i
121
+ equity = 0.0
122
+ for s in range(n_syms):
123
+ current_pos[s] = 0.0
124
+ equity_curve[i] = 0.0
125
+ continue
126
+
127
+ # 4 ── Execute signal changes ──────────────────────────────────────
128
+ cur_im = 0.0
129
+ for s in range(n_syms):
130
+ cur_im += abs(current_pos[s]) * closes[i, s] * contract_sizes[s] / leverage
131
+
132
+ avail = equity - cur_im
133
+ if avail < 0.0:
134
+ avail = 0.0
135
+
136
+ for s in range(n_syms):
137
+ target = signals[i, s]
138
+ if abs(target - current_pos[s]) < 1e-12:
139
+ continue
140
+
141
+ delta = target - current_pos[s]
142
+ exec_p = closes[i, s] * (1.0 + slippage if delta > 0.0 else 1.0 - slippage)
143
+
144
+ fee_cost = abs(delta) * exec_p * contract_sizes[s] * fee_rate
145
+ # equity already marked to close; exec_p deviates → always a cost
146
+ slip_cost = abs(delta) * abs(exec_p - closes[i, s]) * contract_sizes[s]
147
+
148
+ old_im = abs(current_pos[s]) * closes[i, s] * contract_sizes[s] / leverage
149
+ new_im = abs(target) * exec_p * contract_sizes[s] / leverage
150
+ required = (new_im - old_im) + fee_cost + slip_cost
151
+
152
+ if required > avail:
153
+ continue # order rejected: insufficient margin
154
+
155
+ equity -= fee_cost + slip_cost
156
+ current_pos[s] = target
157
+ avail -= required
158
+
159
+ equity_curve[i] = equity
160
+
161
+ return equity_curve, liq_flag, liq_idx
162
+
163
+
164
+ @njit(cache=True)
165
+ def _engine_pct_equity(
166
+ n_bars: int,
167
+ n_syms: int,
168
+ highs: np.ndarray,
169
+ lows: np.ndarray,
170
+ closes: np.ndarray,
171
+ signals: np.ndarray, # (n_bars, n_syms) raw weight e.g. 1.0 / -0.5 / 0.0
172
+ funding_rates: np.ndarray,
173
+ is_funding_bar: np.ndarray,
174
+ init_capital: float,
175
+ leverage: float,
176
+ maint_ratio: float,
177
+ fee_rate: float,
178
+ contract_sizes: np.ndarray,
179
+ slippage: float,
180
+ alloc_pct: np.ndarray, # (n_syms,) fraction of equity, in (0, 1]
181
+ qty_steps: np.ndarray,
182
+ min_qtys: np.ndarray,
183
+ min_notionals: np.ndarray,
184
+ ):
185
+ """
186
+ Target units = equity × alloc_pct[s] × weight[i,s] / (close[i,s] × cs[s])
187
+ Recalculated only when weight changes; no drift-rebalancing between bars.
188
+ """
189
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
190
+ equity = init_capital
191
+ current_pos = np.zeros(n_syms, dtype=np.float64)
192
+ liq_flag = False
193
+ liq_idx = -1
194
+
195
+ equity_curve[0] = equity
196
+
197
+ for i in range(1, n_bars):
198
+ if liq_flag:
199
+ equity_curve[i] = 0.0
200
+ continue
201
+
202
+ # MTM
203
+ for s in range(n_syms):
204
+ p = current_pos[s]
205
+ if p != 0.0:
206
+ equity += p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
207
+
208
+ # Liquidation
209
+ worst_equity = equity
210
+ maint_req = 0.0
211
+ for s in range(n_syms):
212
+ p = current_pos[s]
213
+ if p == 0.0:
214
+ continue
215
+ worst_p = lows[i, s] if p > 0.0 else highs[i, s]
216
+ worst_equity += p * (worst_p - closes[i, s]) * contract_sizes[s]
217
+ maint_req += abs(p) * worst_p * contract_sizes[s] * maint_ratio
218
+
219
+ if maint_req > 0.0 and worst_equity <= maint_req:
220
+ liq_flag = True
221
+ liq_idx = i
222
+ equity = 0.0
223
+ for s in range(n_syms):
224
+ current_pos[s] = 0.0
225
+ equity_curve[i] = 0.0
226
+ continue
227
+
228
+ # Funding
229
+ if is_funding_bar[i]:
230
+ for s in range(n_syms):
231
+ p = current_pos[s]
232
+ if p != 0.0:
233
+ equity -= p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
234
+
235
+ close_maint_req = 0.0
236
+ for s in range(n_syms):
237
+ p = current_pos[s]
238
+ if p != 0.0:
239
+ close_maint_req += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
240
+
241
+ if close_maint_req > 0.0 and equity <= close_maint_req:
242
+ liq_flag = True
243
+ liq_idx = i
244
+ equity = 0.0
245
+ for s in range(n_syms):
246
+ current_pos[s] = 0.0
247
+ equity_curve[i] = 0.0
248
+ continue
249
+
250
+ # Execute on weight-change only
251
+ cur_im = 0.0
252
+ for s in range(n_syms):
253
+ cur_im += abs(current_pos[s]) * closes[i, s] * contract_sizes[s] / leverage
254
+
255
+ avail = equity - cur_im
256
+ if avail < 0.0:
257
+ avail = 0.0
258
+
259
+ for s in range(n_syms):
260
+ if signals[i, s] == signals[i - 1, s]:
261
+ continue
262
+
263
+ denom = closes[i, s] * contract_sizes[s]
264
+ if denom == 0.0:
265
+ continue
266
+
267
+ target = (equity * alloc_pct[s] * signals[i, s]) / denom
268
+ target = _quantize_signed_qty(
269
+ target, closes[i, s], contract_sizes[s], qty_steps[s], min_qtys[s], min_notionals[s]
270
+ )
271
+
272
+ if abs(target - current_pos[s]) < 1e-12:
273
+ continue
274
+
275
+ delta = target - current_pos[s]
276
+ exec_p = closes[i, s] * (1.0 + slippage if delta > 0.0 else 1.0 - slippage)
277
+
278
+ fee_cost = abs(delta) * exec_p * contract_sizes[s] * fee_rate
279
+ slip_cost = abs(delta) * abs(exec_p - closes[i, s]) * contract_sizes[s]
280
+
281
+ old_im = abs(current_pos[s]) * closes[i, s] * contract_sizes[s] / leverage
282
+ new_im = abs(target) * exec_p * contract_sizes[s] / leverage
283
+ required = (new_im - old_im) + fee_cost + slip_cost
284
+
285
+ if required > avail:
286
+ continue
287
+
288
+ equity -= fee_cost + slip_cost
289
+ current_pos[s] = target
290
+ avail -= required
291
+
292
+ equity_curve[i] = equity
293
+
294
+ return equity_curve, liq_flag, liq_idx
295
+
296
+
297
+ @njit(cache=True)
298
+ def _dca_check_liquidation(
299
+ n_syms: int,
300
+ i: int,
301
+ equity: float,
302
+ current_pos: np.ndarray,
303
+ highs: np.ndarray,
304
+ lows: np.ndarray,
305
+ closes: np.ndarray,
306
+ contract_sizes: np.ndarray,
307
+ maint_ratio: float,
308
+ ):
309
+ worst_equity = equity
310
+ maint_req = 0.0
311
+ for s in range(n_syms):
312
+ p = current_pos[s]
313
+ if p == 0.0:
314
+ continue
315
+ worst_p = lows[i, s] if p > 0.0 else highs[i, s]
316
+ worst_equity += p * (worst_p - closes[i, s]) * contract_sizes[s]
317
+ maint_req += abs(p) * worst_p * contract_sizes[s] * maint_ratio
318
+
319
+ return maint_req > 0.0 and worst_equity <= maint_req
320
+
321
+
322
+ @njit(cache=True)
323
+ def _engine_dca_ladder(
324
+ n_bars: int,
325
+ n_syms: int,
326
+ highs: np.ndarray,
327
+ lows: np.ndarray,
328
+ closes: np.ndarray,
329
+ signals: np.ndarray, # signed desired structural level
330
+ funding_rates: np.ndarray,
331
+ is_funding_bar: np.ndarray,
332
+ init_capital: float,
333
+ leverage: float,
334
+ maint_ratio: float,
335
+ fee_rate: float,
336
+ contract_sizes: np.ndarray,
337
+ market_slippage: float,
338
+ base_notional: np.ndarray,
339
+ safety_notional: np.ndarray,
340
+ step_pct: np.ndarray,
341
+ step_scale: np.ndarray,
342
+ volume_scale: np.ndarray,
343
+ max_safety_orders: int,
344
+ take_profit_pct: np.ndarray,
345
+ allow_same_bar_exit: bool,
346
+ qty_steps: np.ndarray,
347
+ min_qtys: np.ndarray,
348
+ min_notionals: np.ndarray,
349
+ ):
350
+ """
351
+ DCA ladder execution model.
352
+
353
+ signals are structural caps, not target units:
354
+ +N enables a long ladder up to level N, -N enables a short ladder.
355
+ level 1 is the base order; levels 2..N are safety orders.
356
+
357
+ Base orders and signal-zero exits execute as market-at-close with
358
+ market_slippage. Safety orders and take-profit exits are limit fills at
359
+ their trigger prices when High/Low touches them.
360
+ """
361
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
362
+ pos_out = np.zeros((n_bars, n_syms), dtype=np.float64)
363
+ level_out = np.zeros((n_bars, n_syms), dtype=np.float64)
364
+
365
+ equity = init_capital
366
+ current_pos = np.zeros(n_syms, dtype=np.float64)
367
+ current_side = np.zeros(n_syms, dtype=np.int64)
368
+ current_lvl = np.zeros(n_syms, dtype=np.int64)
369
+ anchor_price = np.zeros(n_syms, dtype=np.float64)
370
+ avg_entry = np.zeros(n_syms, dtype=np.float64)
371
+
372
+ liq_flag = False
373
+ liq_idx = -1
374
+
375
+ equity_curve[0] = equity
376
+
377
+ for i in range(1, n_bars):
378
+ if liq_flag:
379
+ equity_curve[i] = 0.0
380
+ for s in range(n_syms):
381
+ pos_out[i, s] = 0.0
382
+ level_out[i, s] = 0.0
383
+ continue
384
+
385
+ # 1. Mark existing positions to current close.
386
+ for s in range(n_syms):
387
+ p = current_pos[s]
388
+ if p != 0.0:
389
+ equity += p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
390
+
391
+ # 2. Existing-book liquidation before any new ladder orders.
392
+ if _dca_check_liquidation(
393
+ n_syms, i, equity, current_pos, highs, lows, closes,
394
+ contract_sizes, maint_ratio
395
+ ):
396
+ liq_flag = True
397
+ liq_idx = i
398
+ equity = 0.0
399
+ for s in range(n_syms):
400
+ current_pos[s] = 0.0
401
+ current_side[s] = 0
402
+ current_lvl[s] = 0
403
+ pos_out[i, s] = 0.0
404
+ level_out[i, s] = 0.0
405
+ equity_curve[i] = 0.0
406
+ continue
407
+
408
+ # 3. Funding on the position carried into the funding timestamp.
409
+ if is_funding_bar[i]:
410
+ for s in range(n_syms):
411
+ p = current_pos[s]
412
+ if p != 0.0:
413
+ equity -= p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
414
+
415
+ close_maint_req = 0.0
416
+ for s in range(n_syms):
417
+ p = current_pos[s]
418
+ if p != 0.0:
419
+ close_maint_req += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
420
+
421
+ if close_maint_req > 0.0 and equity <= close_maint_req:
422
+ liq_flag = True
423
+ liq_idx = i
424
+ equity = 0.0
425
+ for s in range(n_syms):
426
+ current_pos[s] = 0.0
427
+ current_side[s] = 0
428
+ current_lvl[s] = 0
429
+ pos_out[i, s] = 0.0
430
+ level_out[i, s] = 0.0
431
+ equity_curve[i] = 0.0
432
+ continue
433
+
434
+ # 4. Execute structural DCA state.
435
+ for s in range(n_syms):
436
+ c = closes[i, s]
437
+ hi = highs[i, s]
438
+ lo = lows[i, s]
439
+ cs = contract_sizes[s]
440
+
441
+ if c <= 0.0 or cs <= 0.0:
442
+ continue
443
+
444
+ raw_sig = signals[i, s]
445
+ desired_side = 0
446
+ if raw_sig > 0.0:
447
+ desired_side = 1
448
+ elif raw_sig < 0.0:
449
+ desired_side = -1
450
+
451
+ desired_level = int(abs(raw_sig))
452
+ max_level = max_safety_orders + 1
453
+ if desired_level > max_level:
454
+ desired_level = max_level
455
+
456
+ # Existing ladder TP has priority over a later close/flip signal.
457
+ if current_lvl[s] > 0 and take_profit_pct[s] > 0.0:
458
+ tp = avg_entry[s] * (
459
+ 1.0 + take_profit_pct[s] if current_side[s] > 0
460
+ else 1.0 - take_profit_pct[s]
461
+ )
462
+ hit_tp = False
463
+ if current_side[s] > 0 and hi >= tp:
464
+ hit_tp = True
465
+ elif current_side[s] < 0 and lo <= tp:
466
+ hit_tp = True
467
+
468
+ if hit_tp:
469
+ delta = -current_pos[s]
470
+ fee_cost = abs(delta) * tp * cs * fee_rate
471
+ equity += delta * (c - tp) * cs - fee_cost
472
+ current_pos[s] = 0.0
473
+ current_side[s] = 0
474
+ current_lvl[s] = 0
475
+ anchor_price[s] = 0.0
476
+ avg_entry[s] = 0.0
477
+
478
+ # Signal-side change or signal flat closes the existing ladder.
479
+ if current_lvl[s] > 0 and desired_side != current_side[s]:
480
+ delta = -current_pos[s]
481
+ exec_p = c
482
+ if delta > 0.0:
483
+ exec_p = c * (1.0 + market_slippage)
484
+ elif delta < 0.0:
485
+ exec_p = c * (1.0 - market_slippage)
486
+ fee_cost = abs(delta) * exec_p * cs * fee_rate
487
+ equity += delta * (c - exec_p) * cs - fee_cost
488
+ current_pos[s] = 0.0
489
+ current_side[s] = 0
490
+ current_lvl[s] = 0
491
+ anchor_price[s] = 0.0
492
+ avg_entry[s] = 0.0
493
+
494
+ if desired_side == 0 or desired_level == 0:
495
+ continue
496
+
497
+ filled_this_bar = False
498
+ started_this_bar = False
499
+
500
+ # Base order: market-at-close when a cycle starts.
501
+ if current_lvl[s] == 0:
502
+ delta = desired_side * base_notional[s] / c
503
+ exec_p = c * (1.0 + market_slippage if delta > 0.0 else 1.0 - market_slippage)
504
+ delta = _quantize_signed_qty(delta, exec_p, cs, qty_steps[s], min_qtys[s], min_notionals[s])
505
+ if delta == 0.0:
506
+ continue
507
+
508
+ cur_im = 0.0
509
+ for k in range(n_syms):
510
+ cur_im += abs(current_pos[k]) * closes[i, k] * contract_sizes[k] / leverage
511
+ avail = equity - cur_im
512
+ im_needed = abs(delta) * exec_p * cs / leverage
513
+
514
+ fee_cost = abs(delta) * exec_p * cs * fee_rate
515
+ slip_cost = abs(delta) * abs(exec_p - c) * cs
516
+
517
+ if im_needed + fee_cost + slip_cost <= avail:
518
+ equity += delta * (c - exec_p) * cs - fee_cost
519
+ current_pos[s] = delta
520
+ current_side[s] = desired_side
521
+ current_lvl[s] = 1
522
+ anchor_price[s] = exec_p
523
+ avg_entry[s] = exec_p
524
+ filled_this_bar = True
525
+ started_this_bar = True
526
+
527
+ # Safety orders: limit fills at the structural grid prices.
528
+ while current_lvl[s] > 0 and current_lvl[s] < desired_level and not started_this_bar:
529
+ next_level = current_lvl[s] + 1
530
+ ao_idx = next_level - 2
531
+
532
+ dev = 0.0
533
+ step = step_pct[s]
534
+ for k in range(ao_idx + 1):
535
+ dev += step
536
+ step *= step_scale[s]
537
+
538
+ trigger = anchor_price[s] * (1.0 - dev if current_side[s] > 0 else 1.0 + dev)
539
+ if trigger <= 0.0:
540
+ break
541
+
542
+ touched = False
543
+ if current_side[s] > 0 and lo <= trigger:
544
+ touched = True
545
+ elif current_side[s] < 0 and hi >= trigger:
546
+ touched = True
547
+
548
+ if not touched:
549
+ break
550
+
551
+ notional = safety_notional[s]
552
+ mult = 1.0
553
+ for k in range(ao_idx):
554
+ mult *= volume_scale[s]
555
+ notional *= mult
556
+
557
+ delta = current_side[s] * notional / trigger
558
+ delta = _quantize_signed_qty(delta, trigger, cs, qty_steps[s], min_qtys[s], min_notionals[s])
559
+ if delta == 0.0:
560
+ break
561
+
562
+ cur_im = 0.0
563
+ for k in range(n_syms):
564
+ cur_im += abs(current_pos[k]) * closes[i, k] * contract_sizes[k] / leverage
565
+ avail = equity - cur_im
566
+ im_needed = abs(delta) * trigger * cs / leverage
567
+ fee_cost = abs(delta) * trigger * cs * fee_rate
568
+
569
+ if im_needed + fee_cost > avail:
570
+ break
571
+
572
+ old_abs = abs(current_pos[s])
573
+ add_abs = abs(delta)
574
+ equity += delta * (c - trigger) * cs - fee_cost
575
+ current_pos[s] += delta
576
+ avg_entry[s] = ((avg_entry[s] * old_abs) + (trigger * add_abs)) / (old_abs + add_abs)
577
+ current_lvl[s] = next_level
578
+ filled_this_bar = True
579
+
580
+ # Take-profit: limit exit from weighted average entry.
581
+ if (
582
+ current_lvl[s] > 0
583
+ and take_profit_pct[s] > 0.0
584
+ and (allow_same_bar_exit or not filled_this_bar)
585
+ ):
586
+ tp = avg_entry[s] * (
587
+ 1.0 + take_profit_pct[s] if current_side[s] > 0
588
+ else 1.0 - take_profit_pct[s]
589
+ )
590
+ hit_tp = False
591
+ if current_side[s] > 0 and hi >= tp:
592
+ hit_tp = True
593
+ elif current_side[s] < 0 and lo <= tp:
594
+ hit_tp = True
595
+
596
+ if hit_tp:
597
+ delta = -current_pos[s]
598
+ fee_cost = abs(delta) * tp * cs * fee_rate
599
+ equity += delta * (c - tp) * cs - fee_cost
600
+ current_pos[s] = 0.0
601
+ current_side[s] = 0
602
+ current_lvl[s] = 0
603
+ anchor_price[s] = 0.0
604
+ avg_entry[s] = 0.0
605
+
606
+ # 5. Conservative post-fill liquidation on same-bar extremes.
607
+ if _dca_check_liquidation(
608
+ n_syms, i, equity, current_pos, highs, lows, closes,
609
+ contract_sizes, maint_ratio
610
+ ):
611
+ liq_flag = True
612
+ liq_idx = i
613
+ equity = 0.0
614
+ for s in range(n_syms):
615
+ current_pos[s] = 0.0
616
+ current_side[s] = 0
617
+ current_lvl[s] = 0
618
+ pos_out[i, s] = 0.0
619
+ level_out[i, s] = 0.0
620
+ equity_curve[i] = 0.0
621
+ continue
622
+
623
+ for s in range(n_syms):
624
+ pos_out[i, s] = current_pos[s]
625
+ level_out[i, s] = current_side[s] * current_lvl[s]
626
+
627
+ equity_curve[i] = equity
628
+
629
+ return equity_curve, pos_out, level_out, liq_flag, liq_idx
630
+
631
+
632
+ @njit(cache=True)
633
+ def _engine_portfolio(
634
+ n_bars: int,
635
+ n_syms: int,
636
+ highs: np.ndarray,
637
+ lows: np.ndarray,
638
+ closes: np.ndarray,
639
+ target_pos: np.ndarray,
640
+ funding_rates: np.ndarray,
641
+ is_funding_bar: np.ndarray,
642
+ init_capital: float,
643
+ leverages: np.ndarray,
644
+ maint_ratio: float,
645
+ fee_rate: float,
646
+ slippage_rate: float,
647
+ contract_sizes: np.ndarray,
648
+ use_funding: bool,
649
+ tradable: np.ndarray,
650
+ ):
651
+ """
652
+ Numba portfolio simulation kernel.
653
+
654
+ target_pos is a pre-built units matrix after portfolio allocation mode.
655
+ The kernel applies cross-margin buying-power gates and returns the actual
656
+ accepted positions, equity, per-symbol cumulative PnL, fees, and turnover.
657
+ """
658
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
659
+ pos_out = np.zeros((n_bars, n_syms), dtype=np.float64)
660
+ sym_pnl = np.zeros((n_bars, n_syms), dtype=np.float64)
661
+ fee_arr = np.zeros(n_bars, dtype=np.float64)
662
+ slip_arr = np.zeros(n_bars, dtype=np.float64)
663
+ turn_arr = np.zeros(n_bars, dtype=np.float64)
664
+
665
+ current_pos = np.zeros(n_syms, dtype=np.float64)
666
+ current_pnl = np.zeros(n_syms, dtype=np.float64)
667
+ equity = init_capital
668
+ liq_flag = False
669
+ liq_idx = -1
670
+
671
+ equity_curve[0] = equity
672
+
673
+ for i in range(1, n_bars):
674
+ if liq_flag:
675
+ equity_curve[i] = 0.0
676
+ for s in range(n_syms):
677
+ pos_out[i, s] = 0.0
678
+ sym_pnl[i, s] = current_pnl[s]
679
+ continue
680
+
681
+ # 1. Mark carried positions to close.
682
+ for s in range(n_syms):
683
+ p = current_pos[s]
684
+ if p != 0.0:
685
+ pnl = p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
686
+ equity += pnl
687
+ current_pnl[s] += pnl
688
+
689
+ # 2. Intrabar liquidation using worst price.
690
+ worst_equity = equity
691
+ worst_mm = 0.0
692
+ for s in range(n_syms):
693
+ p = current_pos[s]
694
+ if p == 0.0:
695
+ continue
696
+ worst_p = lows[i, s] if p > 0.0 else highs[i, s]
697
+ worst_equity += p * (worst_p - closes[i, s]) * contract_sizes[s]
698
+ worst_mm += abs(p) * worst_p * contract_sizes[s] * maint_ratio
699
+
700
+ if worst_mm > 0.0 and worst_equity <= worst_mm:
701
+ liq_flag = True
702
+ liq_idx = i
703
+ equity = 0.0
704
+ for s in range(n_syms):
705
+ current_pos[s] = 0.0
706
+ pos_out[i, s] = 0.0
707
+ sym_pnl[i, s] = current_pnl[s]
708
+ equity_curve[i] = 0.0
709
+ continue
710
+
711
+ # 3. Funding on carried positions. Funding rates are per event.
712
+ if is_funding_bar[i] and use_funding:
713
+ for s in range(n_syms):
714
+ p = current_pos[s]
715
+ if p != 0.0:
716
+ fc = p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
717
+ equity -= fc
718
+ current_pnl[s] -= fc
719
+
720
+ close_mm = 0.0
721
+ for s in range(n_syms):
722
+ p = current_pos[s]
723
+ if p != 0.0:
724
+ close_mm += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
725
+
726
+ if close_mm > 0.0 and equity <= close_mm:
727
+ liq_flag = True
728
+ liq_idx = i
729
+ equity = 0.0
730
+ for s in range(n_syms):
731
+ current_pos[s] = 0.0
732
+ pos_out[i, s] = 0.0
733
+ sym_pnl[i, s] = current_pnl[s]
734
+ equity_curve[i] = 0.0
735
+ continue
736
+
737
+ # 4. Cross-margin buying-power gate for target portfolio.
738
+ cur_im = 0.0
739
+ target_im = 0.0
740
+ target_mm = 0.0
741
+ fee_est = 0.0
742
+ slip_est = 0.0
743
+ invalid_target = False
744
+ for s in range(n_syms):
745
+ c = closes[i, s]
746
+ cs = contract_sizes[s]
747
+ lev = leverages[s]
748
+ cur_im += abs(current_pos[s]) * c * cs / lev
749
+ target_im += abs(target_pos[i, s]) * c * cs / lev
750
+ target_mm += abs(target_pos[i, s]) * c * cs * maint_ratio
751
+
752
+ delta = target_pos[i, s] - current_pos[s]
753
+ if abs(delta) > 1e-12:
754
+ if not tradable[i, s] or c <= 0.0 or not np.isfinite(c):
755
+ invalid_target = True
756
+ continue
757
+ exec_price = c * (1.0 + slippage_rate) if delta > 0.0 else c * (1.0 - slippage_rate)
758
+ trade_notional = abs(delta) * exec_price * cs
759
+ fee_est += trade_notional * fee_rate
760
+ slip_est += abs(delta) * c * cs * slippage_rate
761
+
762
+ can_rebalance = True
763
+ post_trade_equity = equity - fee_est - slip_est
764
+ if invalid_target or post_trade_equity < target_im or post_trade_equity < target_mm:
765
+ can_rebalance = False
766
+
767
+ # 5. Execute accepted target at close.
768
+ if can_rebalance:
769
+ for s in range(n_syms):
770
+ c = closes[i, s]
771
+ cs = contract_sizes[s]
772
+ delta = target_pos[i, s] - current_pos[s]
773
+ if abs(delta) > 1e-12:
774
+ exec_price = c * (1.0 + slippage_rate) if delta > 0.0 else c * (1.0 - slippage_rate)
775
+ tv = abs(delta) * exec_price * cs
776
+ fee = tv * fee_rate
777
+ slip = abs(delta) * c * cs * slippage_rate
778
+ equity -= fee + slip
779
+ current_pnl[s] -= fee + slip
780
+ fee_arr[i] += fee
781
+ slip_arr[i] += slip
782
+
783
+ turn_arr[i] += tv
784
+ current_pos[s] = target_pos[i, s]
785
+
786
+ # 6. Post-fee maintenance check.
787
+ close_mm = 0.0
788
+ for s in range(n_syms):
789
+ p = current_pos[s]
790
+ if p != 0.0:
791
+ close_mm += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
792
+
793
+ if close_mm > 0.0 and equity <= close_mm:
794
+ liq_flag = True
795
+ liq_idx = i
796
+ equity = 0.0
797
+ for s in range(n_syms):
798
+ current_pos[s] = 0.0
799
+ pos_out[i, s] = 0.0
800
+ sym_pnl[i, s] = current_pnl[s]
801
+ equity_curve[i] = 0.0
802
+ continue
803
+
804
+ for s in range(n_syms):
805
+ pos_out[i, s] = current_pos[s]
806
+ sym_pnl[i, s] = current_pnl[s]
807
+
808
+ equity_curve[i] = equity
809
+
810
+ return equity_curve, pos_out, sym_pnl, fee_arr, slip_arr, turn_arr, liq_flag, liq_idx
811
+
812
+
813
+ @njit(cache=True)
814
+ def _engine_portfolio_equity_sizing(
815
+ n_bars: int,
816
+ n_syms: int,
817
+ highs: np.ndarray,
818
+ lows: np.ndarray,
819
+ closes: np.ndarray,
820
+ raw_signals: np.ndarray,
821
+ funding_rates: np.ndarray,
822
+ is_funding_bar: np.ndarray,
823
+ init_capital: float,
824
+ leverages: np.ndarray,
825
+ maint_ratio: float,
826
+ fee_rate: float,
827
+ slippage_rate: float,
828
+ contract_sizes: np.ndarray,
829
+ use_funding: bool,
830
+ allocs: np.ndarray,
831
+ sizing_mode_id: int,
832
+ portfolio_mode_id: int,
833
+ use_pyramiding: bool,
834
+ exposure_scalar: float,
835
+ beta: np.ndarray,
836
+ inv_vol: np.ndarray,
837
+ qty_steps: np.ndarray,
838
+ min_qtys: np.ndarray,
839
+ min_notionals: np.ndarray,
840
+ tradable: np.ndarray,
841
+ ):
842
+ """
843
+ Portfolio kernel for sizing modes which depend on live equity.
844
+
845
+ sizing_mode_id:
846
+ 0 = %_equity, signal * alloc[s] * equity
847
+ 1 = target_weight, signal * equity
848
+ 2 = gross_exposure, normalized signed signal with target gross equity * scalar
849
+ 3 = net_exposure, normalized signed signal with target net equity * scalar
850
+
851
+ portfolio_mode_id:
852
+ 0 = longshort, 1 = market_neutral, 2 = directional, 3 = equal_weight,
853
+ 4 = risk_parity, 5 = beta_neutral.
854
+ """
855
+ equity_curve = np.zeros(n_bars, dtype=np.float64)
856
+ target_out = np.zeros((n_bars, n_syms), dtype=np.float64)
857
+ pos_out = np.zeros((n_bars, n_syms), dtype=np.float64)
858
+ sym_pnl = np.zeros((n_bars, n_syms), dtype=np.float64)
859
+ fee_arr = np.zeros(n_bars, dtype=np.float64)
860
+ slip_arr = np.zeros(n_bars, dtype=np.float64)
861
+ turn_arr = np.zeros(n_bars, dtype=np.float64)
862
+
863
+ current_pos = np.zeros(n_syms, dtype=np.float64)
864
+ current_pnl = np.zeros(n_syms, dtype=np.float64)
865
+ target_notional = np.zeros(n_syms, dtype=np.float64)
866
+ target_units = np.zeros(n_syms, dtype=np.float64)
867
+ equity = init_capital
868
+ liq_flag = False
869
+ liq_idx = -1
870
+ equity_curve[0] = equity
871
+
872
+ for i in range(1, n_bars):
873
+ if liq_flag:
874
+ equity_curve[i] = 0.0
875
+ for s in range(n_syms):
876
+ pos_out[i, s] = 0.0
877
+ sym_pnl[i, s] = current_pnl[s]
878
+ continue
879
+
880
+ for s in range(n_syms):
881
+ p = current_pos[s]
882
+ if p != 0.0:
883
+ pnl = p * (closes[i, s] - closes[i - 1, s]) * contract_sizes[s]
884
+ equity += pnl
885
+ current_pnl[s] += pnl
886
+
887
+ worst_equity = equity
888
+ worst_mm = 0.0
889
+ for s in range(n_syms):
890
+ p = current_pos[s]
891
+ if p == 0.0:
892
+ continue
893
+ worst_p = lows[i, s] if p > 0.0 else highs[i, s]
894
+ worst_equity += p * (worst_p - closes[i, s]) * contract_sizes[s]
895
+ worst_mm += abs(p) * worst_p * contract_sizes[s] * maint_ratio
896
+
897
+ if worst_mm > 0.0 and worst_equity <= worst_mm:
898
+ liq_flag = True
899
+ liq_idx = i
900
+ equity = 0.0
901
+ for s in range(n_syms):
902
+ current_pos[s] = 0.0
903
+ pos_out[i, s] = 0.0
904
+ sym_pnl[i, s] = current_pnl[s]
905
+ equity_curve[i] = 0.0
906
+ continue
907
+
908
+ if is_funding_bar[i] and use_funding:
909
+ for s in range(n_syms):
910
+ p = current_pos[s]
911
+ if p != 0.0:
912
+ fc = p * closes[i, s] * contract_sizes[s] * funding_rates[i, s]
913
+ equity -= fc
914
+ current_pnl[s] -= fc
915
+
916
+ close_mm = 0.0
917
+ for s in range(n_syms):
918
+ p = current_pos[s]
919
+ if p != 0.0:
920
+ close_mm += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
921
+
922
+ if close_mm > 0.0 and equity <= close_mm:
923
+ liq_flag = True
924
+ liq_idx = i
925
+ equity = 0.0
926
+ for s in range(n_syms):
927
+ current_pos[s] = 0.0
928
+ pos_out[i, s] = 0.0
929
+ sym_pnl[i, s] = current_pnl[s]
930
+ equity_curve[i] = 0.0
931
+ continue
932
+
933
+ sum_abs_sig = 0.0
934
+ sum_sig = 0.0
935
+ for s in range(n_syms):
936
+ sig = raw_signals[i, s]
937
+ if not use_pyramiding:
938
+ if sig > 0.0:
939
+ sig = 1.0
940
+ elif sig < 0.0:
941
+ sig = -1.0
942
+ else:
943
+ sig = 0.0
944
+ sum_abs_sig += abs(sig)
945
+ sum_sig += sig
946
+ target_notional[s] = sig
947
+
948
+ for s in range(n_syms):
949
+ sig = target_notional[s]
950
+ if sizing_mode_id == 0:
951
+ target_notional[s] = sig * allocs[s] * equity
952
+ elif sizing_mode_id == 1:
953
+ target_notional[s] = sig * equity
954
+ elif sizing_mode_id == 2:
955
+ if sum_abs_sig > 0.0:
956
+ target_notional[s] = sig / sum_abs_sig * equity * exposure_scalar
957
+ else:
958
+ target_notional[s] = 0.0
959
+ else:
960
+ if abs(sum_sig) > 1e-12:
961
+ target_notional[s] = sig / sum_sig * equity * exposure_scalar
962
+ else:
963
+ target_notional[s] = 0.0
964
+
965
+ _apply_portfolio_notional_mode(i, n_syms, portfolio_mode_id, target_notional, beta, inv_vol)
966
+
967
+ for s in range(n_syms):
968
+ denom = closes[i, s] * contract_sizes[s]
969
+ if tradable[i, s] and denom != 0.0:
970
+ target_units[s] = target_notional[s] / denom
971
+ else:
972
+ target_units[s] = 0.0
973
+ target_units[s] = _quantize_signed_qty(
974
+ target_units[s], closes[i, s], contract_sizes[s], qty_steps[s], min_qtys[s], min_notionals[s]
975
+ )
976
+ target_out[i, s] = target_units[s]
977
+
978
+ cur_im = 0.0
979
+ target_im = 0.0
980
+ target_mm = 0.0
981
+ fee_est = 0.0
982
+ slip_est = 0.0
983
+ invalid_target = False
984
+ for s in range(n_syms):
985
+ c = closes[i, s]
986
+ cs = contract_sizes[s]
987
+ lev = leverages[s]
988
+ cur_im += abs(current_pos[s]) * c * cs / lev
989
+ target_im += abs(target_units[s]) * c * cs / lev
990
+ target_mm += abs(target_units[s]) * c * cs * maint_ratio
991
+ delta = target_units[s] - current_pos[s]
992
+ if abs(delta) > 1e-12:
993
+ if not tradable[i, s] or c <= 0.0 or not np.isfinite(c):
994
+ invalid_target = True
995
+ continue
996
+ exec_price = c * (1.0 + slippage_rate) if delta > 0.0 else c * (1.0 - slippage_rate)
997
+ trade_notional = abs(delta) * exec_price * cs
998
+ fee_est += trade_notional * fee_rate
999
+ slip_est += abs(delta) * c * cs * slippage_rate
1000
+
1001
+ can_rebalance = True
1002
+ post_trade_equity = equity - fee_est - slip_est
1003
+ if invalid_target or post_trade_equity < target_im or post_trade_equity < target_mm:
1004
+ can_rebalance = False
1005
+
1006
+ if can_rebalance:
1007
+ for s in range(n_syms):
1008
+ c = closes[i, s]
1009
+ cs = contract_sizes[s]
1010
+ delta = target_units[s] - current_pos[s]
1011
+ if abs(delta) > 1e-12:
1012
+ exec_price = c * (1.0 + slippage_rate) if delta > 0.0 else c * (1.0 - slippage_rate)
1013
+ tv = abs(delta) * exec_price * cs
1014
+ fee = tv * fee_rate
1015
+ slip = abs(delta) * c * cs * slippage_rate
1016
+ equity -= fee + slip
1017
+ current_pnl[s] -= fee + slip
1018
+ fee_arr[i] += fee
1019
+ slip_arr[i] += slip
1020
+ turn_arr[i] += tv
1021
+ current_pos[s] = target_units[s]
1022
+
1023
+ close_mm = 0.0
1024
+ for s in range(n_syms):
1025
+ p = current_pos[s]
1026
+ if p != 0.0:
1027
+ close_mm += abs(p) * closes[i, s] * contract_sizes[s] * maint_ratio
1028
+
1029
+ if close_mm > 0.0 and equity <= close_mm:
1030
+ liq_flag = True
1031
+ liq_idx = i
1032
+ equity = 0.0
1033
+ for s in range(n_syms):
1034
+ current_pos[s] = 0.0
1035
+ pos_out[i, s] = 0.0
1036
+ sym_pnl[i, s] = current_pnl[s]
1037
+ equity_curve[i] = 0.0
1038
+ continue
1039
+
1040
+ for s in range(n_syms):
1041
+ pos_out[i, s] = current_pos[s]
1042
+ sym_pnl[i, s] = current_pnl[s]
1043
+ equity_curve[i] = equity
1044
+
1045
+ return equity_curve, target_out, pos_out, sym_pnl, fee_arr, slip_arr, turn_arr, liq_flag, liq_idx
1046
+
1047
+
1048
+ @njit(cache=True)
1049
+ def _apply_portfolio_notional_mode(
1050
+ i: int,
1051
+ n_syms: int,
1052
+ mode_id: int,
1053
+ target_notional: np.ndarray,
1054
+ beta: np.ndarray,
1055
+ inv_vol: np.ndarray,
1056
+ ):
1057
+ if mode_id == 1:
1058
+ long_sum = 0.0
1059
+ short_sum = 0.0
1060
+ for s in range(n_syms):
1061
+ v = target_notional[s]
1062
+ if v > 0.0:
1063
+ long_sum += v
1064
+ elif v < 0.0:
1065
+ short_sum += -v
1066
+ if long_sum == 0.0 or short_sum == 0.0:
1067
+ for s in range(n_syms):
1068
+ target_notional[s] = 0.0
1069
+ return
1070
+ target = (long_sum + short_sum) / 2.0
1071
+ long_scale = target / long_sum
1072
+ short_scale = target / short_sum
1073
+ for s in range(n_syms):
1074
+ if target_notional[s] > 0.0:
1075
+ target_notional[s] *= long_scale
1076
+ elif target_notional[s] < 0.0:
1077
+ target_notional[s] *= short_scale
1078
+ elif mode_id == 2:
1079
+ max_abs = 0.0
1080
+ dominant = -1
1081
+ for s in range(n_syms):
1082
+ v = abs(target_notional[s])
1083
+ if v > max_abs:
1084
+ max_abs = v
1085
+ dominant = s
1086
+ for s in range(n_syms):
1087
+ if s != dominant:
1088
+ target_notional[s] = 0.0
1089
+ elif mode_id == 3:
1090
+ active = 0
1091
+ gross = 0.0
1092
+ for s in range(n_syms):
1093
+ if target_notional[s] != 0.0:
1094
+ active += 1
1095
+ gross += abs(target_notional[s])
1096
+ if active == 0:
1097
+ return
1098
+ target_abs = gross / active
1099
+ for s in range(n_syms):
1100
+ if target_notional[s] > 0.0:
1101
+ target_notional[s] = target_abs
1102
+ elif target_notional[s] < 0.0:
1103
+ target_notional[s] = -target_abs
1104
+ elif mode_id == 4:
1105
+ gross = 0.0
1106
+ inv_sum = 0.0
1107
+ for s in range(n_syms):
1108
+ if target_notional[s] != 0.0:
1109
+ gross += abs(target_notional[s])
1110
+ inv_sum += inv_vol[i, s]
1111
+ if gross == 0.0:
1112
+ return
1113
+ if inv_sum == 0.0:
1114
+ for s in range(n_syms):
1115
+ target_notional[s] = 0.0
1116
+ return
1117
+ for s in range(n_syms):
1118
+ if target_notional[s] > 0.0:
1119
+ target_notional[s] = gross * inv_vol[i, s] / inv_sum
1120
+ elif target_notional[s] < 0.0:
1121
+ target_notional[s] = -gross * inv_vol[i, s] / inv_sum
1122
+ elif mode_id == 5:
1123
+ long_beta = 0.0
1124
+ short_beta = 0.0
1125
+ for s in range(n_syms):
1126
+ b = beta[s]
1127
+ v = target_notional[s] * b
1128
+ if v > 0.0:
1129
+ long_beta += v
1130
+ elif v < 0.0:
1131
+ short_beta += -v
1132
+ if long_beta == 0.0 or short_beta == 0.0:
1133
+ for s in range(n_syms):
1134
+ target_notional[s] = 0.0
1135
+ return
1136
+ target_beta = (long_beta + short_beta) / 2.0
1137
+ long_scale = target_beta / long_beta
1138
+ short_scale = target_beta / short_beta
1139
+ for s in range(n_syms):
1140
+ v = target_notional[s] * beta[s]
1141
+ if v > 0.0:
1142
+ target_notional[s] *= long_scale
1143
+ elif v < 0.0:
1144
+ target_notional[s] *= short_scale