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", ]