Enterprise clean-rebuild (no foxhunt code). Hexagonal/ports-and-adapters: pure domain (gauntlet math, strategies, backtest, models) | ports (DataProvider, repositories) | adapters (Yahoo data, SQLAlchemy operational [Postgres/SQLite], DuckDB analytical) | application (ResearchService, DI) | CLI composition root. Gauntlet-first: Deflated Sharpe (Bailey-LdP) built + falsification-tested (kills best-of-N-on-noise, keeps real premium). Full vertical slice runs end-to-end on real data: data -> strategy(trend) -> backtest(net of costs) -> IS/OOS gauntlet -> persistence. 4/4 tests green. Postgres+DuckDB split, pydantic contracts, typed, DRY via one-contract-per-port. ADR + README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
2.3 KiB
Python
56 lines
2.3 KiB
Python
"""End-to-end vertical slice with a FAKE data provider (no network) + temp SQLite + temp DuckDB.
|
|
Proves data → backtest → gauntlet → persistence wires together and the domain stays infra-agnostic."""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from fxhnt.adapters.persistence import DuckDbAnalyticalStore, SqlOperationalRepository
|
|
from fxhnt.application import ResearchService
|
|
from fxhnt.config import GauntletSettings, Settings
|
|
from fxhnt.domain.models import AssetClass, Market, PriceSeries, StrategySpec
|
|
|
|
|
|
class FakeDataProvider:
|
|
name = "fake"
|
|
|
|
def __init__(self, prices: PriceSeries) -> None:
|
|
self._prices = prices
|
|
|
|
def fetch(self, market: Market, start: str | None = None, end: str | None = None) -> PriceSeries:
|
|
return self._prices
|
|
|
|
|
|
def _trending_prices(market: Market, n: int = 2000, seed: int = 1) -> PriceSeries:
|
|
rng = np.random.default_rng(seed)
|
|
rets = rng.normal(0.0004, 0.01, n) # genuine upward drift -> trend has something real to ride
|
|
close = 100.0 * np.cumprod(1.0 + rets)
|
|
dates = tuple(f"20{10 + i // 365:02d}-{1 + (i // 30) % 12:02d}-{1 + i % 28:02d}" for i in range(n))
|
|
return PriceSeries(market=market, dates=dates, close=close)
|
|
|
|
|
|
def test_vertical_slice(tmp_path) -> None:
|
|
market = Market(symbol="TEST", asset_class=AssetClass.ETF)
|
|
settings = Settings(
|
|
operational_dsn=f"sqlite:///{tmp_path / 'op.db'}",
|
|
analytical_path=str(tmp_path / "an.duckdb"),
|
|
gauntlet=GauntletSettings(),
|
|
)
|
|
svc = ResearchService(
|
|
data=FakeDataProvider(_trending_prices(market)),
|
|
operational=SqlOperationalRepository(settings.operational_dsn),
|
|
analytical=DuckDbAnalyticalStore(settings.analytical_path),
|
|
settings=settings,
|
|
)
|
|
|
|
run = svc.evaluate_candidate(market, StrategySpec(kind="trend", params={"window": 100.0}), n_trials=1)
|
|
|
|
# the pipeline produced a coherent run
|
|
assert run.stats.n_obs > 1000
|
|
assert run.verdict.n_trials == 1
|
|
# it persisted to the operational store and round-trips
|
|
loaded = svc._op.get_run(run.run_id) # noqa: SLF001
|
|
assert loaded is not None and loaded.run_id == run.run_id
|
|
assert loaded.verdict.passed == run.verdict.passed
|
|
# the analytical store cached the prices
|
|
assert svc._an.load_prices(market) is not None
|