feat(EXEC-001A): add named strategy templates

This commit is contained in:
Bu5hm4nn
2026-03-24 12:27:39 +01:00
parent 78a01d9fc5
commit 2161e10626
10 changed files with 949 additions and 61 deletions

View File

@@ -0,0 +1,144 @@
from __future__ import annotations
from pathlib import Path
import pytest
import app.core.pricing.black_scholes as black_scholes
from app.models.strategy_template import EntryPolicy, RollPolicy, StrategyTemplate, StrikeRule, TemplateLeg
from app.services.strategy_templates import FileStrategyTemplateRepository, StrategyTemplateService
from app.strategies.engine import StrategySelectionEngine
def _force_analytic_pricing(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(black_scholes, "ql", None)
def test_strategy_template_rejects_invalid_weight_sum() -> None:
template = StrategyTemplate.protective_put(
template_id="bad-template",
slug="bad-template",
display_name="Bad Template",
description="Invalid weights should fail validation.",
strike_pct=1.0,
target_expiry_days=365,
)
with pytest.raises(ValueError, match="weights must sum to 1.0"):
StrategyTemplate(
template_id=template.template_id,
slug=template.slug,
display_name=template.display_name,
description=template.description,
template_kind=template.template_kind,
status=template.status,
version=template.version,
underlying_symbol=template.underlying_symbol,
contract_mode=template.contract_mode,
legs=(
TemplateLeg(
leg_id="leg-1",
side="long",
option_type="put",
allocation_weight=0.6,
strike_rule=template.legs[0].strike_rule,
target_expiry_days=365,
quantity_rule="target_coverage_pct",
target_coverage_pct=1.0,
),
TemplateLeg(
leg_id="leg-2",
side="long",
option_type="put",
allocation_weight=0.3,
strike_rule=template.legs[0].strike_rule,
target_expiry_days=365,
quantity_rule="target_coverage_pct",
target_coverage_pct=1.0,
),
),
roll_policy=template.roll_policy,
entry_policy=template.entry_policy,
tags=template.tags,
created_at=template.created_at,
updated_at=template.updated_at,
)
def test_file_strategy_template_repository_seeds_default_templates(tmp_path: Path) -> None:
repository = FileStrategyTemplateRepository(tmp_path / "strategy_templates.json")
templates = repository.list_templates()
assert [template.slug for template in templates] == [
"protective-put-atm-12m",
"protective-put-95pct-12m",
"protective-put-90pct-12m",
"ladder-50-50-atm-95pct-12m",
"ladder-33-33-33-atm-95pct-90pct-12m",
]
assert (tmp_path / "strategy_templates.json").exists() is True
def test_strategy_template_rejects_collar_kind_for_mvp() -> None:
with pytest.raises(ValueError, match="unsupported template_kind"):
StrategyTemplate(
template_id="collar-template",
slug="collar-template",
display_name="Collar Template",
description="Collars are not supported by the template service in the MVP.",
template_kind="collar", # type: ignore[arg-type]
status="active",
version=1,
underlying_symbol="GLD",
contract_mode="continuous_units",
legs=(
TemplateLeg(
leg_id="collar-put-leg",
side="long",
option_type="put",
allocation_weight=1.0,
strike_rule=StrikeRule(rule_type="spot_pct", value=0.95),
target_expiry_days=365,
quantity_rule="target_coverage_pct",
target_coverage_pct=1.0,
),
),
roll_policy=RollPolicy(policy_type="hold_to_expiry"),
entry_policy=EntryPolicy(entry_timing="scenario_start_close"),
)
def test_strategy_selection_engine_uses_named_templates(monkeypatch: pytest.MonkeyPatch) -> None:
_force_analytic_pricing(monkeypatch)
service = StrategyTemplateService()
engine = StrategySelectionEngine(template_service=service)
strategies = engine.compare_all_strategies()
assert [item["name"] for item in strategies[:5]] == [
"protective_put_atm",
"protective_put_otm_95",
"protective_put_otm_90",
"laddered_put_50_50_atm_otm95",
"laddered_put_33_33_33_atm_otm95_otm90",
]
def test_strategy_template_service_catalog_reads_named_templates() -> None:
catalog = StrategyTemplateService().catalog_items()
assert [item["label"] for item in catalog] == [
"Protective Put ATM",
"Protective Put 95%",
"Protective Put 90%",
"Laddered Puts 50/50 ATM + 95%",
"Laddered Puts 33/33/33 ATM + 95% + 90%",
]
assert [item["name"] for item in catalog] == [
"protective_put_atm",
"protective_put_otm_95",
"protective_put_otm_90",
"laddered_put_50_50_atm_otm95",
"laddered_put_33_33_33_atm_otm95_otm90",
]