diff --git a/core/results.py b/core/results.py index 93e322a..ac71499 100644 --- a/core/results.py +++ b/core/results.py @@ -206,7 +206,10 @@ def initial_margin(self) -> np.ndarray: def maintenance_margin(self) -> np.ndarray: return self.accounting.maintenance_margin - def full_report(self, trading_days: int = 365) -> Dict: + def full_report(self, trading_days: int = 365, scope: str = "auto") -> Dict: + if str(scope).lower().strip() not in {"auto", "full"}: + raise ValueError("NativeEventScoreResult supports scope='auto' or scope='full'") + from ..metrics.performance import compute_performance_metrics return compute_performance_metrics( diff --git a/quantbt_phase34_merge_gate.py b/quantbt_phase34_merge_gate.py new file mode 100644 index 0000000..088e393 --- /dev/null +++ b/quantbt_phase34_merge_gate.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import inspect + +import numpy as np +import pandas as pd + +import quantbt +from quantbt import EndpointConfig, OrderCommand, PreparedNativeEventStrategyRunner, QuantBTEndpoint +from quantbt.core.schema import OrderSide, OrderType, TimeInForce +from quantbt.optimization import ObjectiveResult, PreparedNativeEventStrategyEvaluator, ReportMetricObjective + + +class _GateStrategy: + def on_bar_close(self, context): + symbol = context.symbols[0] + if context.bar_index == 0: + return [ + OrderCommand( + timestamp=context.timestamp, + symbol=symbol, + side=OrderSide.BUY, + order_type=OrderType.MARKET, + qty=1.0, + tif=TimeInForce.IOC, + order_id="entry", + ) + ] + if context.bar_index == 4 and context.positions[symbol] > 0.0: + return [ + OrderCommand( + timestamp=context.timestamp, + symbol=symbol, + side=OrderSide.SELL, + order_type=OrderType.MARKET, + qty=abs(context.positions[symbol]), + tif=TimeInForce.IOC, + reduce_only=True, + order_id="exit", + ) + ] + return [] + + +def _bars() -> pd.DataFrame: + idx = pd.date_range("2024-01-01", periods=12, freq="1h", tz="UTC") + close = pd.Series(100.0 + np.sin(np.arange(len(idx)) / 2.0), index=idx) + return pd.DataFrame( + { + "open": close, + "high": close + 2.0, + "low": close - 2.0, + "close": close, + "volume": 1_000.0, + }, + index=idx, + ) + + +def _assert(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def main() -> None: + fields = EndpointConfig.__dataclass_fields__ + _assert( + all(name in fields for name in ("reactive_kernel_mode", "audit_sink", "audit_sink_path")), + "EndpointConfig missing Phase 34 fields", + ) + print("EndpointConfig fields: PASSED") + + _assert(hasattr(QuantBTEndpoint, "prepare_native_event_strategy"), "prepare_native_event_strategy missing") + _assert(hasattr(quantbt, "PreparedNativeEventStrategyRunner"), "PreparedNativeEventStrategyRunner missing") + _assert(quantbt.PreparedNativeEventStrategyRunner is PreparedNativeEventStrategyRunner, "Prepared runner export mismatch") + print("Prepared endpoint API: PASSED") + + endpoint = QuantBTEndpoint.native_event_strategy(initial_capital=10_000, leverage=10, use_funding=False) + prepared = endpoint.prepare_native_event_strategy(data=_bars(), symbols=["BTC"]) + _assert(isinstance(prepared, PreparedNativeEventStrategyRunner), "prepared runner type mismatch") + score = prepared.score(_GateStrategy()) + _assert(score.metadata["reactive_kernel_mode"] == "single_pass", "prepared score did not use single_pass") + print("prepared.score(): PASSED") + + signature = inspect.signature(score.full_report) + _assert("scope" in signature.parameters, "NativeEventScoreResult.full_report missing scope") + score.full_report(scope="auto") + print("Score/full_report signature: PASSED") + + def strategy_factory(_params): + return _GateStrategy() + + def objective_builder(result, params): + objective = ReportMetricObjective(value_metrics=("sharpe",), scope="auto") + return objective(result, params) + + evaluator = PreparedNativeEventStrategyEvaluator( + runner=prepared, + strategy_factory=strategy_factory, + objective_builder=objective_builder, + ) + objective = evaluator.evaluate({}) + _assert(isinstance(objective, ObjectiveResult), "Prepared evaluator did not return ObjectiveResult") + print("Prepared evaluator import: PASSED") + + objective = ReportMetricObjective(value_metrics=("sharpe",), scope="auto")(score, {}) + _assert(isinstance(objective, ObjectiveResult), "ReportMetricObjective(score) failed") + print("ReportMetricObjective(score): PASSED") + print("PHASE 34 MERGE GATE: PASSED") + + +if __name__ == "__main__": + main() diff --git a/tests/test_phase34b_native_event_prepared_score.py b/tests/test_phase34b_native_event_prepared_score.py index 4e52dde..9e81403 100644 --- a/tests/test_phase34b_native_event_prepared_score.py +++ b/tests/test_phase34b_native_event_prepared_score.py @@ -1,12 +1,15 @@ from __future__ import annotations +import inspect + import numpy as np import pandas as pd -from quantbt import QuantBTEndpoint +import quantbt +from quantbt import EndpointConfig, PreparedNativeEventStrategyRunner, QuantBTEndpoint from quantbt.core.orders import OrderCommand from quantbt.core.schema import OrderSide, OrderType, TimeInForce -from quantbt.optimization import ObjectiveResult, PreparedNativeEventStrategyEvaluator +from quantbt.optimization import ObjectiveResult, PreparedNativeEventStrategyEvaluator, ReportMetricObjective def _bars(n: int = 16) -> pd.DataFrame: @@ -141,3 +144,29 @@ def objective_builder(result, params): assert isinstance(objective, ObjectiveResult) assert evaluator.last_result.metadata["engine"] == "event_v2_reactive_score" assert prepared.metadata["scores"] == 1 + + +def test_public_native_event_phase34_contract_is_available_from_quantbt(): + fields = EndpointConfig.__dataclass_fields__ + + assert hasattr(quantbt.QuantBTEndpoint, "prepare_native_event_strategy") + assert hasattr(quantbt, "PreparedNativeEventStrategyRunner") + assert PreparedNativeEventStrategyRunner is quantbt.PreparedNativeEventStrategyRunner + assert "reactive_kernel_mode" in fields + assert "audit_sink" in fields + assert "audit_sink_path" in fields + + +def test_report_metric_objective_accepts_native_event_score_result_scope_contract(): + df = _bars() + endpoint = QuantBTEndpoint.native_event_strategy(initial_capital=10_000, leverage=10, use_funding=False) + prepared = endpoint.prepare_native_event_strategy(data=df, symbols=["BTC"]) + score_result = prepared.score(TwoTradeStrategy(entry_bar=0, exit_bar=5)) + + assert "scope" in inspect.signature(score_result.full_report).parameters + objective = ReportMetricObjective(value_metrics=("sharpe",), scope="auto") + result = objective(score_result, {"entry_bar": 0, "exit_bar": 5}) + + assert isinstance(result, ObjectiveResult) + assert result.values == (score_result.metrics["sharpe"],) + assert result.metrics["num_trades"] == score_result.metrics["num_trades"] diff --git a/upgrade/implement.md b/upgrade/implement.md index 804c081..3f2e7e2 100644 --- a/upgrade/implement.md +++ b/upgrade/implement.md @@ -6910,3 +6910,43 @@ Scope note: - Benchmark report records wall time, CPU time, peak RSS, Python heap peak, NumPy allocated bytes, object count, ledger bytes, command count, fill count, report construction time, and stage timings. + +Merge regression fix on `dev`: + +- Cherry-picked Phase 34A, 34B, and 34C onto `dev` after detecting that the + native-event public endpoint integration was missing from the research + branch. +- Restored public exports and endpoint contracts: + - `PreparedNativeEventStrategyRunner`; + - `QuantBTEndpoint.prepare_native_event_strategy(...)`; + - `EndpointConfig.reactive_kernel_mode`; + - `EndpointConfig.audit_sink`; + - `EndpointConfig.audit_sink_path`. +- Added `scope="auto"` compatibility to + `NativeEventScoreResult.full_report(...)`, matching the public result + contract expected by `ReportMetricObjective`. +- Added `quantbt_phase34_merge_gate.py` so future merges can verify public + Phase 34 integration directly. +- Added regression tests covering: + - public `quantbt` imports; + - endpoint Phase 34 fields; + - prepared native-event runner availability; + - `prepared.score(...)`; + - `PreparedNativeEventStrategyEvaluator`; + - `ReportMetricObjective(score_result)`. + +Validation on `dev`: + +```bash +MPLCONFIGDIR=/tmp PYTHONPATH=/root/bobby/pool_alpha poetry run python3 quantbt_phase34_merge_gate.py +# PHASE 34 MERGE GATE: PASSED + +MPLCONFIGDIR=/tmp PYTHONPATH=/root/bobby/pool_alpha poetry run pytest -q \ + quantbt/tests/test_phase34a_native_event_artifacts.py \ + quantbt/tests/test_phase34b_native_event_prepared_score.py \ + quantbt/tests/test_phase34c_native_event_single_pass.py +# 11 passed + +MPLCONFIGDIR=/tmp PYTHONPATH=/root/bobby/pool_alpha poetry run pytest -q quantbt/tests +# 549 passed, 1 skipped +```