Skip to content

Commit 5541b91

Browse files
authored
Handle missing Firstrade trade threshold (#75)
1 parent 1d9d777 commit 5541b91

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

decision_mapper.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from quant_platform_kit.strategy_contracts import (
88
PositionTarget,
9+
StrategyContractValidationError,
910
StrategyDecision,
1011
ValueTargetExecutionAnnotations,
1112
build_value_target_execution_annotations,
@@ -244,7 +245,23 @@ def _normalize_to_value_decision(
244245

245246

246247
def _build_annotations(decision: StrategyDecision, *, portfolio_inputs) -> ValueTargetExecutionAnnotations:
247-
annotations = build_value_target_execution_annotations(decision)
248+
try:
249+
annotations = build_value_target_execution_annotations(decision)
250+
except StrategyContractValidationError as exc:
251+
if "requires trade_threshold_value" not in str(exc):
252+
raise
253+
diagnostics = dict(decision.diagnostics)
254+
raw_annotations = diagnostics.get("execution_annotations")
255+
execution_annotations = (
256+
dict(raw_annotations) if isinstance(raw_annotations, Mapping) else {}
257+
)
258+
execution_annotations["trade_threshold_value"] = _default_threshold_value(
259+
float(portfolio_inputs.total_equity)
260+
)
261+
diagnostics["execution_annotations"] = execution_annotations
262+
annotations = build_value_target_execution_annotations(
263+
replace(decision, diagnostics=diagnostics)
264+
)
248265
investable_cash = annotations.investable_cash
249266
if investable_cash is None:
250267
investable_cash = max(0.0, float(portfolio_inputs.liquid_cash) - annotations.reserved_cash)

tests/test_decision_mapper.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,50 @@ def test_platform_reserved_cash_policy_does_not_lower_strategy_reserve():
7070

7171
assert plan["execution"]["reserved_cash"] == 1200.0
7272
assert plan["execution"]["investable_cash"] == 1800.0
73+
74+
75+
def test_value_decision_without_threshold_uses_platform_default():
76+
decision = StrategyDecision(
77+
positions=(PositionTarget(symbol="AAA", target_value=500.0),),
78+
diagnostics={"signal_display": "hold AAA"},
79+
)
80+
snapshot = PortfolioSnapshot(
81+
as_of=datetime.now(timezone.utc),
82+
total_equity=20000.0,
83+
buying_power=3000.0,
84+
positions=(),
85+
)
86+
87+
plan = map_strategy_decision_to_plan(
88+
decision,
89+
snapshot=snapshot,
90+
strategy_profile="mega_cap_leader_rotation_top50_balanced",
91+
)
92+
93+
assert plan["execution"]["trade_threshold_value"] == 200.0
94+
assert plan["execution"]["current_min_trade"] == 200.0
95+
assert plan["allocation"]["targets"]["AAA"] == 500.0
96+
97+
98+
def test_no_execute_decision_without_threshold_holds_current_positions():
99+
decision = StrategyDecision(
100+
positions=(),
101+
risk_flags=("no_execute",),
102+
diagnostics={"signal_description": "no actionable signal"},
103+
)
104+
snapshot = PortfolioSnapshot(
105+
as_of=datetime.now(timezone.utc),
106+
total_equity=5000.0,
107+
buying_power=1000.0,
108+
positions=(Position(symbol="AAA", quantity=3, market_value=750.0),),
109+
)
110+
111+
plan = map_strategy_decision_to_plan(
112+
decision,
113+
snapshot=snapshot,
114+
strategy_profile="mega_cap_leader_rotation_top50_balanced",
115+
)
116+
117+
assert plan["execution"]["trade_threshold_value"] == 100.0
118+
assert plan["execution"]["current_min_trade"] == 100.0
119+
assert plan["allocation"]["targets"]["AAA"] == 750.0

0 commit comments

Comments
 (0)