feat(CORE-001D): close remaining boundary cleanup slices
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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]] = []
|
||||
|
||||
|
||||
@@ -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
128
tests/test_price_feed.py
Normal 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",
|
||||
}
|
||||
Reference in New Issue
Block a user