fix(CORE-002C): align historical units with workspace weight
This commit is contained in:
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from app.domain.units import BaseCurrency, Money, _coerce_currency, decimal_from_float, to_decimal
|
from app.domain.units import BaseCurrency, Money, Weight, WeightUnit, _coerce_currency, decimal_from_float, to_decimal
|
||||||
from app.models.backtest import BacktestPortfolioState
|
from app.models.backtest import BacktestPortfolioState
|
||||||
|
from app.models.portfolio import PortfolioConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -94,6 +95,23 @@ def asset_quantity_from_floats(portfolio_value: float, entry_spot: float, symbol
|
|||||||
return float(asset_quantity_from_money(notional, spot).amount)
|
return float(asset_quantity_from_money(notional, spot).amount)
|
||||||
|
|
||||||
|
|
||||||
|
def asset_quantity_from_workspace_config(config: PortfolioConfig, *, entry_spot: float, symbol: str) -> float:
|
||||||
|
if config.gold_ounces is not None and config.gold_ounces > 0:
|
||||||
|
try:
|
||||||
|
from app.domain.instruments import asset_quantity_from_weight
|
||||||
|
|
||||||
|
quantity = asset_quantity_from_weight(
|
||||||
|
symbol,
|
||||||
|
Weight(amount=decimal_from_float(float(config.gold_ounces)), unit=WeightUnit.OUNCE_TROY),
|
||||||
|
)
|
||||||
|
return float(quantity.amount)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if config.gold_value is not None:
|
||||||
|
return asset_quantity_from_floats(float(config.gold_value), entry_spot, symbol)
|
||||||
|
raise ValueError("Workspace config must provide collateral weight or value")
|
||||||
|
|
||||||
|
|
||||||
def materialize_backtest_portfolio_state(
|
def materialize_backtest_portfolio_state(
|
||||||
*,
|
*,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import date, datetime
|
|||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from nicegui import ui
|
from nicegui import ui
|
||||||
|
|
||||||
from app.domain.backtesting_math import asset_quantity_from_floats
|
from app.domain.backtesting_math import asset_quantity_from_workspace_config
|
||||||
from app.models.workspace import get_workspace_repository
|
from app.models.workspace import get_workspace_repository
|
||||||
from app.pages.common import dashboard_page, render_workspace_recovery
|
from app.pages.common import dashboard_page, render_workspace_recovery
|
||||||
from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService
|
from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService
|
||||||
@@ -66,8 +66,8 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
config = repo.load_portfolio_config(workspace_id) if workspace_id else None
|
config = repo.load_portfolio_config(workspace_id) if workspace_id else None
|
||||||
default_entry_spot = service.derive_entry_spot("GLD", date(2024, 1, 2), date(2024, 1, 8))
|
default_entry_spot = service.derive_entry_spot("GLD", date(2024, 1, 2), date(2024, 1, 8))
|
||||||
default_units = (
|
default_units = (
|
||||||
asset_quantity_from_floats(float(config.gold_value or 0.0), default_entry_spot, "GLD")
|
asset_quantity_from_workspace_config(config, entry_spot=default_entry_spot, symbol="GLD")
|
||||||
if config and config.gold_value is not None and default_entry_spot > 0
|
if config is not None and default_entry_spot > 0
|
||||||
else 1000.0
|
else 1000.0
|
||||||
)
|
)
|
||||||
default_loan = float(config.loan_amount) if config else 68000.0
|
default_loan = float(config.loan_amount) if config else 68000.0
|
||||||
@@ -217,7 +217,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None:
|
|||||||
)
|
)
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
return
|
return
|
||||||
units_input.value = asset_quantity_from_floats(float(config.gold_value), entry_spot, "GLD")
|
units_input.value = asset_quantity_from_workspace_config(config, entry_spot=entry_spot, symbol="GLD")
|
||||||
entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}")
|
entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}")
|
||||||
|
|
||||||
def run_backtest() -> None:
|
def run_backtest() -> None:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from nicegui import ui
|
from nicegui import ui
|
||||||
|
|
||||||
from app.domain.backtesting_math import asset_quantity_from_floats
|
from app.domain.backtesting_math import asset_quantity_from_workspace_config
|
||||||
from app.models.workspace import get_workspace_repository
|
from app.models.workspace import get_workspace_repository
|
||||||
from app.pages.common import dashboard_page, render_workspace_recovery
|
from app.pages.common import dashboard_page, render_workspace_recovery
|
||||||
from app.services.event_comparison_ui import EventComparisonPageService
|
from app.services.event_comparison_ui import EventComparisonPageService
|
||||||
@@ -54,8 +54,8 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
)
|
)
|
||||||
default_entry_spot = default_preview.initial_portfolio.entry_spot
|
default_entry_spot = default_preview.initial_portfolio.entry_spot
|
||||||
default_units = (
|
default_units = (
|
||||||
asset_quantity_from_floats(float(config.gold_value or 0.0), default_entry_spot, "GLD")
|
asset_quantity_from_workspace_config(config, entry_spot=default_entry_spot, symbol="GLD")
|
||||||
if config and config.gold_value is not None and default_entry_spot > 0
|
if config is not None and default_entry_spot > 0
|
||||||
else 1000.0
|
else 1000.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
template_select.value = list(service.default_template_selection(str(option["slug"])))
|
template_select.value = list(service.default_template_selection(str(option["slug"])))
|
||||||
try:
|
try:
|
||||||
preview_units = float(units_input.value or 0.0)
|
preview_units = float(units_input.value or 0.0)
|
||||||
if workspace_id and config is not None and config.gold_value is not None:
|
if workspace_id and config is not None:
|
||||||
preview_scenario = service.preview_scenario(
|
preview_scenario = service.preview_scenario(
|
||||||
preset_slug=str(option["slug"]),
|
preset_slug=str(option["slug"]),
|
||||||
template_slugs=selected_template_slugs(),
|
template_slugs=selected_template_slugs(),
|
||||||
@@ -137,10 +137,10 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
|
|||||||
loan_amount=float(loan_input.value or 0.0),
|
loan_amount=float(loan_input.value or 0.0),
|
||||||
margin_call_ltv=float(ltv_input.value or 0.0),
|
margin_call_ltv=float(ltv_input.value or 0.0),
|
||||||
)
|
)
|
||||||
preview_units = asset_quantity_from_floats(
|
preview_units = asset_quantity_from_workspace_config(
|
||||||
float(config.gold_value),
|
config,
|
||||||
float(preview_scenario.initial_portfolio.entry_spot),
|
entry_spot=float(preview_scenario.initial_portfolio.entry_spot),
|
||||||
"GLD",
|
symbol="GLD",
|
||||||
)
|
)
|
||||||
units_input.value = preview_units
|
units_input.value = preview_units
|
||||||
scenario = service.preview_scenario(
|
scenario = service.preview_scenario(
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ from app.domain.backtesting_math import (
|
|||||||
PricePerAsset,
|
PricePerAsset,
|
||||||
asset_quantity_from_floats,
|
asset_quantity_from_floats,
|
||||||
asset_quantity_from_money,
|
asset_quantity_from_money,
|
||||||
|
asset_quantity_from_workspace_config,
|
||||||
materialize_backtest_portfolio_state,
|
materialize_backtest_portfolio_state,
|
||||||
)
|
)
|
||||||
from app.domain.units import BaseCurrency, Money
|
from app.domain.units import BaseCurrency, Money
|
||||||
|
from app.models.portfolio import PortfolioConfig
|
||||||
from app.services.backtesting.comparison import EventComparisonService
|
from app.services.backtesting.comparison import EventComparisonService
|
||||||
from app.services.backtesting.historical_provider import SyntheticHistoricalProvider
|
from app.services.backtesting.historical_provider import SyntheticHistoricalProvider
|
||||||
from app.services.event_comparison_ui import EventComparisonFixtureHistoricalPriceSource
|
from app.services.event_comparison_ui import EventComparisonFixtureHistoricalPriceSource
|
||||||
@@ -47,6 +49,20 @@ def test_asset_quantity_from_floats_matches_workspace_backtest_conversion() -> N
|
|||||||
assert asset_quantity_from_floats(968000.0, 100.0, "GLD") == 9680.0
|
assert asset_quantity_from_floats(968000.0, 100.0, "GLD") == 9680.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset_quantity_from_workspace_config_uses_instrument_weight_conversion_for_gld() -> None:
|
||||||
|
config = PortfolioConfig(
|
||||||
|
gold_ounces=220.0,
|
||||||
|
entry_price=4400.0,
|
||||||
|
entry_basis_mode="weight",
|
||||||
|
loan_amount=222000.0,
|
||||||
|
margin_threshold=0.80,
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = asset_quantity_from_workspace_config(config, entry_spot=100.0, symbol="GLD")
|
||||||
|
|
||||||
|
assert quantity == 2200.0
|
||||||
|
|
||||||
|
|
||||||
def test_materialize_backtest_portfolio_state_uses_typed_asset_boundary() -> None:
|
def test_materialize_backtest_portfolio_state_uses_typed_asset_boundary() -> None:
|
||||||
portfolio = materialize_backtest_portfolio_state(
|
portfolio = materialize_backtest_portfolio_state(
|
||||||
symbol="GLD",
|
symbol="GLD",
|
||||||
|
|||||||
@@ -119,20 +119,20 @@ def test_homepage_and_options_page_render() -> None:
|
|||||||
page.screenshot(path=str(ARTIFACTS / "settings.png"), full_page=True)
|
page.screenshot(path=str(ARTIFACTS / "settings.png"), full_page=True)
|
||||||
|
|
||||||
page.goto(f"{workspace_url}/backtests", wait_until="networkidle", timeout=30000)
|
page.goto(f"{workspace_url}/backtests", wait_until="networkidle", timeout=30000)
|
||||||
expect(page.get_by_label("Underlying units")).to_have_value("9680")
|
expect(page.get_by_label("Underlying units")).to_have_value("2200")
|
||||||
expect(page.get_by_label("Loan amount")).to_have_value("222000")
|
expect(page.get_by_label("Loan amount")).to_have_value("222000")
|
||||||
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
|
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
|
||||||
backtests_workspace_text = page.locator("body").inner_text(timeout=15000)
|
backtests_workspace_text = page.locator("body").inner_text(timeout=15000)
|
||||||
assert "Scenario Summary" in backtests_workspace_text
|
assert "Scenario Summary" in backtests_workspace_text
|
||||||
assert "$968,000" in backtests_workspace_text
|
assert "$220,000" in backtests_workspace_text
|
||||||
|
|
||||||
page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000)
|
page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000)
|
||||||
expect(page.get_by_label("Underlying units")).to_have_value("9680")
|
expect(page.get_by_label("Underlying units")).to_have_value("2200")
|
||||||
expect(page.get_by_label("Loan amount")).to_have_value("222000")
|
expect(page.get_by_label("Loan amount")).to_have_value("222000")
|
||||||
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
|
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
|
||||||
event_workspace_text = page.locator("body").inner_text(timeout=15000)
|
event_workspace_text = page.locator("body").inner_text(timeout=15000)
|
||||||
assert "$222,000" in event_workspace_text
|
assert "$222,000" in event_workspace_text
|
||||||
assert "9,680" in event_workspace_text
|
assert "2,200" in event_workspace_text
|
||||||
assert "80.0%" in event_workspace_text
|
assert "80.0%" in event_workspace_text
|
||||||
|
|
||||||
page.goto(workspace_url, wait_until="domcontentloaded", timeout=30000)
|
page.goto(workspace_url, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ def test_workspace_routes_seed_page_defaults_from_workspace_portfolio_config(tmp
|
|||||||
|
|
||||||
assert backtests_response.status_code == 200
|
assert backtests_response.status_code == 200
|
||||||
assert "Workspace defaults seed underlying units, loan amount, and margin threshold." in backtests_response.text
|
assert "Workspace defaults seed underlying units, loan amount, and margin threshold." in backtests_response.text
|
||||||
assert "9680" in backtests_response.text or "9,680" in backtests_response.text
|
assert "2200" in backtests_response.text or "2,200" in backtests_response.text
|
||||||
assert "222000" in backtests_response.text or "222,000" in backtests_response.text
|
assert "222000" in backtests_response.text or "222,000" in backtests_response.text
|
||||||
assert "0.8" in backtests_response.text or "80.0%" in backtests_response.text
|
assert "0.8" in backtests_response.text or "80.0%" in backtests_response.text
|
||||||
|
|
||||||
@@ -233,8 +233,8 @@ def test_workspace_routes_seed_page_defaults_from_workspace_portfolio_config(tmp
|
|||||||
assert "Underlying units" in event_response.text
|
assert "Underlying units" in event_response.text
|
||||||
assert "Loan amount" in event_response.text
|
assert "Loan amount" in event_response.text
|
||||||
assert "222,000" in event_response.text or "222000" in event_response.text
|
assert "222,000" in event_response.text or "222000" in event_response.text
|
||||||
assert "9,680" in event_response.text or "9680" in event_response.text
|
assert "2,200" in event_response.text or "2200" in event_response.text
|
||||||
assert "80.0%" in event_response.text
|
assert "80.0%" in event_response.text or "0.8" in event_response.text
|
||||||
|
|
||||||
|
|
||||||
def test_hedge_page_upgrades_cached_gld_quote_and_uses_converted_spot(tmp_path, monkeypatch) -> None:
|
def test_hedge_page_upgrades_cached_gld_quote_and_uses_converted_spot(tmp_path, monkeypatch) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user