fix(CORE-002C): explain undercollateralized historical seeds
This commit is contained in:
@@ -20,6 +20,17 @@ from app.services.strategy_templates import StrategyTemplateService
|
|||||||
|
|
||||||
SUPPORTED_BACKTEST_PAGE_SYMBOL = "GLD"
|
SUPPORTED_BACKTEST_PAGE_SYMBOL = "GLD"
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_initial_collateral(underlying_units: float, entry_spot: float, loan_amount: float) -> None:
|
||||||
|
initial_collateral_value = underlying_units * entry_spot
|
||||||
|
if loan_amount >= initial_collateral_value:
|
||||||
|
raise ValueError(
|
||||||
|
"Historical scenario starts undercollateralized: "
|
||||||
|
f"loan ${loan_amount:,.0f} exceeds initial collateral ${initial_collateral_value:,.0f} "
|
||||||
|
f"at entry spot ${entry_spot:,.2f}. Reduce loan amount or increase underlying units."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
DETERMINISTIC_UI_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = (
|
DETERMINISTIC_UI_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = (
|
||||||
DailyClosePoint(date=date(2024, 1, 2), close=100.0),
|
DailyClosePoint(date=date(2024, 1, 2), close=100.0),
|
||||||
DailyClosePoint(date=date(2024, 1, 3), close=96.0),
|
DailyClosePoint(date=date(2024, 1, 3), close=96.0),
|
||||||
@@ -116,6 +127,7 @@ class BacktestPageService:
|
|||||||
|
|
||||||
template = self.template_service.get_template(template_slug)
|
template = self.template_service.get_template(template_slug)
|
||||||
entry_spot = self.derive_entry_spot(normalized_symbol, start_date, end_date)
|
entry_spot = self.derive_entry_spot(normalized_symbol, start_date, end_date)
|
||||||
|
_validate_initial_collateral(underlying_units, entry_spot, loan_amount)
|
||||||
initial_portfolio = materialize_backtest_portfolio_state(
|
initial_portfolio = materialize_backtest_portfolio_state(
|
||||||
symbol=normalized_symbol,
|
symbol=normalized_symbol,
|
||||||
underlying_units=underlying_units,
|
underlying_units=underlying_units,
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ from app.services.strategy_templates import StrategyTemplateService
|
|||||||
|
|
||||||
SUPPORTED_EVENT_COMPARISON_SYMBOL = "GLD"
|
SUPPORTED_EVENT_COMPARISON_SYMBOL = "GLD"
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_initial_collateral(underlying_units: float, entry_spot: float, loan_amount: float) -> None:
|
||||||
|
initial_collateral_value = underlying_units * entry_spot
|
||||||
|
if loan_amount >= initial_collateral_value:
|
||||||
|
raise ValueError(
|
||||||
|
"Historical scenario starts undercollateralized: "
|
||||||
|
f"loan ${loan_amount:,.0f} exceeds initial collateral ${initial_collateral_value:,.0f} "
|
||||||
|
f"at entry spot ${entry_spot:,.2f}. Reduce loan amount or increase underlying units."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
DETERMINISTIC_EVENT_COMPARISON_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = (
|
DETERMINISTIC_EVENT_COMPARISON_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = (
|
||||||
DailyClosePoint(date=date(2024, 1, 2), close=100.0),
|
DailyClosePoint(date=date(2024, 1, 2), close=100.0),
|
||||||
DailyClosePoint(date=date(2024, 1, 3), close=96.0),
|
DailyClosePoint(date=date(2024, 1, 3), close=96.0),
|
||||||
@@ -103,13 +114,27 @@ class EventComparisonPageService:
|
|||||||
loan_amount: float,
|
loan_amount: float,
|
||||||
margin_call_ltv: float,
|
margin_call_ltv: float,
|
||||||
) -> BacktestScenario:
|
) -> BacktestScenario:
|
||||||
return self.comparison_service.preview_scenario_from_inputs(
|
try:
|
||||||
|
scenario = self.comparison_service.preview_scenario_from_inputs(
|
||||||
preset_slug=preset_slug,
|
preset_slug=preset_slug,
|
||||||
template_slugs=template_slugs,
|
template_slugs=template_slugs,
|
||||||
underlying_units=underlying_units,
|
underlying_units=underlying_units,
|
||||||
loan_amount=loan_amount,
|
loan_amount=loan_amount,
|
||||||
margin_call_ltv=margin_call_ltv,
|
margin_call_ltv=margin_call_ltv,
|
||||||
)
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
if str(exc) == "loan_amount must be less than initial collateral value":
|
||||||
|
preset = self.event_preset_service.get_preset(preset_slug)
|
||||||
|
preview = self.comparison_service.provider.load_history(
|
||||||
|
preset.symbol.strip().upper(),
|
||||||
|
preset.window_start,
|
||||||
|
preset.window_end,
|
||||||
|
)
|
||||||
|
if preview:
|
||||||
|
_validate_initial_collateral(underlying_units, preview[0].close, loan_amount)
|
||||||
|
raise
|
||||||
|
_validate_initial_collateral(underlying_units, scenario.initial_portfolio.entry_spot, loan_amount)
|
||||||
|
return scenario
|
||||||
|
|
||||||
def run_read_only_comparison(
|
def run_read_only_comparison(
|
||||||
self,
|
self,
|
||||||
@@ -134,6 +159,25 @@ class EventComparisonPageService:
|
|||||||
if normalized_symbol != SUPPORTED_EVENT_COMPARISON_SYMBOL:
|
if normalized_symbol != SUPPORTED_EVENT_COMPARISON_SYMBOL:
|
||||||
raise ValueError("BT-003A event comparison is currently limited to GLD on this page")
|
raise ValueError("BT-003A event comparison is currently limited to GLD on this page")
|
||||||
|
|
||||||
|
try:
|
||||||
|
preview = self.comparison_service.preview_scenario_from_inputs(
|
||||||
|
preset_slug=preset.slug,
|
||||||
|
template_slugs=tuple(template_slugs or preset.scenario_overrides.default_template_slugs),
|
||||||
|
underlying_units=underlying_units,
|
||||||
|
loan_amount=loan_amount,
|
||||||
|
margin_call_ltv=margin_call_ltv,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
if str(exc) == "loan_amount must be less than initial collateral value":
|
||||||
|
preview_history = self.comparison_service.provider.load_history(
|
||||||
|
normalized_symbol,
|
||||||
|
preset.window_start,
|
||||||
|
preset.window_end,
|
||||||
|
)
|
||||||
|
if preview_history:
|
||||||
|
_validate_initial_collateral(underlying_units, preview_history[0].close, loan_amount)
|
||||||
|
raise
|
||||||
|
_validate_initial_collateral(underlying_units, preview.initial_portfolio.entry_spot, loan_amount)
|
||||||
return self.comparison_service.compare_event_from_inputs(
|
return self.comparison_service.compare_event_from_inputs(
|
||||||
preset_slug=preset.slug,
|
preset_slug=preset.slug,
|
||||||
template_slugs=tuple(template_slugs or preset.scenario_overrides.default_template_slugs),
|
template_slugs=tuple(template_slugs or preset.scenario_overrides.default_template_slugs),
|
||||||
|
|||||||
@@ -131,6 +131,18 @@ def test_backtest_page_service_keeps_fixture_history_while_using_caller_portfoli
|
|||||||
},
|
},
|
||||||
"BT-001A backtests are currently limited to GLD on this page",
|
"BT-001A backtests are currently limited to GLD on this page",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"symbol": "GLD",
|
||||||
|
"start_date": date(2024, 1, 2),
|
||||||
|
"end_date": date(2024, 1, 8),
|
||||||
|
"template_slug": "protective-put-atm-12m",
|
||||||
|
"underlying_units": 1000.0,
|
||||||
|
"loan_amount": 145000.0,
|
||||||
|
"margin_call_ltv": 0.75,
|
||||||
|
},
|
||||||
|
"Historical scenario starts undercollateralized",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_backtest_page_service_validation_errors_are_user_facing(kwargs: dict[str, object], message: str) -> None:
|
def test_backtest_page_service_validation_errors_are_user_facing(kwargs: dict[str, object], message: str) -> None:
|
||||||
|
|||||||
@@ -118,6 +118,28 @@ def test_event_comparison_page_service_preview_uses_same_materialization_path()
|
|||||||
assert [ref.slug for ref in scenario.template_refs] == ["protective-put-atm-12m"]
|
assert [ref.slug for ref in scenario.template_refs] == ["protective-put-atm-12m"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_comparison_page_service_rejects_undercollateralized_historical_start() -> None:
|
||||||
|
service = EventComparisonPageService()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Historical scenario starts undercollateralized"):
|
||||||
|
service.preview_scenario(
|
||||||
|
preset_slug="gld-jan-2024-selloff",
|
||||||
|
template_slugs=("protective-put-atm-12m",),
|
||||||
|
underlying_units=1000.0,
|
||||||
|
loan_amount=145000.0,
|
||||||
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Historical scenario starts undercollateralized"):
|
||||||
|
service.run_read_only_comparison(
|
||||||
|
preset_slug="gld-jan-2024-selloff",
|
||||||
|
template_slugs=("protective-put-atm-12m",),
|
||||||
|
underlying_units=1000.0,
|
||||||
|
loan_amount=145000.0,
|
||||||
|
margin_call_ltv=0.75,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_event_comparison_fixture_fails_closed_for_unsupported_range() -> None:
|
def test_event_comparison_fixture_fails_closed_for_unsupported_range() -> None:
|
||||||
source = EventComparisonFixtureHistoricalPriceSource()
|
source = EventComparisonFixtureHistoricalPriceSource()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user