feat(EXEC-001A): add named strategy templates
This commit is contained in:
144
tests/test_strategy_templates.py
Normal file
144
tests/test_strategy_templates.py
Normal 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",
|
||||
]
|
||||
Reference in New Issue
Block a user