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/basket.py ADDED
@@ -0,0 +1,234 @@
1
+ """
2
+ quantbt.core.basket
3
+ -------------------
4
+ Basket and pair-trading order-plan helpers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, Optional, Tuple
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ from .orders import OrderIntent
16
+ from .preprocessor import align_series, validate_datetime
17
+ from .schema import BasketSpec, OrderSide, OrderType, TimeInForce
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class FrozenBasketPlan:
22
+ basket: BasketSpec
23
+ orders: Tuple[OrderIntent, ...]
24
+ target_units: pd.DataFrame
25
+ signals: pd.Series
26
+ entry_ratios: pd.DataFrame
27
+ metadata: Dict = field(default_factory=dict)
28
+
29
+
30
+ def build_frozen_basket_orders(
31
+ datetime_index,
32
+ basket: BasketSpec,
33
+ signal: pd.Series,
34
+ closes: Dict[str, pd.Series],
35
+ hedge_ratios: Optional[Dict[str, pd.Series]] = None,
36
+ order_type: OrderType = OrderType.MARKET,
37
+ tif: TimeInForce = TimeInForce.IOC,
38
+ rebalance_threshold: Optional[float] = None,
39
+ min_abs_delta: float = 1e-12,
40
+ ) -> FrozenBasketPlan:
41
+ """
42
+ Convert a scalar basket signal into leg orders with hedge freezing.
43
+
44
+ Ratios are interpreted as unit ratios per one basket unit. At every signal
45
+ transition, units are recomputed from the entry bar prices and then held
46
+ unchanged until the next signal transition. Price drift alone does not
47
+ generate micro-rebalancing orders.
48
+ """
49
+ if order_type is not OrderType.MARKET:
50
+ raise NotImplementedError("Phase 4 basket order generation supports market orders")
51
+ if min_abs_delta < 0.0:
52
+ raise ValueError("min_abs_delta must be >= 0")
53
+ if rebalance_threshold is not None and rebalance_threshold < 0.0:
54
+ raise ValueError("rebalance_threshold must be >= 0")
55
+
56
+ idx = validate_datetime(datetime_index)
57
+ symbols = [leg.symbol for leg in basket.legs]
58
+ if len(set(symbols)) != len(symbols):
59
+ raise ValueError("basket legs must have unique symbols")
60
+ if not set(symbols).issubset(closes.keys()):
61
+ missing = sorted(set(symbols) - set(closes.keys()))
62
+ raise ValueError(f"missing closes for basket legs: {missing}")
63
+
64
+ close_dict = align_series(closes, symbols, idx)
65
+ sig = _align_signal(signal, idx)
66
+ ratio_dict = _build_ratio_series(basket, hedge_ratios, symbols, idx)
67
+
68
+ orders = []
69
+ current_units = {s: 0.0 for s in symbols}
70
+ current_signal = 0.0
71
+ target_rows = []
72
+ ratio_rows = []
73
+
74
+ for ts in idx:
75
+ raw_signal = float(sig.loc[ts])
76
+ if abs(raw_signal) < min_abs_delta:
77
+ raw_signal = 0.0
78
+
79
+ signal_changed = abs(raw_signal - current_signal) > min_abs_delta
80
+ ratio_drift = _max_ratio_drift(current_units, ratio_dict, symbols, ts)
81
+ should_rebalance = (
82
+ rebalance_threshold is not None
83
+ and raw_signal != 0.0
84
+ and not signal_changed
85
+ and ratio_drift > rebalance_threshold
86
+ )
87
+ if signal_changed or should_rebalance:
88
+ target_units = _compute_entry_units(
89
+ basket=basket,
90
+ signal_value=raw_signal,
91
+ symbols=symbols,
92
+ timestamp=ts,
93
+ closes=close_dict,
94
+ ratios=ratio_dict,
95
+ )
96
+
97
+ for sym in symbols:
98
+ delta = target_units[sym] - current_units[sym]
99
+ if abs(delta) <= min_abs_delta:
100
+ continue
101
+ side = OrderSide.BUY if delta > 0.0 else OrderSide.SELL
102
+ orders.append(
103
+ OrderIntent(
104
+ timestamp=ts,
105
+ symbol=sym,
106
+ side=side,
107
+ order_type=order_type,
108
+ qty=abs(delta),
109
+ tif=tif,
110
+ tag=basket.basket_id,
111
+ metadata={
112
+ "basket_id": basket.basket_id,
113
+ "basket_signal": raw_signal,
114
+ "basket_policy": basket.execution_policy.value,
115
+ "hedge_frozen": basket.freeze_hedge,
116
+ "rebalance": should_rebalance,
117
+ "ratio_drift": ratio_drift,
118
+ "target_units": target_units[sym],
119
+ "previous_units": current_units[sym],
120
+ },
121
+ )
122
+ )
123
+ current_units[sym] = target_units[sym]
124
+
125
+ current_signal = raw_signal
126
+
127
+ target_rows.append({s: current_units[s] for s in symbols})
128
+ ratio_rows.append({s: float(ratio_dict[s].loc[ts]) for s in symbols})
129
+
130
+ return FrozenBasketPlan(
131
+ basket=basket,
132
+ orders=tuple(orders),
133
+ target_units=pd.DataFrame(target_rows, index=idx),
134
+ signals=sig,
135
+ entry_ratios=pd.DataFrame(ratio_rows, index=idx),
136
+ metadata={
137
+ "basket_id": basket.basket_id,
138
+ "gross_notional": basket.gross_notional,
139
+ "freeze_hedge": basket.freeze_hedge,
140
+ "execution_policy": basket.execution_policy.value,
141
+ "hedged_margin_offset": basket.hedged_margin_offset,
142
+ "rebalance_threshold": rebalance_threshold,
143
+ },
144
+ )
145
+
146
+
147
+ def _align_signal(signal: pd.Series, idx: pd.DatetimeIndex) -> pd.Series:
148
+ if not isinstance(signal, pd.Series):
149
+ signal = pd.Series(signal, index=idx)
150
+ else:
151
+ signal = signal.copy()
152
+ if isinstance(signal.index, pd.DatetimeIndex):
153
+ if signal.index.tz is None:
154
+ signal.index = signal.index.tz_localize("UTC")
155
+ else:
156
+ signal.index = signal.index.tz_convert("UTC")
157
+ return signal.reindex(idx, method="ffill").fillna(0.0).astype(float)
158
+
159
+
160
+ def _build_ratio_series(
161
+ basket: BasketSpec,
162
+ hedge_ratios: Optional[Dict[str, pd.Series]],
163
+ symbols: list[str],
164
+ idx: pd.DatetimeIndex,
165
+ ) -> Dict[str, pd.Series]:
166
+ defaults = {leg.symbol: float(leg.ratio) for leg in basket.legs}
167
+ if hedge_ratios is None:
168
+ return {s: pd.Series(defaults[s], index=idx, dtype=float) for s in symbols}
169
+
170
+ out = {}
171
+ for s in symbols:
172
+ value = hedge_ratios.get(s, defaults[s])
173
+ if isinstance(value, pd.Series):
174
+ ser = value.copy()
175
+ if isinstance(ser.index, pd.DatetimeIndex):
176
+ if ser.index.tz is None:
177
+ ser.index = ser.index.tz_localize("UTC")
178
+ else:
179
+ ser.index = ser.index.tz_convert("UTC")
180
+ out[s] = ser.reindex(idx, method="ffill").fillna(defaults[s]).astype(float)
181
+ else:
182
+ out[s] = pd.Series(float(value), index=idx, dtype=float)
183
+ return out
184
+
185
+
186
+ def _compute_entry_units(
187
+ basket: BasketSpec,
188
+ signal_value: float,
189
+ symbols: list[str],
190
+ timestamp,
191
+ closes: Dict[str, pd.Series],
192
+ ratios: Dict[str, pd.Series],
193
+ ) -> Dict[str, float]:
194
+ if signal_value == 0.0:
195
+ return {s: 0.0 for s in symbols}
196
+
197
+ gross_unit_notional = 0.0
198
+ for s in symbols:
199
+ price = float(closes[s].loc[timestamp])
200
+ ratio = float(ratios[s].loc[timestamp])
201
+ gross_unit_notional += abs(ratio) * price
202
+
203
+ if not np.isfinite(gross_unit_notional) or gross_unit_notional <= 0.0:
204
+ return {s: 0.0 for s in symbols}
205
+
206
+ basket_units = basket.gross_notional * abs(signal_value) / gross_unit_notional
207
+ signal_side = 1.0 if signal_value > 0.0 else -1.0
208
+ return {
209
+ s: basket_units * float(ratios[s].loc[timestamp]) * signal_side
210
+ for s in symbols
211
+ }
212
+
213
+
214
+ def _max_ratio_drift(
215
+ current_units: Dict[str, float],
216
+ ratios: Dict[str, pd.Series],
217
+ symbols: list[str],
218
+ timestamp,
219
+ ) -> float:
220
+ if not symbols:
221
+ return 0.0
222
+ ref_symbol = symbols[0]
223
+ frozen_ref = float(current_units[ref_symbol])
224
+ current_ref = float(ratios[ref_symbol].loc[timestamp])
225
+ if abs(frozen_ref) <= 1e-12 or abs(current_ref) <= 1e-12:
226
+ return 0.0
227
+
228
+ max_drift = 0.0
229
+ for symbol in symbols[1:]:
230
+ frozen_ratio = float(current_units[symbol]) / frozen_ref
231
+ current_ratio = float(ratios[symbol].loc[timestamp]) / current_ref
232
+ denom = max(abs(frozen_ratio), 1e-12)
233
+ max_drift = max(max_drift, abs(current_ratio - frozen_ratio) / denom)
234
+ return max_drift
@@ -0,0 +1,274 @@
1
+ """
2
+ Alpha execution-contract certification helpers.
3
+
4
+ These helpers are intentionally lightweight and conservative. They do not try
5
+ to prove a strategy has no look-ahead bias from source text alone; they identify
6
+ which execution contract a file appears to require and what certification level
7
+ an already-run result can claim from its metadata.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import asdict, dataclass, field
13
+ from enum import IntEnum
14
+ from pathlib import Path
15
+ import re
16
+ from typing import Dict, Iterable, List, Optional, Sequence
17
+
18
+
19
+ class CertificationLevel(IntEnum):
20
+ LEGACY = 0
21
+ ACCOUNTING_REPLAY = 1
22
+ ENGINE_CAUSAL = 2
23
+ CROSS_BACKEND = 3
24
+ EXTERNAL_VALIDATION = 4
25
+
26
+
27
+ LEVEL_DESCRIPTIONS = {
28
+ CertificationLevel.LEGACY: "legacy_or_unspecified_execution_contract",
29
+ CertificationLevel.ACCOUNTING_REPLAY: "explicit_fills_accounted_but_fill_generation_not_certified",
30
+ CertificationLevel.ENGINE_CAUSAL: "engine_owned_causal_execution_with_oracle_or_kernel_parity",
31
+ CertificationLevel.CROSS_BACKEND: "native_engine_matches_native_event_on_known_scenarios",
32
+ CertificationLevel.EXTERNAL_VALIDATION: "external_or_lower_timeframe_validation_available",
33
+ }
34
+
35
+
36
+ INTRABAR_MARKERS = (
37
+ "exit_price",
38
+ "exit_type",
39
+ "stop_loss",
40
+ "stoploss",
41
+ "take_profit",
42
+ "takeprofit",
43
+ "trailing",
44
+ "trailing_stop",
45
+ "use_sl",
46
+ "use_tp",
47
+ "slpercent",
48
+ "tppercent",
49
+ "high[",
50
+ "low[",
51
+ )
52
+ FILL_REPLAY_MARKERS = ("fill_replay", "fills_df", "compact_fill", "bar_index", "sequence")
53
+ GRID_MARKERS = ("dca_ladder", "grid", "safety_order", "take_profit_price", "stop_loss_price")
54
+ NEXT_OPEN_MARKERS = ("next_open", "open[t+1]", "shift(1)", "open.shift")
55
+ CLOSE_TARGET_MARKERS = ("native_vectorized", "signal_notional", "pos_weight", "target_weight")
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class AlphaExecutionClassification:
60
+ alpha_id: str
61
+ path: str
62
+ required_engine: str
63
+ current_backend: str
64
+ certification_status: str
65
+ certification_level: int
66
+ markers: tuple[str, ...] = ()
67
+ notes: tuple[str, ...] = ()
68
+ uses_intrabar_high_low: bool = False
69
+ uses_stop: bool = False
70
+ uses_take_profit: bool = False
71
+ uses_trailing: bool = False
72
+ uses_custom_exit_price: bool = False
73
+ uses_explicit_fills: bool = False
74
+ uses_grid_or_dca: bool = False
75
+ metadata: Dict = field(default_factory=dict)
76
+
77
+ def to_dict(self) -> Dict:
78
+ return asdict(self)
79
+
80
+
81
+ def classify_alpha_source(source: str, *, alpha_id: str = "unknown", path: str = "") -> AlphaExecutionClassification:
82
+ text = source.lower()
83
+ markers = _matched_markers(text)
84
+ current_backend = _detect_current_backend(text)
85
+ uses_explicit_fills = any(marker in text for marker in FILL_REPLAY_MARKERS)
86
+ uses_grid_or_dca = any(marker in text for marker in GRID_MARKERS)
87
+ uses_stop = any(marker in text for marker in ("stop_loss", "stoploss", "slpercent", "use_sl"))
88
+ uses_take_profit = any(marker in text for marker in ("take_profit", "takeprofit", "tppercent", "use_tp"))
89
+ uses_trailing = "trailing" in text
90
+ uses_custom_exit_price = "exit_price" in text or "exit_type" in text
91
+ uses_intrabar_high_low = bool(re.search(r"\bhigh\s*\[|\blow\s*\[|df\s*\[\s*['\"]high|df\s*\[\s*['\"]low", text))
92
+
93
+ if uses_grid_or_dca:
94
+ required_engine = "event_lifecycle_v2"
95
+ level = CertificationLevel.LEGACY
96
+ status = "needs_specialized_event_or_nautilus_certification"
97
+ elif uses_explicit_fills and not (uses_stop or uses_take_profit or uses_trailing):
98
+ required_engine = "fill_replay_v1"
99
+ level = CertificationLevel.ACCOUNTING_REPLAY
100
+ status = "can_start_with_accounting_replay"
101
+ elif uses_stop or uses_take_profit or uses_trailing or uses_custom_exit_price or uses_intrabar_high_low:
102
+ required_engine = "intrabar_bracket_v1"
103
+ level = CertificationLevel.LEGACY
104
+ status = "requires_intrabar_migration"
105
+ elif any(marker in text for marker in NEXT_OPEN_MARKERS):
106
+ required_engine = "next_open_v1"
107
+ level = CertificationLevel.LEGACY
108
+ status = "requires_next_open_contract"
109
+ elif any(marker in text for marker in CLOSE_TARGET_MARKERS):
110
+ required_engine = "close_target_v2"
111
+ level = CertificationLevel.ENGINE_CAUSAL if current_backend in {"native_vectorized", "close_target_v2"} else CertificationLevel.LEGACY
112
+ status = "close_target_candidate"
113
+ else:
114
+ required_engine = "unknown"
115
+ level = CertificationLevel.LEGACY
116
+ status = "manual_review_required"
117
+
118
+ notes = _notes_for_classification(required_engine, current_backend, markers)
119
+ return AlphaExecutionClassification(
120
+ alpha_id=alpha_id,
121
+ path=path,
122
+ required_engine=required_engine,
123
+ current_backend=current_backend,
124
+ certification_status=status,
125
+ certification_level=int(level),
126
+ markers=tuple(markers),
127
+ notes=tuple(notes),
128
+ uses_intrabar_high_low=uses_intrabar_high_low,
129
+ uses_stop=uses_stop,
130
+ uses_take_profit=uses_take_profit,
131
+ uses_trailing=uses_trailing,
132
+ uses_custom_exit_price=uses_custom_exit_price,
133
+ uses_explicit_fills=uses_explicit_fills,
134
+ uses_grid_or_dca=uses_grid_or_dca,
135
+ )
136
+
137
+
138
+ def scan_alpha_directory(root: str | Path, *, suffixes: Sequence[str] = (".py", ".ipynb", ".md"), max_bytes: int = 2_000_000) -> List[AlphaExecutionClassification]:
139
+ base = Path(root)
140
+ if not base.exists():
141
+ raise FileNotFoundError(str(base))
142
+ out: list[AlphaExecutionClassification] = []
143
+ for path in sorted(p for p in base.rglob("*") if p.is_file() and p.suffix.lower() in suffixes):
144
+ if any(part.startswith(".") for part in path.relative_to(base).parts):
145
+ continue
146
+ if path.stat().st_size > max_bytes:
147
+ out.append(
148
+ AlphaExecutionClassification(
149
+ alpha_id=path.stem,
150
+ path=str(path),
151
+ required_engine="unknown",
152
+ current_backend="unknown",
153
+ certification_status="skipped_large_file",
154
+ certification_level=int(CertificationLevel.LEGACY),
155
+ notes=("file exceeds scanner max_bytes",),
156
+ )
157
+ )
158
+ continue
159
+ text = path.read_text(encoding="utf-8", errors="ignore")
160
+ out.append(classify_alpha_source(text, alpha_id=path.stem, path=str(path)))
161
+ return out
162
+
163
+
164
+ def certify_result_metadata(metadata: Dict) -> Dict:
165
+ engine = str(metadata.get("engine_id") or metadata.get("engine") or "").lower()
166
+ backend = str(metadata.get("backend") or metadata.get("backend_alias") or "").lower()
167
+ if engine == "fill_replay_v1":
168
+ level = CertificationLevel.ACCOUNTING_REPLAY
169
+ status = "accounting_certified"
170
+ elif engine == "intrabar_bracket_v1":
171
+ level = CertificationLevel.ENGINE_CAUSAL
172
+ status = "engine_causal_certified"
173
+ if metadata.get("cross_backend_parity_passed"):
174
+ level = CertificationLevel.CROSS_BACKEND
175
+ status = "cross_backend_certified"
176
+ elif backend == "nautilus" or "nautilus" in engine:
177
+ level = CertificationLevel.EXTERNAL_VALIDATION
178
+ status = "external_validation_route"
179
+ elif engine == "close_target_v2":
180
+ level = CertificationLevel.ENGINE_CAUSAL
181
+ status = "close_target_certified"
182
+ if str(metadata.get("certification_status", "")).startswith("uncertified"):
183
+ level = CertificationLevel.LEGACY
184
+ status = str(metadata.get("certification_status"))
185
+ else:
186
+ level = CertificationLevel.LEGACY
187
+ status = "uncertified_or_unknown"
188
+ return {
189
+ "engine_id": engine or "unknown",
190
+ "backend": backend or "unknown",
191
+ "certification_level": int(level),
192
+ "certification_label": f"LEVEL {int(level)}",
193
+ "certification_status": status,
194
+ "description": LEVEL_DESCRIPTIONS[level],
195
+ }
196
+
197
+
198
+ def build_alpha_certification_report(items: Iterable[AlphaExecutionClassification]) -> Dict:
199
+ rows = [item.to_dict() for item in items]
200
+ by_engine: Dict[str, int] = {}
201
+ by_status: Dict[str, int] = {}
202
+ for row in rows:
203
+ by_engine[row["required_engine"]] = by_engine.get(row["required_engine"], 0) + 1
204
+ by_status[row["certification_status"]] = by_status.get(row["certification_status"], 0) + 1
205
+ return {
206
+ "total": len(rows),
207
+ "by_required_engine": by_engine,
208
+ "by_status": by_status,
209
+ "items": rows,
210
+ }
211
+
212
+
213
+ def alpha_report_markdown(report: Dict) -> str:
214
+ lines = [
215
+ "# Alpha Execution Certification Report",
216
+ "",
217
+ f"- Total files scanned: `{report['total']}`",
218
+ "",
219
+ "## By Required Engine",
220
+ "",
221
+ "| Engine | Count |",
222
+ "|---|---:|",
223
+ ]
224
+ for engine, count in sorted(report["by_required_engine"].items()):
225
+ lines.append(f"| `{engine}` | {count} |")
226
+ lines.extend(["", "## By Status", "", "| Status | Count |", "|---|---:|"])
227
+ for status, count in sorted(report["by_status"].items()):
228
+ lines.append(f"| `{status}` | {count} |")
229
+ lines.extend(["", "## Files", "", "| Alpha | Required engine | Current backend | Status | Markers |", "|---|---|---|---|---|"])
230
+ for item in report["items"]:
231
+ markers = ", ".join(item["markers"][:8])
232
+ if len(item["markers"]) > 8:
233
+ markers += ", ..."
234
+ lines.append(
235
+ f"| `{item['alpha_id']}` | `{item['required_engine']}` | `{item['current_backend']}` | "
236
+ f"`{item['certification_status']}` | {markers or '-'} |"
237
+ )
238
+ return "\n".join(lines) + "\n"
239
+
240
+
241
+ def _matched_markers(text: str) -> list[str]:
242
+ all_markers = sorted(set(INTRABAR_MARKERS + FILL_REPLAY_MARKERS + GRID_MARKERS + NEXT_OPEN_MARKERS + CLOSE_TARGET_MARKERS))
243
+ return [marker for marker in all_markers if marker in text]
244
+
245
+
246
+ def _detect_current_backend(text: str) -> str:
247
+ if "nautilus_validation" in text or "backend=\"nautilus\"" in text or "backend='nautilus'" in text:
248
+ return "nautilus"
249
+ if "intrabar_bracket" in text:
250
+ return "native_intrabar"
251
+ if "fill_replay" in text:
252
+ return "fill_replay_v1"
253
+ if "native_event" in text:
254
+ return "native_event"
255
+ if "native_vectorized" in text:
256
+ return "native_vectorized"
257
+ if "%_equity" in text or "pct_equity" in text:
258
+ return "legacy_pct_equity"
259
+ if "backtestengine(" in text:
260
+ return "legacy"
261
+ return "unknown"
262
+
263
+
264
+ def _notes_for_classification(required_engine: str, current_backend: str, markers: Sequence[str]) -> list[str]:
265
+ notes: list[str] = []
266
+ if required_engine == "intrabar_bracket_v1" and current_backend in {"native_vectorized", "legacy", "legacy_pct_equity"}:
267
+ notes.append("intrabar markers found on a close-target/legacy route; migrate to intrabar intent or fill replay")
268
+ if required_engine == "fill_replay_v1":
269
+ notes.append("accounting can be validated from explicit fills, but fill generation remains alpha-owned")
270
+ if required_engine == "event_lifecycle_v2":
271
+ notes.append("multi-order/grid/DCA behavior should stay on event lifecycle or Nautilus validation")
272
+ if not markers:
273
+ notes.append("no execution-sensitive markers detected; manual review still required before production certification")
274
+ return notes
@@ -0,0 +1,155 @@
1
+ """Exchange/instrument quantity constraints shared by all backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Dict, Optional, Sequence, Union
7
+
8
+ import numpy as np
9
+
10
+ from .schema import InstrumentSpec
11
+
12
+
13
+ NumberOrMap = Union[float, Dict[str, float], None]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class QuantityConstraints:
18
+ """Per-symbol exchange quantity rules.
19
+
20
+ `contract_size` remains the PnL/notional multiplier. Fractional crypto
21
+ acceptance is controlled by `qty_step`/`lot_size`, `min_qty`, and
22
+ `min_notional`. QuantBT rounds target quantities down by default, matching
23
+ the conservative side of common exchange filters.
24
+ """
25
+
26
+ symbols: tuple[str, ...]
27
+ qty_step: np.ndarray
28
+ min_qty: np.ndarray
29
+ min_notional: np.ndarray
30
+
31
+ @property
32
+ def enabled(self) -> bool:
33
+ return bool(
34
+ np.any(self.qty_step > 0.0)
35
+ or np.any(self.min_qty > 0.0)
36
+ or np.any(self.min_notional > 0.0)
37
+ )
38
+
39
+ def as_dict(self) -> Dict[str, Dict[str, float]]:
40
+ return {
41
+ symbol: {
42
+ "qty_step": float(self.qty_step[i]),
43
+ "lot_size": float(self.qty_step[i]),
44
+ "min_qty": float(self.min_qty[i]),
45
+ "min_notional": float(self.min_notional[i]),
46
+ }
47
+ for i, symbol in enumerate(self.symbols)
48
+ }
49
+
50
+
51
+ def build_quantity_constraints(
52
+ symbols: Sequence[str],
53
+ *,
54
+ instruments: Optional[Union[Dict[str, InstrumentSpec], Sequence[InstrumentSpec]]] = None,
55
+ qty_step: NumberOrMap = None,
56
+ lot_size: NumberOrMap = None,
57
+ slot_size: NumberOrMap = None,
58
+ min_qty: NumberOrMap = None,
59
+ min_notional: NumberOrMap = None,
60
+ ) -> QuantityConstraints:
61
+ """Resolve quantity constraints from explicit kwargs and InstrumentSpec.
62
+
63
+ Explicit kwargs override `InstrumentSpec`. `slot_size` is accepted as a
64
+ backward-compatible alias for `lot_size`.
65
+ """
66
+
67
+ symbol_list = tuple(symbols)
68
+ inst_map = _instrument_map(instruments)
69
+ step_source = qty_step if qty_step is not None else (lot_size if lot_size is not None else slot_size)
70
+
71
+ steps = []
72
+ min_qtys = []
73
+ min_notionals = []
74
+ for symbol in symbol_list:
75
+ inst = inst_map.get(symbol)
76
+ default_step = 0.0 if inst is None else float(inst.lot_size)
77
+ default_min_qty = 0.0 if inst is None else float(inst.min_qty)
78
+ default_min_notional = 0.0 if inst is None else float(inst.min_notional)
79
+ steps.append(_value_for(step_source, symbol, default_step))
80
+ min_qtys.append(_value_for(min_qty, symbol, default_min_qty))
81
+ min_notionals.append(_value_for(min_notional, symbol, default_min_notional))
82
+
83
+ out = QuantityConstraints(
84
+ symbols=symbol_list,
85
+ qty_step=np.asarray(steps, dtype=np.float64),
86
+ min_qty=np.asarray(min_qtys, dtype=np.float64),
87
+ min_notional=np.asarray(min_notionals, dtype=np.float64),
88
+ )
89
+ if np.any(out.qty_step < 0.0) or np.any(out.min_qty < 0.0) or np.any(out.min_notional < 0.0):
90
+ raise ValueError("qty_step/lot_size, min_qty, and min_notional must be >= 0")
91
+ return out
92
+
93
+
94
+ def quantize_target_units_matrix(
95
+ target_units: np.ndarray,
96
+ prices: np.ndarray,
97
+ contract_sizes: np.ndarray,
98
+ constraints: QuantityConstraints,
99
+ ) -> np.ndarray:
100
+ """Round target-unit matrix down to exchange-acceptable quantities."""
101
+
102
+ if not constraints.enabled:
103
+ return np.ascontiguousarray(target_units, dtype=np.float64)
104
+ out = np.asarray(target_units, dtype=np.float64).copy(order="C")
105
+ prices_arr = np.asarray(prices, dtype=np.float64)
106
+ cs = np.asarray(contract_sizes, dtype=np.float64)
107
+ for j in range(out.shape[1]):
108
+ step = float(constraints.qty_step[j])
109
+ mnq = float(constraints.min_qty[j])
110
+ mnn = float(constraints.min_notional[j])
111
+ for i in range(out.shape[0]):
112
+ out[i, j] = quantize_signed_quantity(out[i, j], prices_arr[i, j], cs[j], step, mnq, mnn)
113
+ return np.ascontiguousarray(out, dtype=np.float64)
114
+
115
+
116
+ def quantize_signed_quantity(
117
+ qty: float,
118
+ price: float,
119
+ contract_size: float = 1.0,
120
+ qty_step: float = 0.0,
121
+ min_qty: float = 0.0,
122
+ min_notional: float = 0.0,
123
+ ) -> float:
124
+ """Round a signed quantity down and zero it if below exchange minima."""
125
+
126
+ q = float(qty)
127
+ if q == 0.0:
128
+ return 0.0
129
+ sign = 1.0 if q > 0.0 else -1.0
130
+ abs_q = abs(q)
131
+ if qty_step > 0.0:
132
+ abs_q = np.floor((abs_q / float(qty_step)) + 1e-12) * float(qty_step)
133
+ if abs_q <= 0.0:
134
+ return 0.0
135
+ if min_qty > 0.0 and abs_q + 1e-12 < min_qty:
136
+ return 0.0
137
+ if min_notional > 0.0 and abs_q * float(price) * float(contract_size) + 1e-12 < min_notional:
138
+ return 0.0
139
+ return sign * abs_q
140
+
141
+
142
+ def _instrument_map(instruments) -> Dict[str, InstrumentSpec]:
143
+ if instruments is None:
144
+ return {}
145
+ if isinstance(instruments, dict):
146
+ return {symbol: spec for symbol, spec in instruments.items() if spec is not None}
147
+ return {spec.symbol: spec for spec in instruments}
148
+
149
+
150
+ def _value_for(value: NumberOrMap, symbol: str, default: float) -> float:
151
+ if value is None:
152
+ return float(default)
153
+ if isinstance(value, dict):
154
+ return float(value.get(symbol, default))
155
+ return float(value)