feat(EXEC-001): add hedge strategy builder

This commit is contained in:
Bu5hm4nn
2026-03-27 22:33:20 +01:00
parent 554a41a060
commit 4620234967
9 changed files with 429 additions and 37 deletions

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
from pathlib import Path
from uuid import uuid4
from playwright.sync_api import expect, sync_playwright
BASE_URL = "http://127.0.0.1:8000"
ARTIFACTS = Path("tests/artifacts")
ARTIFACTS.mkdir(parents=True, exist_ok=True)
def test_hedge_builder_saves_template_and_reuses_it_in_backtests() -> None:
template_name = f"Crash Guard 95 {uuid4().hex[:8]}"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1440, "height": 1000})
page.goto(BASE_URL, wait_until="domcontentloaded", timeout=30000)
page.get_by_role("button", name="Get started").click()
page.wait_for_url(f"{BASE_URL}/*", timeout=15000)
workspace_url = page.url
page.goto(f"{workspace_url}/hedge", wait_until="domcontentloaded", timeout=30000)
expect(page.locator("text=Hedge Analysis").first).to_be_visible(timeout=15000)
expect(page.locator("text=Strategy Builder").first).to_be_visible(timeout=15000)
expect(page.get_by_label("Strategy selector")).to_be_visible(timeout=15000)
hedge_text = page.locator("body").inner_text(timeout=15000)
assert "$6.25/oz" in hedge_text
page.get_by_label("Template name").fill(template_name)
page.get_by_label("Expiration days").fill("180")
page.get_by_label("Primary strike (% of spot)").fill("95")
page.get_by_role("button", name="Save template").click()
expect(
page.locator(f"text=Saved template {template_name}. Reusable on hedge, backtests, and event comparison.")
).to_be_visible(timeout=15000)
expect(page.get_by_label("Strategy selector")).to_have_value(template_name, timeout=15000)
hedge_text = page.locator("body").inner_text(timeout=15000)
assert "Selected template: " + template_name in hedge_text
assert "Custom 180-day protective put at 95% strike." in hedge_text
assert "$3.49/oz" in hedge_text
assert "$4.95/oz" not in hedge_text
assert "RuntimeError" not in hedge_text
assert "Server error" not in hedge_text
page.screenshot(path=str(ARTIFACTS / "hedge-builder.png"), full_page=True)
page.goto(f"{workspace_url}/backtests", wait_until="networkidle", timeout=30000)
expect(page.locator("text=Backtests").first).to_be_visible(timeout=15000)
page.get_by_label("Template").click()
expect(page.get_by_text(template_name, exact=True)).to_be_visible(timeout=15000)
page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000)
expect(page.locator("text=Event Comparison").first).to_be_visible(timeout=15000)
page.get_by_label("Strategy templates").click()
expect(page.get_by_text(template_name, exact=True)).to_be_visible(timeout=15000)
browser.close()

View File

@@ -125,8 +125,10 @@ def test_strategy_selection_engine_uses_named_templates(monkeypatch: pytest.Monk
]
def test_strategy_template_service_catalog_reads_named_templates() -> None:
catalog = StrategyTemplateService().catalog_items()
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",
@@ -142,3 +144,77 @@ def test_strategy_template_service_catalog_reads_named_templates() -> None:
"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