diff --git a/README.md b/README.md index 534f775..0d7b198 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ It is an execution layer, not a strategy research repository. Strategy logic com - Handles broker/API connectivity, dry-run checks, notifications, and deployment settings. - Must keep credentials in GitHub Secrets, cloud secret stores, or the broker-specific secret system, never in Git. - Should start with dry-run or paper mode before any live order path is enabled. +- The account new-risk gate consumes an explicit account snapshot first and the + current portfolio status second. It does not derive production drift from a + shared research PerformanceStore: absent optional drift is not fabricated, + while missing required account inputs and review, critical, or invalid + evidence remain fail-closed for new buys. `GET /account-snapshot` is a separate, read-only diagnostic and stays disabled unless `LONGBRIDGE_ACCOUNT_SNAPSHOT_ENABLED=true` is set exactly. It returns diff --git a/application/account_new_risk_gate_support.py b/application/account_new_risk_gate_support.py index 5f4b2a7..e2c3f69 100644 --- a/application/account_new_risk_gate_support.py +++ b/application/account_new_risk_gate_support.py @@ -127,45 +127,6 @@ def _resolve_production_drift_status(portfolio: Mapping[str, Any], projection: M return str(raw).strip() -def maybe_inject_production_drift_status( - portfolio: Mapping[str, Any], - *, - strategy_profile: str | None, - domain: str | None, - store=None, -) -> dict[str, Any]: - """Return a shallow-copied portfolio with production_drift_status filled from store when absent. - - Explicit account_new_risk_snapshot.production_drift_status or portfolio.production_drift_status wins. - Missing profile/domain, parked store, or any exception → leave unchanged (omit; never invent CRITICAL). - Never optimizes or grants live. - """ - out = dict(portfolio) - projection = dict(out.get("account_new_risk_snapshot") or {}) - if _resolve_production_drift_status(out, projection) is not None: - return out - profile = str(strategy_profile or "").strip() or None - domain_key = str(domain or "").strip() or None - if not profile or not domain_key: - return out - try: - from quant_platform_kit.risk.production_drift_new_risk import ( - resolve_production_drift_status_from_store, - ) - - status = resolve_production_drift_status_from_store( - strategy_profile=profile, - domain=domain_key, - store=store, - ) - except Exception: - return out - if status: - projection["production_drift_status"] = status - out["account_new_risk_snapshot"] = projection - return out - - def build_snapshot_from_portfolio( portfolio: Mapping[str, Any], *, diff --git a/application/rebalance_service.py b/application/rebalance_service.py index c5e74b2..01ccbd7 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -952,29 +952,6 @@ def submit_claimed_order(order_intent): _lot_sizes = fetch_lot_sizes(quote_context, _candidates) except Exception: _lot_sizes = {} - from application.account_new_risk_gate_support import maybe_inject_production_drift_status - from strategy_registry import LONGBRIDGE_PLATFORM, resolve_strategy_definition - - domain = "" - metadata = portfolio.get("metadata") if isinstance(portfolio, dict) else None - if isinstance(metadata, dict): - domain = str(metadata.get("strategy_domain") or "").strip() - if not domain: - try: - domain = str( - resolve_strategy_definition( - getattr(config, "strategy_profile", None), - platform_id=LONGBRIDGE_PLATFORM, - ).domain - or "" - ) - except Exception: - domain = "" - portfolio = maybe_inject_production_drift_status( - portfolio, - strategy_profile=getattr(config, "strategy_profile", None), - domain=domain or None, - ) execution_result = execute_rebalance_cycle( trade_context=trade_context, plan=plan, diff --git a/tests/test_account_new_risk_gate.py b/tests/test_account_new_risk_gate.py index c10ee8d..c886337 100644 --- a/tests/test_account_new_risk_gate.py +++ b/tests/test_account_new_risk_gate.py @@ -26,7 +26,6 @@ build_account_new_risk_snapshot, build_snapshot_from_portfolio, evaluate_portfolio_new_risk_admission, - maybe_inject_production_drift_status, new_risk_buy_prohibited, set_cycle_snapshot, ) @@ -100,77 +99,40 @@ def test_production_drift_critical_prohibits_new_risk(self) -> None: self.assertEqual(result.disposition, NewRiskDisposition.NEW_RISK_PROHIBITED) self.assertIn("PRODUCTION_DRIFT_CRITICAL", result.reason_codes) + def test_invalid_production_drift_status_prohibits_new_risk(self) -> None: + result = evaluate_portfolio_new_risk_admission( + { + "total_strategy_equity": 50_000.0, + "account_new_risk_snapshot": {"production_drift_status": "historical_simulation"}, + } + ) + self.assertEqual(result.disposition, NewRiskDisposition.NEW_RISK_PROHIBITED) + def test_absent_production_drift_status_still_allows_when_healthy(self) -> None: portfolio = {"total_strategy_equity": 50_000.0} result = evaluate_portfolio_new_risk_admission(portfolio) self.assertEqual(result.disposition, NewRiskDisposition.ALLOW_NEW_RISK) - def test_maybe_inject_fills_production_drift_from_store_resolver(self) -> None: - portfolio = {"total_strategy_equity": 50_000.0} - with patch( - "quant_platform_kit.risk.production_drift_new_risk.resolve_production_drift_status_from_store", - return_value="review", - ): - injected = maybe_inject_production_drift_status( - portfolio, - strategy_profile="demo_profile", - domain="us_equity", - ) - self.assertEqual( - injected["account_new_risk_snapshot"]["production_drift_status"], - "review", - ) - result = evaluate_portfolio_new_risk_admission(injected) - self.assertEqual(result.disposition, NewRiskDisposition.NEW_RISK_PROHIBITED) - self.assertIn("PRODUCTION_DRIFT_REVIEW", result.reason_codes) - - def test_maybe_inject_does_not_overwrite_explicit_status(self) -> None: + def test_explicit_snapshot_status_wins_over_portfolio_status(self) -> None: portfolio = { "total_strategy_equity": 50_000.0, "account_new_risk_snapshot": {"production_drift_status": "critical"}, + "production_drift_status": "review", } - with patch( - "quant_platform_kit.risk.production_drift_new_risk.resolve_production_drift_status_from_store", - return_value="review", - ) as resolve: - injected = maybe_inject_production_drift_status( - portfolio, - strategy_profile="demo_profile", - domain="us_equity", - ) - resolve.assert_not_called() - self.assertEqual( - injected["account_new_risk_snapshot"]["production_drift_status"], - "critical", - ) + snapshot = build_snapshot_from_portfolio(portfolio) + self.assertEqual(snapshot.production_drift_status, "critical") + self.assertTrue(new_risk_buy_prohibited(evaluate_portfolio_new_risk_admission(portfolio))) - def test_maybe_inject_leaves_absent_when_resolver_returns_none(self) -> None: - portfolio = {"total_strategy_equity": 50_000.0} - with patch( - "quant_platform_kit.risk.production_drift_new_risk.resolve_production_drift_status_from_store", - return_value=None, - ): - injected = maybe_inject_production_drift_status( - portfolio, - strategy_profile="demo_profile", - domain="us_equity", - ) - self.assertNotIn("account_new_risk_snapshot", injected) - result = evaluate_portfolio_new_risk_admission(injected) - self.assertEqual(result.disposition, NewRiskDisposition.ALLOW_NEW_RISK) + def test_portfolio_status_is_used_when_snapshot_omits_it(self) -> None: + portfolio = {"total_strategy_equity": 50_000.0, "production_drift_status": "review"} + snapshot = build_snapshot_from_portfolio(portfolio) + self.assertEqual(snapshot.production_drift_status, "review") + self.assertTrue(new_risk_buy_prohibited(evaluate_portfolio_new_risk_admission(portfolio))) - def test_maybe_inject_leaves_absent_when_resolver_raises(self) -> None: - portfolio = {"total_strategy_equity": 50_000.0} - with patch( - "quant_platform_kit.risk.production_drift_new_risk.resolve_production_drift_status_from_store", - side_effect=RuntimeError("store unavailable"), - ): - injected = maybe_inject_production_drift_status( - portfolio, - strategy_profile="demo_profile", - domain="us_equity", - ) - self.assertNotIn("account_new_risk_snapshot", injected) + def test_rebalance_service_has_no_research_store_drift_injection(self) -> None: + source = (ROOT / "application" / "rebalance_service.py").read_text(encoding="utf-8") + self.assertNotIn("maybe_inject_production_drift_status", source) + self.assertNotIn("resolve_production_drift_status_from_store", source) def test_unknown_pending_orders_prohibits_new_risk(self) -> None: portfolio = {"total_strategy_equity": 50_000.0, "unknown_pending_orders": True} diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 2821a2a..cff5e61 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -242,8 +242,12 @@ def record_marker(self, *_args, **_kwargs): notify_no_trade_cycles=False, ) - result = rebalance_service.run_strategy(runtime=runtime, config=config) + with patch( + "quant_platform_kit.risk.production_drift_new_risk.resolve_production_drift_status_from_store" + ) as resolver: + result = rebalance_service.run_strategy(runtime=runtime, config=config) + resolver.assert_not_called() self.assertEqual(result.dry_run_orders, ()) self.assertEqual(observed["submit"], 0) self.assertEqual(observed["notify"], 0)