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

@@ -3,6 +3,7 @@
from .option import Greeks, OptionContract, OptionMoneyness
from .portfolio import LombardPortfolio
from .strategy import HedgingStrategy, ScenarioResult, StrategyType
from .strategy_template import EntryPolicy, RollPolicy, StrategyTemplate, TemplateLeg
__all__ = [
"Greeks",
@@ -12,4 +13,8 @@ __all__ = [
"OptionMoneyness",
"ScenarioResult",
"StrategyType",
"StrategyTemplate",
"TemplateLeg",
"RollPolicy",
"EntryPolicy",
]

View File

@@ -0,0 +1,309 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Literal
StrategyTemplateKind = Literal["protective_put", "laddered_put"]
StrategyTemplateStatus = Literal["draft", "active", "archived"]
ContractMode = Literal["continuous_units", "listed_contracts"]
LegSide = Literal["long", "short"]
LegOptionType = Literal["put", "call"]
StrikeRuleType = Literal["spot_pct"]
QuantityRule = Literal["target_coverage_pct"]
RollPolicyType = Literal["hold_to_expiry", "roll_n_days_before_expiry"]
EntryTiming = Literal["scenario_start_close"]
@dataclass(frozen=True)
class StrikeRule:
rule_type: StrikeRuleType
value: float
def __post_init__(self) -> None:
if self.rule_type != "spot_pct":
raise ValueError("unsupported strike rule")
if self.value <= 0:
raise ValueError("strike rule value must be positive")
def to_dict(self) -> dict[str, Any]:
return {"rule_type": self.rule_type, "value": self.value}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> StrikeRule:
return cls(rule_type=payload["rule_type"], value=float(payload["value"]))
@dataclass(frozen=True)
class TemplateLeg:
leg_id: str
side: LegSide
option_type: LegOptionType
allocation_weight: float
strike_rule: StrikeRule
target_expiry_days: int
quantity_rule: QuantityRule
target_coverage_pct: float = 1.0
def __post_init__(self) -> None:
if self.side not in {"long", "short"}:
raise ValueError("unsupported leg side")
if self.option_type not in {"put", "call"}:
raise ValueError("unsupported option type")
if self.allocation_weight <= 0:
raise ValueError("allocation_weight must be positive")
if self.target_expiry_days <= 0:
raise ValueError("target_expiry_days must be positive")
if self.quantity_rule != "target_coverage_pct":
raise ValueError("unsupported quantity rule")
if self.target_coverage_pct <= 0:
raise ValueError("target_coverage_pct must be positive")
def to_dict(self) -> dict[str, Any]:
return {
"leg_id": self.leg_id,
"side": self.side,
"option_type": self.option_type,
"allocation_weight": self.allocation_weight,
"strike_rule": self.strike_rule.to_dict(),
"target_expiry_days": self.target_expiry_days,
"quantity_rule": self.quantity_rule,
"target_coverage_pct": self.target_coverage_pct,
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> TemplateLeg:
return cls(
leg_id=payload["leg_id"],
side=payload["side"],
option_type=payload["option_type"],
allocation_weight=float(payload["allocation_weight"]),
strike_rule=StrikeRule.from_dict(payload["strike_rule"]),
target_expiry_days=int(payload["target_expiry_days"]),
quantity_rule=payload.get("quantity_rule", "target_coverage_pct"),
target_coverage_pct=float(payload.get("target_coverage_pct", 1.0)),
)
@dataclass(frozen=True)
class RollPolicy:
policy_type: RollPolicyType
days_before_expiry: int | None = None
rebalance_on_new_deposit: bool = False
def __post_init__(self) -> None:
if self.policy_type not in {"hold_to_expiry", "roll_n_days_before_expiry"}:
raise ValueError("unsupported roll policy")
if self.policy_type == "roll_n_days_before_expiry" and (self.days_before_expiry or 0) <= 0:
raise ValueError("days_before_expiry is required for rolling policies")
def to_dict(self) -> dict[str, Any]:
return {
"policy_type": self.policy_type,
"days_before_expiry": self.days_before_expiry,
"rebalance_on_new_deposit": self.rebalance_on_new_deposit,
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> RollPolicy:
return cls(
policy_type=payload["policy_type"],
days_before_expiry=payload.get("days_before_expiry"),
rebalance_on_new_deposit=bool(payload.get("rebalance_on_new_deposit", False)),
)
@dataclass(frozen=True)
class EntryPolicy:
entry_timing: EntryTiming
stagger_days: int | None = None
def __post_init__(self) -> None:
if self.entry_timing != "scenario_start_close":
raise ValueError("unsupported entry timing")
if self.stagger_days is not None and self.stagger_days < 0:
raise ValueError("stagger_days must be non-negative")
def to_dict(self) -> dict[str, Any]:
return {"entry_timing": self.entry_timing, "stagger_days": self.stagger_days}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> EntryPolicy:
return cls(entry_timing=payload["entry_timing"], stagger_days=payload.get("stagger_days"))
@dataclass(frozen=True)
class StrategyTemplate:
template_id: str
slug: str
display_name: str
description: str
template_kind: StrategyTemplateKind
status: StrategyTemplateStatus
version: int
underlying_symbol: str
contract_mode: ContractMode
legs: tuple[TemplateLeg, ...]
roll_policy: RollPolicy
entry_policy: EntryPolicy
tags: tuple[str, ...] = field(default_factory=tuple)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
def __post_init__(self) -> None:
if self.template_kind not in {"protective_put", "laddered_put"}:
raise ValueError("unsupported template_kind")
if self.status not in {"draft", "active", "archived"}:
raise ValueError("unsupported template status")
if self.contract_mode not in {"continuous_units", "listed_contracts"}:
raise ValueError("unsupported contract mode")
if not self.slug:
raise ValueError("slug is required")
if not self.display_name:
raise ValueError("display_name is required")
if self.version <= 0:
raise ValueError("version must be positive")
if not self.legs:
raise ValueError("at least one template leg is required")
if self.template_kind in {"protective_put", "laddered_put"}:
if any(leg.side != "long" or leg.option_type != "put" for leg in self.legs):
raise ValueError("put templates support only long put legs")
total_weight = sum(leg.allocation_weight for leg in self.legs)
if abs(total_weight - 1.0) > 1e-9:
raise ValueError("weights must sum to 1.0")
expiry_days = {leg.target_expiry_days for leg in self.legs}
if len(expiry_days) != 1:
raise ValueError("all template legs must share target_expiry_days in MVP")
@property
def target_expiry_days(self) -> int:
return self.legs[0].target_expiry_days
def to_dict(self) -> dict[str, Any]:
return {
"template_id": self.template_id,
"slug": self.slug,
"display_name": self.display_name,
"description": self.description,
"template_kind": self.template_kind,
"status": self.status,
"version": self.version,
"underlying_symbol": self.underlying_symbol,
"contract_mode": self.contract_mode,
"legs": [leg.to_dict() for leg in self.legs],
"roll_policy": self.roll_policy.to_dict(),
"entry_policy": self.entry_policy.to_dict(),
"tags": list(self.tags),
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> StrategyTemplate:
return cls(
template_id=payload["template_id"],
slug=payload["slug"],
display_name=payload["display_name"],
description=payload.get("description", ""),
template_kind=payload["template_kind"],
status=payload.get("status", "active"),
version=int(payload.get("version", 1)),
underlying_symbol=payload.get("underlying_symbol", "GLD"),
contract_mode=payload.get("contract_mode", "continuous_units"),
legs=tuple(TemplateLeg.from_dict(leg) for leg in payload["legs"]),
roll_policy=RollPolicy.from_dict(payload["roll_policy"]),
entry_policy=EntryPolicy.from_dict(payload["entry_policy"]),
tags=tuple(payload.get("tags", [])),
created_at=datetime.fromisoformat(payload["created_at"]),
updated_at=datetime.fromisoformat(payload["updated_at"]),
)
@classmethod
def protective_put(
cls,
*,
template_id: str,
slug: str,
display_name: str,
description: str,
strike_pct: float,
target_expiry_days: int,
underlying_symbol: str = "GLD",
tags: tuple[str, ...] = (),
) -> StrategyTemplate:
now = datetime.now(timezone.utc)
return cls(
template_id=template_id,
slug=slug,
display_name=display_name,
description=description,
template_kind="protective_put",
status="active",
version=1,
underlying_symbol=underlying_symbol,
contract_mode="continuous_units",
legs=(
TemplateLeg(
leg_id=f"{template_id}-leg-1",
side="long",
option_type="put",
allocation_weight=1.0,
strike_rule=StrikeRule(rule_type="spot_pct", value=strike_pct),
target_expiry_days=target_expiry_days,
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"),
tags=tags,
created_at=now,
updated_at=now,
)
@classmethod
def laddered_put(
cls,
*,
template_id: str,
slug: str,
display_name: str,
description: str,
strike_pcts: tuple[float, ...],
weights: tuple[float, ...],
target_expiry_days: int,
underlying_symbol: str = "GLD",
tags: tuple[str, ...] = (),
) -> StrategyTemplate:
if len(strike_pcts) != len(weights):
raise ValueError("strike_pcts and weights must have the same length")
now = datetime.now(timezone.utc)
return cls(
template_id=template_id,
slug=slug,
display_name=display_name,
description=description,
template_kind="laddered_put",
status="active",
version=1,
underlying_symbol=underlying_symbol,
contract_mode="continuous_units",
legs=tuple(
TemplateLeg(
leg_id=f"{template_id}-leg-{index}",
side="long",
option_type="put",
allocation_weight=weight,
strike_rule=StrikeRule(rule_type="spot_pct", value=strike_pct),
target_expiry_days=target_expiry_days,
quantity_rule="target_coverage_pct",
target_coverage_pct=1.0,
)
for index, (strike_pct, weight) in enumerate(zip(strike_pcts, weights, strict=True), start=1)
),
roll_policy=RollPolicy(policy_type="hold_to_expiry"),
entry_policy=EntryPolicy(entry_timing="scenario_start_close"),
tags=tags,
created_at=now,
updated_at=now,
)

View File

@@ -6,6 +6,8 @@ from typing import Any
from nicegui import ui
from app.services.strategy_templates import StrategyTemplateService
NAV_ITEMS: list[tuple[str, str, str]] = [
("overview", "/", "Overview"),
("hedge", "/hedge", "Hedge Analysis"),
@@ -38,33 +40,7 @@ def portfolio_snapshot() -> dict[str, float]:
def strategy_catalog() -> list[dict[str, Any]]:
return [
{
"name": "protective_put",
"label": "Protective Put",
"description": "Full downside protection below the hedge strike with uncapped upside.",
"estimated_cost": 6.25,
"max_drawdown_floor": 210.0,
"coverage": "High",
},
{
"name": "collar",
"label": "Collar",
"description": "Lower premium by financing puts with covered call upside caps.",
"estimated_cost": 2.10,
"max_drawdown_floor": 208.0,
"upside_cap": 228.0,
"coverage": "Balanced",
},
{
"name": "laddered_puts",
"label": "Laddered Puts",
"description": "Multiple maturities and strikes reduce roll concentration and smooth protection.",
"estimated_cost": 4.45,
"max_drawdown_floor": 205.0,
"coverage": "Layered",
},
]
return StrategyTemplateService().catalog_items()
def quick_recommendations(portfolio: dict[str, Any] | None = None) -> list[dict[str, str]]:
@@ -73,7 +49,7 @@ def quick_recommendations(portfolio: dict[str, Any] | None = None) -> list[dict[
return [
{
"title": "Balanced hedge favored",
"summary": "A collar keeps the current LTV comfortably below the margin threshold while limiting upfront spend.",
"summary": "A 95% protective put balances margin-call protection with a lower upfront hedge cost.",
"tone": "positive",
},
{

View File

@@ -75,7 +75,7 @@ def _waterfall_options(metrics: dict) -> dict:
def hedge_page() -> None:
strategies = strategy_catalog()
strategy_map = {strategy["label"]: strategy["name"] for strategy in strategies}
selected = {"strategy": strategies[0]["name"], "scenario_pct": 0}
selected = {"strategy": strategies[0]["name"], "label": strategies[0]["label"], "scenario_pct": 0}
with dashboard_page(
"Hedge Analysis",
@@ -87,9 +87,7 @@ def hedge_page() -> None:
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
):
ui.label("Strategy Controls").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
selector = ui.select(strategy_map, value=selected["strategy"], label="Strategy selector").classes(
"w-full"
)
selector = ui.select(strategy_map, value=selected["label"], label="Strategy selector").classes("w-full")
slider_value = ui.label("Scenario move: +0%").classes("text-sm text-slate-500 dark:text-slate-400")
slider = ui.slider(min=-25, max=25, value=0, step=5).classes("w-full")
ui.label(f"Current spot reference: ${demo_spot_price():,.2f}").classes(
@@ -134,13 +132,16 @@ def hedge_page() -> None:
ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100")
ui.label(strategy["description"]).classes("text-sm text-slate-600 dark:text-slate-300")
cost_chart.options = _cost_benefit_options(metrics)
cost_chart.options.clear()
cost_chart.options.update(_cost_benefit_options(metrics))
cost_chart.update()
waterfall_chart.options = _waterfall_options(metrics)
waterfall_chart.options.clear()
waterfall_chart.options.update(_waterfall_options(metrics))
waterfall_chart.update()
def refresh_from_selector(event) -> None:
selected["strategy"] = event.value
selected["label"] = str(event.value)
selected["strategy"] = strategy_map[selected["label"]]
render_summary()
def refresh_from_slider(event) -> None:

View File

@@ -0,0 +1,199 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from app.models.strategy_template import StrategyTemplate
from app.strategies.base import BaseStrategy, StrategyConfig
from app.strategies.laddered_put import LadderedPutStrategy, LadderSpec
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
DEFAULT_TEMPLATE_FILE = Path(__file__).resolve().parents[2] / "config" / "strategy_templates.json"
def default_strategy_templates() -> list[StrategyTemplate]:
return [
StrategyTemplate.protective_put(
template_id="protective-put-atm-12m-v1",
slug="protective-put-atm-12m",
display_name="Protective Put ATM",
description="Full downside protection using a 12-month at-the-money put.",
strike_pct=1.0,
target_expiry_days=365,
tags=("system", "protective_put", "conservative"),
),
StrategyTemplate.protective_put(
template_id="protective-put-95pct-12m-v1",
slug="protective-put-95pct-12m",
display_name="Protective Put 95%",
description="Lower-cost 12-month protective put using a 95% spot strike.",
strike_pct=0.95,
target_expiry_days=365,
tags=("system", "protective_put", "balanced"),
),
StrategyTemplate.protective_put(
template_id="protective-put-90pct-12m-v1",
slug="protective-put-90pct-12m",
display_name="Protective Put 90%",
description="Cost-sensitive 12-month protective put using a 90% spot strike.",
strike_pct=0.90,
target_expiry_days=365,
tags=("system", "protective_put", "cost_sensitive"),
),
StrategyTemplate.laddered_put(
template_id="ladder-50-50-atm-95pct-12m-v1",
slug="ladder-50-50-atm-95pct-12m",
display_name="Laddered Puts 50/50 ATM + 95%",
description="Split hedge evenly across ATM and 95% strike 12-month puts.",
strike_pcts=(1.0, 0.95),
weights=(0.5, 0.5),
target_expiry_days=365,
tags=("system", "laddered_put", "balanced"),
),
StrategyTemplate.laddered_put(
template_id="ladder-33-33-33-atm-95pct-90pct-12m-v1",
slug="ladder-33-33-33-atm-95pct-90pct-12m",
display_name="Laddered Puts 33/33/33 ATM + 95% + 90%",
description="Three-layer 12-month put ladder across ATM, 95%, and 90% strikes.",
strike_pcts=(1.0, 0.95, 0.90),
weights=(1 / 3, 1 / 3, 1 / 3),
target_expiry_days=365,
tags=("system", "laddered_put", "cost_sensitive"),
),
]
class FileStrategyTemplateRepository:
def __init__(self, path: str | Path = DEFAULT_TEMPLATE_FILE) -> None:
self.path = Path(path)
def list_templates(self) -> list[StrategyTemplate]:
self._ensure_seeded()
payload = json.loads(self.path.read_text())
return [StrategyTemplate.from_dict(item) for item in payload.get("templates", [])]
def get_by_slug(self, slug: str) -> StrategyTemplate | None:
return next((template for template in self.list_templates() if template.slug == slug), None)
def save_all(self, templates: list[StrategyTemplate]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
payload = {"templates": [template.to_dict() for template in templates]}
self.path.write_text(json.dumps(payload, indent=2) + "\n")
def _ensure_seeded(self) -> None:
if self.path.exists():
return
self.save_all(default_strategy_templates())
class StrategyTemplateService:
def __init__(self, repository: FileStrategyTemplateRepository | None = None) -> None:
self.repository = repository or FileStrategyTemplateRepository()
def list_active_templates(self, underlying_symbol: str = "GLD") -> list[StrategyTemplate]:
symbol = underlying_symbol.upper()
return [
template
for template in self.repository.list_templates()
if template.status == "active" and template.underlying_symbol.upper() in {symbol, "*"}
]
def get_template(self, slug: str) -> StrategyTemplate:
template = self.repository.get_by_slug(slug)
if template is None:
raise KeyError(f"Unknown strategy template: {slug}")
return template
def build_strategy(self, config: StrategyConfig, slug: str) -> BaseStrategy:
return self.build_strategy_from_template(config, self.get_template(slug))
def build_strategy_from_template(self, config: StrategyConfig, template: StrategyTemplate) -> BaseStrategy:
months = max(1, round(template.target_expiry_days / 30.4167))
if template.template_kind == "protective_put":
leg = template.legs[0]
return ProtectivePutStrategy(
config,
ProtectivePutSpec(
label=self._protective_label(leg.strike_rule.value),
strike_pct=leg.strike_rule.value,
months=months,
),
)
if template.template_kind == "laddered_put":
return LadderedPutStrategy(
config,
LadderSpec(
label=self._ladder_label(template),
weights=tuple(leg.allocation_weight for leg in template.legs),
strike_pcts=tuple(leg.strike_rule.value for leg in template.legs),
months=months,
),
)
raise ValueError(f"Unsupported template kind: {template.template_kind}")
def catalog_items(self) -> list[dict[str, Any]]:
ui_defaults = {
"protective_put_atm": {"estimated_cost": 6.25, "max_drawdown_floor": 210.0, "coverage": "High"},
"protective_put_otm_95": {"estimated_cost": 4.95, "max_drawdown_floor": 205.0, "coverage": "Balanced"},
"protective_put_otm_90": {"estimated_cost": 3.7, "max_drawdown_floor": 194.0, "coverage": "Cost-efficient"},
"laddered_put_50_50_atm_otm95": {
"estimated_cost": 4.45,
"max_drawdown_floor": 205.0,
"coverage": "Layered",
},
"laddered_put_33_33_33_atm_otm95_otm90": {
"estimated_cost": 3.85,
"max_drawdown_floor": 200.0,
"coverage": "Layered",
},
}
items: list[dict[str, Any]] = []
for template in self.list_active_templates():
strategy_name = self.strategy_name(template)
items.append(
{
"name": strategy_name,
"template_slug": template.slug,
"label": template.display_name,
"description": template.description,
**ui_defaults.get(strategy_name, {}),
}
)
return items
def strategy_name(self, template: StrategyTemplate) -> str:
strategy = self.build_strategy_from_template(
StrategyConfig(portfolio=self._stub_portfolio(), spot_price=1.0, volatility=0.16, risk_free_rate=0.045),
template,
)
return strategy.name
@staticmethod
def _protective_label(strike_pct: float) -> str:
if abs(strike_pct - 1.0) < 1e-9:
return "ATM"
return f"OTM_{int(round(strike_pct * 100))}"
def _ladder_label(self, template: StrategyTemplate) -> str:
weight_labels = "_".join(str(int(round(leg.allocation_weight * 100))) for leg in template.legs)
strike_labels = "_".join(self._strike_label(leg.strike_rule.value) for leg in template.legs)
return f"{weight_labels}_{strike_labels}"
@staticmethod
def _strike_label(strike_pct: float) -> str:
if abs(strike_pct - 1.0) < 1e-9:
return "ATM"
return f"OTM{int(round(strike_pct * 100))}"
@staticmethod
def _stub_portfolio():
from app.models.portfolio import LombardPortfolio
return LombardPortfolio(
gold_ounces=1.0,
gold_price_per_ounce=1.0,
loan_amount=0.5,
initial_ltv=0.5,
margin_call_ltv=0.75,
)

View File

@@ -1,5 +1,4 @@
from .base import BaseStrategy, StrategyConfig
from .engine import StrategySelectionEngine
from .laddered_put import LadderedPutStrategy, LadderSpec
from .lease import LeaseAnalysisSpec, LeaseStrategy
from .protective_put import ProtectivePutSpec, ProtectivePutStrategy
@@ -15,3 +14,11 @@ __all__ = [
"LeaseStrategy",
"StrategySelectionEngine",
]
def __getattr__(name: str):
if name == "StrategySelectionEngine":
from .engine import StrategySelectionEngine
return StrategySelectionEngine
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@@ -9,10 +9,9 @@ from app.core.pricing.black_scholes import (
DEFAULT_VOLATILITY,
)
from app.models.portfolio import LombardPortfolio
from app.services.strategy_templates import StrategyTemplateService
from app.strategies.base import BaseStrategy, StrategyConfig
from app.strategies.laddered_put import LadderedPutStrategy, LadderSpec
from app.strategies.lease import LeaseStrategy
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
RiskProfile = Literal["conservative", "balanced", "cost_sensitive"]
@@ -34,6 +33,7 @@ class StrategySelectionEngine:
spot_price: float = RESEARCH_GLD_SPOT
volatility: float = RESEARCH_VOLATILITY
risk_free_rate: float = RESEARCH_RISK_FREE_RATE
template_service: StrategyTemplateService | None = None
def _config(self) -> StrategyConfig:
portfolio = LombardPortfolio(
@@ -52,30 +52,12 @@ class StrategySelectionEngine:
def _strategies(self) -> list[BaseStrategy]:
config = self._config()
return [
ProtectivePutStrategy(config, ProtectivePutSpec(label="ATM", strike_pct=1.0, months=12)),
ProtectivePutStrategy(config, ProtectivePutSpec(label="OTM_95", strike_pct=0.95, months=12)),
ProtectivePutStrategy(config, ProtectivePutSpec(label="OTM_90", strike_pct=0.90, months=12)),
LadderedPutStrategy(
config,
LadderSpec(
label="50_50_ATM_OTM95",
weights=(0.5, 0.5),
strike_pcts=(1.0, 0.95),
months=12,
),
),
LadderedPutStrategy(
config,
LadderSpec(
label="33_33_33_ATM_OTM95_OTM90",
weights=(1 / 3, 1 / 3, 1 / 3),
strike_pcts=(1.0, 0.95, 0.90),
months=12,
),
),
LeaseStrategy(config),
template_service = self.template_service or StrategyTemplateService()
template_strategies = [
template_service.build_strategy_from_template(config, template)
for template in template_service.list_active_templates("GLD")
]
return [*template_strategies, LeaseStrategy(config)]
def compare_all_strategies(self) -> list[dict]:
comparisons: list[dict] = []
@@ -149,6 +131,7 @@ class StrategySelectionEngine:
spot_price=self.spot_price,
volatility=volatility,
risk_free_rate=self.risk_free_rate,
template_service=self.template_service,
)
recommendation = engine.recommend("balanced")
results["volatility"].append(
@@ -169,6 +152,7 @@ class StrategySelectionEngine:
spot_price=spot_price,
volatility=DEFAULT_VOLATILITY,
risk_free_rate=DEFAULT_RISK_FREE_RATE,
template_service=self.template_service,
)
recommendation = engine.recommend("balanced")
results["spot_price"].append(