From 3cf4e49eec24c2c2eb68390047f755018e54e4dc Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:33:58 +0800 Subject: [PATCH] refactor(qpk): split value-target presentation from execution API B06/N05 first slice: add presentation types and build_value_target_execution_runtime_plan without dashboard-layout kwargs. Keep build_value_target_runtime_plan as a compatibility facade so existing platform imports/payloads stay behavior-equivalent. --- ...b06_execution_presentation_facade.zh-CN.md | 17 +++ .../common/execution_translation.py | 52 +++++++- .../common/presentation/__init__.py | 24 ++++ .../common/presentation/value_target_plan.py | 123 ++++++++++++++++++ .../test_value_target_presentation_facade.py | 123 ++++++++++++++++++ 5 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 docs/b06_execution_presentation_facade.zh-CN.md create mode 100644 src/quant_platform_kit/common/presentation/__init__.py create mode 100644 src/quant_platform_kit/common/presentation/value_target_plan.py create mode 100644 tests/test_value_target_presentation_facade.py diff --git a/docs/b06_execution_presentation_facade.zh-CN.md b/docs/b06_execution_presentation_facade.zh-CN.md new file mode 100644 index 00000000..51ac7038 --- /dev/null +++ b/docs/b06_execution_presentation_facade.zh-CN.md @@ -0,0 +1,17 @@ +# B06:value-target 执行 API 与展示布局分离(第一刀) + +## 做了什么 +- 新增 `quant_platform_kit.common.presentation`: + - `ValueTargetExecutionSemantics`(阈值/时序/数值) + - `ValueTargetDisplayAnnotations`(文案) + - `ValueTargetPlanPresentation`(`portfolio_rows_layout` / `execution_fields`) +- 新增 `build_value_target_execution_runtime_plan`:**不**在签名上接收 dashboard 布局 kwargs +- 保留 `build_value_target_runtime_plan` 为兼容 facade(旧 kwargs → Presentation) + +## 未做(follow-up) +- 未删除 `ValueTargetExecutionAnnotations` 混合字段;未迁移平台 `decision_mapper` 调用方 +- 未把 SOXL 公式移出 `runtime_inputs.py`(属 B03/B07) +- 未改阈值/数量/时序数值行为 + +## 验证 +`pytest tests/test_strategy_contracts.py tests/test_value_target_presentation_facade.py` diff --git a/src/quant_platform_kit/common/execution_translation.py b/src/quant_platform_kit/common/execution_translation.py index 48a6c364..aed30275 100644 --- a/src/quant_platform_kit/common/execution_translation.py +++ b/src/quant_platform_kit/common/execution_translation.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Any +from .presentation.value_target_plan import ValueTargetPlanPresentation from .strategy_contracts import ( PositionTarget, StrategyContractValidationError, @@ -220,18 +221,23 @@ def translate_decision_to_target_mode( ) -def build_value_target_runtime_plan( +def build_value_target_execution_runtime_plan( decision: StrategyDecision, *, strategy_profile: str, portfolio_inputs: ValueTargetPortfolioInputs, strategy_symbols_order: str = "risk_safe_income", - portfolio_rows_layout: tuple[str, ...] = ("risk_safe", "income"), - execution_fields: tuple[str, ...] | None = None, - execution_defaults: Mapping[str, Any] | None = None, annotations: ValueTargetExecutionAnnotations | None = None, include_sellable_quantities: bool | None = None, + presentation: ValueTargetPlanPresentation | None = None, ) -> dict[str, Any]: + """Build the value-target runtime plan without dashboard-layout kwargs. + + Presentation knobs (portfolio row layout / field filters) are optional via + ``ValueTargetPlanPresentation``. Callers that only need execution semantics + should omit ``presentation`` and use the default structural layout. + """ + presentation = presentation or ValueTargetPlanPresentation() execution_plan = build_value_target_execution_plan( decision, strategy_profile=strategy_profile, @@ -245,7 +251,7 @@ def build_value_target_runtime_plan( total_equity=float(portfolio_inputs.total_equity), liquid_cash=float(portfolio_inputs.liquid_cash), strategy_symbols_order=strategy_symbols_order, - portfolio_rows_layout=portfolio_rows_layout, + portfolio_rows_layout=presentation.portfolio_rows_layout, ) return build_value_target_plan_payload( strategy_profile=strategy_profile, @@ -256,6 +262,38 @@ def build_value_target_runtime_plan( if include_sellable_quantities is None else bool(include_sellable_quantities) ), - execution_fields=execution_fields, - execution_defaults=execution_defaults, + execution_fields=presentation.execution_fields, + execution_defaults=presentation.execution_defaults, + ) + + +def build_value_target_runtime_plan( + decision: StrategyDecision, + *, + strategy_profile: str, + portfolio_inputs: ValueTargetPortfolioInputs, + strategy_symbols_order: str = "risk_safe_income", + portfolio_rows_layout: tuple[str, ...] = ("risk_safe", "income"), + execution_fields: tuple[str, ...] | None = None, + execution_defaults: Mapping[str, Any] | None = None, + annotations: ValueTargetExecutionAnnotations | None = None, + include_sellable_quantities: bool | None = None, +) -> dict[str, Any]: + """Compatibility facade: same kwargs/payload as before B06. + + Prefer ``build_value_target_execution_runtime_plan`` for new code so layout + and field filters are not mixed into the execution API surface. + """ + return build_value_target_execution_runtime_plan( + decision, + strategy_profile=strategy_profile, + portfolio_inputs=portfolio_inputs, + strategy_symbols_order=strategy_symbols_order, + annotations=annotations, + include_sellable_quantities=include_sellable_quantities, + presentation=ValueTargetPlanPresentation( + portfolio_rows_layout=portfolio_rows_layout, + execution_fields=execution_fields, + execution_defaults=execution_defaults, + ), ) diff --git a/src/quant_platform_kit/common/presentation/__init__.py b/src/quant_platform_kit/common/presentation/__init__.py new file mode 100644 index 00000000..305c25e6 --- /dev/null +++ b/src/quant_platform_kit/common/presentation/__init__.py @@ -0,0 +1,24 @@ +"""Presentation helpers for strategy/execution contracts. + +Display layout and dashboard copy belong here (or call sites), not in the +pure execution planning API. Compatibility facades may still emit legacy +payloads that mix both concerns. +""" + +from .value_target_plan import ( + ValueTargetDisplayAnnotations, + ValueTargetExecutionSemantics, + ValueTargetPlanPresentation, + display_annotations_from_execution_annotations, + execution_semantics_from_annotations, + merge_value_target_execution_annotations, +) + +__all__ = [ + "ValueTargetDisplayAnnotations", + "ValueTargetExecutionSemantics", + "ValueTargetPlanPresentation", + "display_annotations_from_execution_annotations", + "execution_semantics_from_annotations", + "merge_value_target_execution_annotations", +] diff --git a/src/quant_platform_kit/common/presentation/value_target_plan.py b/src/quant_platform_kit/common/presentation/value_target_plan.py new file mode 100644 index 00000000..55e8f791 --- /dev/null +++ b/src/quant_platform_kit/common/presentation/value_target_plan.py @@ -0,0 +1,123 @@ +"""Split value-target execution semantics from display/layout knobs. + +``ValueTargetExecutionAnnotations`` in ``strategy_contracts`` remains the +compatibility wire type (all fields). New callers should prefer: +- ``ValueTargetExecutionSemantics`` for thresholds / timing / numeric gates +- ``ValueTargetDisplayAnnotations`` for dashboard/signal copy +- ``ValueTargetPlanPresentation`` for portfolio row layout / field selection +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +from quant_platform_kit.common.strategy_contracts import ValueTargetExecutionAnnotations + + +@dataclass(frozen=True) +class ValueTargetExecutionSemantics: + """Execution-relevant annotation fields (no dashboard layout/copy).""" + + trade_threshold_value: float + reserved_cash: float = 0.0 + signal_date: str | None = None + effective_date: str | None = None + execution_timing_contract: str | None = None + execution_calendar_source: str | None = None + signal_effective_after_trading_days: int | None = None + benchmark_symbol: str | None = None + benchmark_price: float | None = None + long_trend_value: float | None = None + exit_line: float | None = None + active_risk_asset: str | None = None + current_min_trade: float | None = None + investable_cash: float | None = None + + +@dataclass(frozen=True) +class ValueTargetDisplayAnnotations: + """Human-facing copy for notifications / dashboards.""" + + signal_display: str | None = None + status_display: str | None = None + dashboard_text: str | None = None + separator: str | None = None + deploy_ratio_text: str | None = None + income_ratio_text: str | None = None + income_locked_ratio_text: str | None = None + + +@dataclass(frozen=True) +class ValueTargetPlanPresentation: + """Legacy payload shaping (row layout + optional execution field filter).""" + + portfolio_rows_layout: tuple[str, ...] = ("risk_safe", "income") + execution_fields: tuple[str, ...] | None = None + execution_defaults: Mapping[str, Any] | None = None + + +def execution_semantics_from_annotations( + annotations: ValueTargetExecutionAnnotations, +) -> ValueTargetExecutionSemantics: + return ValueTargetExecutionSemantics( + trade_threshold_value=float(annotations.trade_threshold_value), + reserved_cash=float(annotations.reserved_cash), + signal_date=annotations.signal_date, + effective_date=annotations.effective_date, + execution_timing_contract=annotations.execution_timing_contract, + execution_calendar_source=annotations.execution_calendar_source, + signal_effective_after_trading_days=annotations.signal_effective_after_trading_days, + benchmark_symbol=annotations.benchmark_symbol, + benchmark_price=annotations.benchmark_price, + long_trend_value=annotations.long_trend_value, + exit_line=annotations.exit_line, + active_risk_asset=annotations.active_risk_asset, + current_min_trade=annotations.current_min_trade, + investable_cash=annotations.investable_cash, + ) + + +def display_annotations_from_execution_annotations( + annotations: ValueTargetExecutionAnnotations, +) -> ValueTargetDisplayAnnotations: + return ValueTargetDisplayAnnotations( + signal_display=annotations.signal_display, + status_display=annotations.status_display, + dashboard_text=annotations.dashboard_text, + separator=annotations.separator, + deploy_ratio_text=annotations.deploy_ratio_text, + income_ratio_text=annotations.income_ratio_text, + income_locked_ratio_text=annotations.income_locked_ratio_text, + ) + + +def merge_value_target_execution_annotations( + semantics: ValueTargetExecutionSemantics, + display: ValueTargetDisplayAnnotations | None = None, +) -> ValueTargetExecutionAnnotations: + display = display or ValueTargetDisplayAnnotations() + return ValueTargetExecutionAnnotations( + trade_threshold_value=float(semantics.trade_threshold_value), + reserved_cash=float(semantics.reserved_cash), + signal_display=display.signal_display, + status_display=display.status_display, + dashboard_text=display.dashboard_text, + signal_date=semantics.signal_date, + effective_date=semantics.effective_date, + execution_timing_contract=semantics.execution_timing_contract, + execution_calendar_source=semantics.execution_calendar_source, + signal_effective_after_trading_days=semantics.signal_effective_after_trading_days, + separator=display.separator, + benchmark_symbol=semantics.benchmark_symbol, + benchmark_price=semantics.benchmark_price, + long_trend_value=semantics.long_trend_value, + exit_line=semantics.exit_line, + deploy_ratio_text=display.deploy_ratio_text, + income_ratio_text=display.income_ratio_text, + income_locked_ratio_text=display.income_locked_ratio_text, + active_risk_asset=semantics.active_risk_asset, + current_min_trade=semantics.current_min_trade, + investable_cash=semantics.investable_cash, + ) diff --git a/tests/test_value_target_presentation_facade.py b/tests/test_value_target_presentation_facade.py new file mode 100644 index 00000000..65a499c0 --- /dev/null +++ b/tests/test_value_target_presentation_facade.py @@ -0,0 +1,123 @@ +"""B06: execution vs presentation facade — behavior-equivalent payloads.""" + +from __future__ import annotations + +import unittest + +from quant_platform_kit.common.execution_translation import ( + ValueTargetPortfolioInputs, + build_value_target_execution_runtime_plan, + build_value_target_runtime_plan, +) +from quant_platform_kit.common.presentation import ( + ValueTargetDisplayAnnotations, + ValueTargetExecutionSemantics, + ValueTargetPlanPresentation, + display_annotations_from_execution_annotations, + execution_semantics_from_annotations, + merge_value_target_execution_annotations, +) +from quant_platform_kit.common.strategy_contracts import ( + PositionTarget, + StrategyDecision, + ValueTargetExecutionAnnotations, +) + + +def _decision() -> StrategyDecision: + return StrategyDecision( + positions=( + PositionTarget(symbol="SOXL", target_value=100.0), + PositionTarget(symbol="BOXX", target_value=50.0, role="safe_haven"), + ), + diagnostics={ + "execution_annotations": { + "trade_threshold_value": 25.0, + "reserved_cash": 10.0, + "signal_display": "risk-on", + "dashboard_text": "line-a\nline-b", + } + }, + ) + + +def _inputs() -> ValueTargetPortfolioInputs: + return ValueTargetPortfolioInputs( + market_values={"SOXL": 80.0, "BOXX": 40.0}, + quantities={"SOXL": 1.0, "BOXX": 2.0}, + total_equity=200.0, + liquid_cash=70.0, + ) + + +class ValueTargetPresentationFacadeTests(unittest.TestCase): + def test_compat_runtime_plan_matches_execution_api_with_same_presentation(self) -> None: + decision = _decision() + inputs = _inputs() + annotations = ValueTargetExecutionAnnotations( + trade_threshold_value=25.0, + reserved_cash=10.0, + signal_display="risk-on", + dashboard_text="line-a\nline-b", + ) + legacy = build_value_target_runtime_plan( + decision, + strategy_profile="soxl_soxx_trend_income", + portfolio_inputs=inputs, + portfolio_rows_layout=("risk_safe", "income"), + annotations=annotations, + ) + modern = build_value_target_execution_runtime_plan( + decision, + strategy_profile="soxl_soxx_trend_income", + portfolio_inputs=inputs, + annotations=annotations, + presentation=ValueTargetPlanPresentation( + portfolio_rows_layout=("risk_safe", "income"), + ), + ) + self.assertEqual(legacy, modern) + self.assertEqual(legacy["execution"]["signal_display"], "risk-on") + self.assertEqual(legacy["execution"]["trade_threshold_value"], 25.0) + + def test_execution_api_omits_layout_kwargs_and_uses_default_presentation(self) -> None: + payload = build_value_target_execution_runtime_plan( + _decision(), + strategy_profile="soxl_soxx_trend_income", + portfolio_inputs=_inputs(), + annotations=ValueTargetExecutionAnnotations( + trade_threshold_value=25.0, + reserved_cash=0.0, + ), + ) + self.assertIn("portfolio", payload) + self.assertIn("execution", payload) + # Default layout still produces portfolio_rows (structural, not dashboard copy). + self.assertIn("portfolio_rows", payload["portfolio"]) + + def test_semantics_display_round_trip(self) -> None: + original = ValueTargetExecutionAnnotations( + trade_threshold_value=12.5, + reserved_cash=3.0, + signal_display="hold", + dashboard_text="dash", + separator="|", + signal_date="2026-10-08", + ) + merged = merge_value_target_execution_annotations( + execution_semantics_from_annotations(original), + display_annotations_from_execution_annotations(original), + ) + self.assertEqual(merged, original) + self.assertIsInstance( + execution_semantics_from_annotations(original), + ValueTargetExecutionSemantics, + ) + self.assertIsInstance( + display_annotations_from_execution_annotations(original), + ValueTargetDisplayAnnotations, + ) + + +if __name__ == "__main__": + unittest.main()