Files
vault-dash/tests/test_strategy_templates.py
2026-03-27 22:33:20 +01:00

221 lines
8.2 KiB
Python

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(tmp_path: Path) -> None:
catalog = StrategyTemplateService(
repository=FileStrategyTemplateRepository(tmp_path / "strategy_templates.json")
).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",
]
def test_strategy_template_service_creates_and_persists_custom_protective_template(tmp_path: Path) -> None:
repository = FileStrategyTemplateRepository(tmp_path / "strategy_templates.json")
service = StrategyTemplateService(repository=repository)
template = service.create_custom_template(
display_name="Crash Guard 97%",
template_kind="protective_put",
target_expiry_days=180,
strike_pcts=(0.97,),
)
assert template.display_name == "Crash Guard 97%"
assert template.slug == "crash-guard-97"
assert template.target_expiry_days == 180
assert template.legs[0].strike_rule.value == 0.97
assert template.tags == ("custom", "protective_put")
assert service.get_template("crash-guard-97").display_name == "Crash Guard 97%"
def test_strategy_template_service_rejects_duplicate_custom_template_name(tmp_path: Path) -> None:
repository = FileStrategyTemplateRepository(tmp_path / "strategy_templates.json")
service = StrategyTemplateService(repository=repository)
service.create_custom_template(
display_name="Crash Guard 97%",
template_kind="protective_put",
target_expiry_days=180,
strike_pcts=(0.97,),
)
with pytest.raises(ValueError, match="Template name already exists"):
service.create_custom_template(
display_name="Crash Guard 97%",
template_kind="protective_put",
target_expiry_days=90,
strike_pcts=(0.92,),
)
def test_strategy_template_service_catalog_includes_custom_ladder_template(tmp_path: Path) -> None:
repository = FileStrategyTemplateRepository(tmp_path / "strategy_templates.json")
service = StrategyTemplateService(repository=repository)
service.create_custom_template(
display_name="Crash Ladder 98/92",
template_kind="laddered_put",
target_expiry_days=270,
strike_pcts=(0.98, 0.92),
weights=(0.5, 0.5),
)
custom_item = next(item for item in service.catalog_items() if item["label"] == "Crash Ladder 98/92")
assert custom_item["coverage"] == "Layered"
assert custom_item["estimated_cost"] > 0
assert custom_item["downside_put_legs"] == [
{"allocation_weight": 0.5, "strike_pct": 0.98},
{"allocation_weight": 0.5, "strike_pct": 0.92},
]
def test_strategy_template_service_catalog_custom_cost_reflects_expiry_days(tmp_path: Path) -> None:
repository = FileStrategyTemplateRepository(tmp_path / "strategy_templates.json")
service = StrategyTemplateService(repository=repository)
service.create_custom_template(
display_name="Crash Guard 95 180d",
template_kind="protective_put",
target_expiry_days=180,
strike_pcts=(0.95,),
)
custom_item = next(item for item in service.catalog_items() if item["label"] == "Crash Guard 95 180d")
assert custom_item["estimated_cost"] == 3.49