Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/b06_execution_presentation_facade.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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`
52 changes: 45 additions & 7 deletions src/quant_platform_kit/common/execution_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
),
)
24 changes: 24 additions & 0 deletions src/quant_platform_kit/common/presentation/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
123 changes: 123 additions & 0 deletions src/quant_platform_kit/common/presentation/value_target_plan.py
Original file line number Diff line number Diff line change
@@ -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,
)
123 changes: 123 additions & 0 deletions tests/test_value_target_presentation_facade.py
Original file line number Diff line number Diff line change
@@ -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()
Loading