from __future__ import annotations from decimal import Decimal import pytest from app.domain.backtesting_math import ( AssetQuantity, PricePerAsset, asset_quantity_from_floats, asset_quantity_from_money, asset_quantity_from_workspace_config, materialize_backtest_portfolio_state, ) from app.domain.units import BaseCurrency, Money from app.models.portfolio import PortfolioConfig from app.services.backtesting.comparison import EventComparisonService from app.services.backtesting.historical_provider import SyntheticHistoricalProvider from app.services.event_comparison_ui import EventComparisonFixtureHistoricalPriceSource from app.services.event_presets import EventPresetService from app.services.strategy_templates import StrategyTemplateService def test_asset_quantity_from_money_preserves_notional_value_at_entry_spot() -> None: quantity = asset_quantity_from_money( Money(amount=Decimal("968000"), currency=BaseCurrency.USD), PricePerAsset(amount=Decimal("100"), currency=BaseCurrency.USD, symbol="GLD"), ) assert quantity == AssetQuantity(amount=Decimal("9680"), symbol="GLD") def test_asset_quantity_multiplied_by_price_per_asset_returns_money() -> None: quantity = AssetQuantity(amount=Decimal("9680"), symbol="GLD") price = PricePerAsset(amount=Decimal("100"), currency=BaseCurrency.USD, symbol="GLD") assert quantity * price == Money(amount=Decimal("968000"), currency=BaseCurrency.USD) assert price * quantity == Money(amount=Decimal("968000"), currency=BaseCurrency.USD) def test_asset_quantity_rejects_symbol_mismatch() -> None: with pytest.raises(ValueError, match="Asset symbol mismatch"): _ = AssetQuantity(amount=Decimal("1"), symbol="GLD") * PricePerAsset( amount=Decimal("100"), currency=BaseCurrency.USD, symbol="SLV" ) def test_asset_quantity_from_floats_matches_workspace_backtest_conversion() -> None: 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: portfolio = materialize_backtest_portfolio_state( symbol="GLD", underlying_units=9680.0, entry_spot=100.0, loan_amount=222000.0, margin_call_ltv=0.80, ) assert portfolio.currency == "USD" assert portfolio.underlying_units == 9680.0 assert portfolio.entry_spot == 100.0 assert portfolio.loan_amount == 222000.0 assert portfolio.margin_call_ltv == 0.80 def test_event_comparison_service_preview_uses_shared_typed_portfolio_materializer() -> None: service = EventComparisonService( provider=SyntheticHistoricalProvider(source=EventComparisonFixtureHistoricalPriceSource()), template_service=StrategyTemplateService(), event_preset_service=EventPresetService(), ) scenario = service.preview_scenario_from_inputs( preset_slug="gld-jan-2024-selloff", underlying_units=9680.0, loan_amount=222000.0, margin_call_ltv=0.80, ) assert scenario.symbol == "GLD" assert scenario.initial_portfolio.currency == "USD" assert scenario.initial_portfolio.entry_spot == 100.0 assert scenario.initial_portfolio.underlying_units == 9680.0 assert scenario.initial_portfolio.loan_amount == 222000.0 assert scenario.initial_portfolio.margin_call_ltv == 0.80 @pytest.mark.parametrize( ("portfolio_value", "entry_spot", "message"), [ (968000.0, 0.0, "Spot price per asset must be positive"), (968000.0, -1.0, "PricePerAsset amount must be non-negative"), ], ) def test_asset_quantity_from_floats_fails_closed_for_invalid_spot( portfolio_value: float, entry_spot: float, message: str ) -> None: with pytest.raises(ValueError, match=message): asset_quantity_from_floats(portfolio_value, entry_spot, "GLD")