feat(CORE-001D): close remaining boundary cleanup slices

This commit is contained in:
Bu5hm4nn
2026-03-26 17:27:44 +01:00
parent 99d22302ee
commit 94f3c1ef83
16 changed files with 552 additions and 107 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from datetime import date
from decimal import Decimal
import pytest
@@ -10,6 +11,24 @@ from app.services.backtesting.ui_service import BacktestPageService
from tests.helpers_backtest_sources import StaticBacktestSource
def test_backtest_page_service_accepts_decimal_boundary_values() -> None:
service = BacktestPageService()
result = service.run_read_only_scenario(
symbol="GLD",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 8),
template_slug="protective-put-atm-12m",
underlying_units=Decimal("1000.0"),
loan_amount=Decimal("68000.0"),
margin_call_ltv=Decimal("0.75"),
)
assert result.scenario.initial_portfolio.underlying_units == 1000.0
assert result.scenario.initial_portfolio.loan_amount == 68000.0
assert result.scenario.initial_portfolio.margin_call_ltv == 0.75
def test_backtest_page_service_uses_fixture_window_for_deterministic_run() -> None:
service = BacktestPageService()

View File

@@ -10,6 +10,7 @@ from app.models.strategy_template import EntryPolicy, RollPolicy, StrategyTempla
from app.services.backtesting.historical_provider import (
DailyClosePoint,
SyntheticHistoricalProvider,
SyntheticOptionQuote,
YFinanceHistoricalPriceSource,
)
from app.services.backtesting.service import BacktestService
@@ -209,6 +210,50 @@ def test_backtest_rejects_unsupported_template_behaviors(field: str, value: obje
service._validate_template_for_mvp(unsupported_template)
def test_yfinance_price_source_normalizes_valid_daily_close_row() -> None:
point = YFinanceHistoricalPriceSource._normalize_daily_close_row(
row_date=pd.Timestamp("2024-01-03T00:00:00Z"),
close=96.0,
)
assert point == DailyClosePoint(date=date(2024, 1, 3), close=96.0)
def test_yfinance_price_source_rejects_invalid_daily_close_row_types() -> None:
with pytest.raises(TypeError, match="historical row date must support .date"):
YFinanceHistoricalPriceSource._normalize_daily_close_row(row_date="2024-01-03", close=96.0)
with pytest.raises(ValueError, match="historical close must be finite"):
YFinanceHistoricalPriceSource._normalize_daily_close_row(
row_date=pd.Timestamp("2024-01-03T00:00:00Z"),
close=float("nan"),
)
def test_synthetic_option_quote_rejects_invalid_field_types() -> None:
with pytest.raises(TypeError, match="spot must be a finite number"):
SyntheticOptionQuote(
position_id="pos-1",
leg_id="leg-1",
spot="bad", # type: ignore[arg-type]
strike=100.0,
expiry=date(2025, 1, 1),
quantity=1.0,
mark=5.0,
)
with pytest.raises(ValueError, match="mark must be non-negative"):
SyntheticOptionQuote(
position_id="pos-1",
leg_id="leg-1",
spot=100.0,
strike=100.0,
expiry=date(2025, 1, 1),
quantity=1.0,
mark=-1.0,
)
def test_yfinance_price_source_treats_end_date_inclusively(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, str, str]] = []

View File

@@ -1,12 +1,29 @@
from __future__ import annotations
from datetime import date
from decimal import Decimal
import pytest
from app.services.event_comparison_ui import EventComparisonFixtureHistoricalPriceSource, EventComparisonPageService
def test_event_comparison_page_service_accepts_string_and_decimal_boundary_values() -> None:
service = EventComparisonPageService()
report = service.run_read_only_comparison(
preset_slug="gld-jan-2024-selloff",
template_slugs=("protective-put-atm-12m", "protective-put-95pct-12m"),
underlying_units="1000.0",
loan_amount=Decimal("68000.0"),
margin_call_ltv="0.75",
)
assert report.scenario.initial_portfolio.underlying_units == 1000.0
assert report.scenario.initial_portfolio.loan_amount == 68000.0
assert report.scenario.initial_portfolio.margin_call_ltv == 0.75
def test_event_comparison_page_service_runs_seeded_gld_preset_deterministically() -> None:
service = EventComparisonPageService()

128
tests/test_price_feed.py Normal file
View File

@@ -0,0 +1,128 @@
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from app.services.price_feed import PriceData, PriceFeed
class _CacheStub:
def __init__(self, payloads: dict[str, object] | None = None, enabled: bool = True) -> None:
self.payloads = payloads or {}
self.enabled = enabled
self.writes: list[tuple[str, object, int | None]] = []
async def get_json(self, key: str):
return self.payloads.get(key)
async def set_json(self, key: str, value, ttl: int | None = None):
self.payloads[key] = value
self.writes.append((key, value, ttl))
@pytest.mark.asyncio
async def test_price_feed_uses_normalized_cached_payload() -> None:
feed = PriceFeed()
feed._cache = _CacheStub(
{
"price:GLD": {
"symbol": "GLD",
"price": 200.0,
"currency": "usd",
"timestamp": "2026-03-26T12:00:00+00:00",
"source": "cache",
}
}
)
data = await feed.get_price("gld")
assert data == PriceData(
symbol="GLD",
price=200.0,
currency="USD",
timestamp=datetime.fromisoformat("2026-03-26T12:00:00+00:00"),
source="cache",
)
@pytest.mark.asyncio
async def test_price_feed_discards_malformed_cached_payload_and_refetches(monkeypatch: pytest.MonkeyPatch) -> None:
feed = PriceFeed()
feed._cache = _CacheStub({"price:GLD": {"symbol": "TLT", "price": 200.0, "timestamp": "2026-03-26T12:00:00+00:00"}})
async def fake_fetch(symbol: str):
return {
"symbol": symbol,
"price": 201.5,
"currency": "USD",
"timestamp": datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc),
"source": "yfinance",
}
monkeypatch.setattr(feed, "_fetch_yfinance", fake_fetch)
data = await feed.get_price("GLD")
assert data == PriceData(
symbol="GLD",
price=201.5,
currency="USD",
timestamp=datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc),
source="yfinance",
)
assert feed._cache.writes[0][0] == "price:GLD"
@pytest.mark.asyncio
async def test_price_feed_rejects_invalid_provider_payload() -> None:
feed = PriceFeed()
feed._cache = _CacheStub(enabled=False)
async def fake_fetch(symbol: str):
return {
"symbol": symbol,
"price": 0.0,
"currency": "USD",
"timestamp": datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc),
"source": "yfinance",
}
feed._fetch_yfinance = fake_fetch # type: ignore[method-assign]
data = await feed.get_price("GLD")
assert data is None
@pytest.mark.asyncio
async def test_price_feed_caches_normalized_provider_payload(monkeypatch: pytest.MonkeyPatch) -> None:
feed = PriceFeed()
feed._cache = _CacheStub()
async def fake_fetch(symbol: str):
return {
"symbol": symbol,
"price": 202.25,
"currency": "usd",
"timestamp": datetime(2026, 3, 26, 12, 2, tzinfo=timezone.utc),
"source": "yfinance",
}
monkeypatch.setattr(feed, "_fetch_yfinance", fake_fetch)
data = await feed.get_price("GLD")
assert data is not None
assert data.currency == "USD"
cache_key, cache_payload, ttl = feed._cache.writes[0]
assert cache_key == "price:GLD"
assert ttl == PriceFeed.CACHE_TTL_SECONDS
assert cache_payload == {
"symbol": "GLD",
"price": 202.25,
"currency": "USD",
"timestamp": "2026-03-26T12:02:00+00:00",
"source": "yfinance",
}