feat(vrp): xsp_option_bars PIT table + repo (upsert/bars_for/chain_asof)

This commit is contained in:
jgrusewski
2026-07-12 22:56:42 +02:00
parent e5e9f7e8de
commit 2cea5be70c
3 changed files with 106 additions and 0 deletions

View File

@@ -365,3 +365,14 @@ class StrategyFundingRow(CockpitBase):
policy_hash: Mapped[str] = mapped_column(String(64))
as_of: Mapped[str] = mapped_column(String(10))
computed_at: Mapped[dt.datetime] = mapped_column(DateTime)
class XspOptionBarRow(CockpitBase):
__tablename__ = "xsp_option_bars"
osi_symbol: Mapped[str] = mapped_column(String(32), primary_key=True)
date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
expiry: Mapped[dt.date] = mapped_column(Date)
strike: Mapped[float] = mapped_column(Float)
right: Mapped[str] = mapped_column(String(1))
close: Mapped[float] = mapped_column(Float)
src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)

View File

@@ -0,0 +1,54 @@
"""PIT store for frozen XSP option daily bars — the deterministic substrate the VRP recompute reads. OPRA is
hit only when FILLING this table (asset/backfill); VrpStrategy never calls OPRA. Same freeze discipline as the
equity Yahoo-PIT snapshot."""
from __future__ import annotations
import datetime as dt
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from fxhnt.adapters.persistence.cockpit_models import CockpitBase, XspOptionBarRow
from fxhnt.domain.models import OptionContract
class XspOptionBarsRepo:
def __init__(self, dsn: str) -> None:
kw = {} if dsn.startswith("postgresql") else {"connect_args": {"check_same_thread": False}}
self._engine = create_engine(dsn, **kw)
def migrate(self) -> None:
CockpitBase.metadata.create_all(self._engine)
def upsert_bars(self, rows: list[dict], at: dt.datetime) -> None:
with Session(self._engine) as s:
for r in rows:
obj = XspOptionBarRow(
osi_symbol=r["osi_symbol"], date=dt.date.fromisoformat(r["date"]),
expiry=dt.date.fromisoformat(r["expiry"]), strike=float(r["strike"]),
right=r["right"], close=float(r["close"]), src_updated_at=at)
s.merge(obj) # merge = insert-or-overwrite on the (osi_symbol, date) PK
s.commit()
def bars_for(self, osi_symbols: list[str], start: str, end: str) -> dict[str, dict[str, float]]:
if not osi_symbols:
return {}
lo, hi = dt.date.fromisoformat(start), dt.date.fromisoformat(end)
out: dict[str, dict[str, float]] = {}
with Session(self._engine) as s:
q = select(XspOptionBarRow).where(
XspOptionBarRow.osi_symbol.in_(osi_symbols),
XspOptionBarRow.date >= lo, XspOptionBarRow.date <= hi)
for row in s.scalars(q):
out.setdefault(row.osi_symbol, {})[row.date.isoformat()] = row.close
return out
def chain_asof(self, date: str, dte_lo: int, dte_hi: int) -> list[OptionContract]:
d = dt.date.fromisoformat(date)
exp_lo, exp_hi = d + dt.timedelta(days=dte_lo), d + dt.timedelta(days=dte_hi)
with Session(self._engine) as s:
q = select(XspOptionBarRow).where(
XspOptionBarRow.date == d, XspOptionBarRow.right == "P",
XspOptionBarRow.expiry >= exp_lo, XspOptionBarRow.expiry <= exp_hi)
return [OptionContract(osi_symbol=r.osi_symbol, expiry=r.expiry, strike=r.strike, right=r.right)
for r in s.scalars(q)]

View File

@@ -0,0 +1,41 @@
import datetime as dt
from fxhnt.adapters.persistence.xsp_option_bars import XspOptionBarsRepo
def _repo(tmp_path):
r = XspOptionBarsRepo(f"sqlite:///{tmp_path/'o.db'}")
r.migrate()
return r
def _bar(osi, date, expiry, strike, right, close):
return dict(osi_symbol=osi, date=date, expiry=expiry, strike=strike, right=right, close=close)
def test_upsert_and_bars_for(tmp_path):
r = _repo(tmp_path)
at = dt.datetime(2024, 7, 15)
r.upsert_bars([_bar("XSP240816P00470000", "2024-07-15", "2024-08-16", 470.0, "P", 1.20),
_bar("XSP240816P00470000", "2024-07-16", "2024-08-16", 470.0, "P", 1.35)], at)
assert r.bars_for(["XSP240816P00470000"], "2024-07-15", "2024-07-16") == {
"XSP240816P00470000": {"2024-07-15": 1.20, "2024-07-16": 1.35}}
def test_upsert_is_idempotent(tmp_path):
r = _repo(tmp_path)
at = dt.datetime(2024, 7, 15)
r.upsert_bars([_bar("A", "2024-07-15", "2024-08-16", 470.0, "P", 1.20)], at)
r.upsert_bars([_bar("A", "2024-07-15", "2024-08-16", 470.0, "P", 1.25)], at) # same PK -> overwrite
assert r.bars_for(["A"], "2024-07-15", "2024-07-15") == {"A": {"2024-07-15": 1.25}}
def test_chain_asof_filters_by_dte_window(tmp_path):
r = _repo(tmp_path)
at = dt.datetime(2024, 7, 15)
r.upsert_bars([
_bar("P30", "2024-07-15", "2024-08-14", 470.0, "P", 1.2), # 30 DTE -> in [20,45]
_bar("P10", "2024-07-15", "2024-07-25", 470.0, "P", 0.4), # 10 DTE -> out
_bar("P60", "2024-07-15", "2024-09-13", 470.0, "P", 2.0), # 60 DTE -> out
], at)
got = {c.osi_symbol for c in r.chain_asof("2024-07-15", 20, 45)}
assert got == {"P30"}