"""Cockpit ORM models — its own Declarative Base so create_all is independent of the factory's Base.
JSON columns work on both SQLite (tests) and Postgres (prod). The forward_nav table becomes a TimescaleDB
hypertable in prod via a guarded DDL call (see forward_nav.py); on SQLite it stays a plain table."""
from __future__ import annotations

import datetime as dt
from typing import Any

from sqlalchemy import JSON, Boolean, Date, DateTime, Float, Integer, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class CockpitBase(DeclarativeBase):
    pass


class ForwardNavRow(CockpitBase):
    __tablename__ = "forward_nav"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    date: Mapped[dt.date] = mapped_column(Date, primary_key=True)   # real DATE — TimescaleDB hypertable dimension
    ret: Mapped[float] = mapped_column(Float)
    nav: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class ForwardSummaryRow(CockpitBase):
    __tablename__ = "forward_summary"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    as_of: Mapped[str] = mapped_column(String(10))
    days: Mapped[int] = mapped_column(Integer)
    nav: Mapped[float] = mapped_column(Float)
    total_return: Mapped[float] = mapped_column(Float)
    sharpe: Mapped[float] = mapped_column(Float)
    maxdd: Mapped[float] = mapped_column(Float)
    gate_status: Mapped[str] = mapped_column(String(8), default="WAIT")
    gate_reason: Mapped[str] = mapped_column(String(256), default="")
    policy_version: Mapped[int | None] = mapped_column(Integer, nullable=True, default=None)
    policy_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, default=None)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class StrategyHealthRow(CockpitBase):
    """Runtime HEALTH verdict per track — a SEPARATE axis from the gate (a track can be legitimately
    WAIT/building AND healthy; STALE/NO_REF/BROKEN is orthogonal). Written each nightly run by
    `forward_health.evaluate_health`; the cockpit reads it to render a LOUD red badge so a live/gated track
    that stops updating surfaces on day 1 (the anti-silent-rot backbone that would have caught a dead
    strategy's silent staleness). One row
    per strategy, replaced each run (a track that recovers clears its stale row). Brand-new table →
    `create_all` (ForwardNavRepo.migrate) creates it; no ALTER, Postgres-safe."""
    __tablename__ = "strategy_health"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    health: Mapped[str] = mapped_column(String(12))         # HEALTHY | STALE | NO_REF | BROKEN
    stale_days: Mapped[int] = mapped_column(Integer, default=0)   # business days stale (0 unless STALE)
    reason: Mapped[str] = mapped_column(String(256), default="")
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class BacktestSummaryRow(CockpitBase):
    __tablename__ = "backtest_summary"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)  # eqfactor_long / eqfactor_ls / eqfactor_tilt
    as_of: Mapped[str] = mapped_column(String(10))            # last rebalance date in the backtest
    cagr: Mapped[float] = mapped_column(Float)
    ann_vol: Mapped[float] = mapped_column(Float)
    sharpe: Mapped[float] = mapped_column(Float)
    max_drawdown: Mapped[float] = mapped_column(Float)
    passed: Mapped[bool] = mapped_column(Boolean)
    dsr: Mapped[float] = mapped_column(Float)
    is_sharpe: Mapped[float] = mapped_column(Float)
    oos_sharpe: Mapped[float] = mapped_column(Float)
    pvalue: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class StrategyRegistryRow(CockpitBase):
    __tablename__ = "strategy_registry"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    display_name: Mapped[str] = mapped_column(String(96))
    sleeve: Mapped[str] = mapped_column(String(32))
    venue: Mapped[str] = mapped_column(String(32), default="")
    capital_base: Mapped[float] = mapped_column(Float, default=0.0)
    gate_spec: Mapped[dict[str, Any]] = mapped_column(JSON)
    enabled: Mapped[bool] = mapped_column(Boolean, default=True)


class StrategyPromotionRow(CockpitBase):
    """One-way, persistent promotion research→deploy: a row here means the track cleared its gate (PASS)
    while flagged `promote_on_pass` and is now part of the fund. Written once by the nightly; never deleted."""
    __tablename__ = "strategy_promotion"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    promoted_at: Mapped[dt.datetime] = mapped_column(DateTime)


class StrategyAnchorRow(CockpitBase):
    """Immutable per-track anchor: the ONLY persistent state for a forward track (spec sub-project A).
    (strategy_id, created_at) PK keeps full lineage — a re-inception archives the old row (sets archived_at)
    and inserts a new one. The ACTIVE anchor is the row with archived_at IS NULL."""
    __tablename__ = "forward_anchor"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    created_at: Mapped[dt.datetime] = mapped_column(DateTime, primary_key=True)
    t0: Mapped[str] = mapped_column(String(10))                       # frozen inception (ISO date) = OOS clock start
    definition_hash: Mapped[str] = mapped_column(String(64))
    definition_version: Mapped[int] = mapped_column(Integer)
    params_json: Mapped[dict[str, Any]] = mapped_column(JSON)         # audit of the hashed return-affecting params
    record_mode: Mapped[str] = mapped_column(String(12))             # "recompute" | "observed"
    archived_at: Mapped[dt.datetime | None] = mapped_column(DateTime, nullable=True, default=None)


class ExecFillRow(CockpitBase):
    __tablename__ = "exec_fills"
    client_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    rebalance_id: Mapped[str] = mapped_column(String(48), index=True)
    strategy_id: Mapped[str] = mapped_column(String(64), index=True, default="")   # "" = back-compat/untagged
    symbol: Mapped[str] = mapped_column(String(32))
    side: Mapped[str] = mapped_column(String(4))
    qty: Mapped[float] = mapped_column(Float)
    price: Mapped[float] = mapped_column(Float)
    fee: Mapped[float] = mapped_column(Float)
    status: Mapped[str] = mapped_column(String(12))
    shortfall_bps: Mapped[float] = mapped_column(Float)
    at: Mapped[dt.datetime] = mapped_column(DateTime)


class IbkrAccountNavRow(CockpitBase):
    """Per-run REAL DU-paper-account snapshot (D2.1) — the actual NLV/cash/gross + positions IBKR reports at
    each `execute-multistrat` run, captured ADDITIVELY alongside the existing intended-book return basis
    (`multistrat_exec_pos`/`forward_record`, unchanged). `run_date` PK — idempotent replace (a same-day re-run
    overwrites, never appends). `positions_json` mirrors `ForwardBookStateRow.extra_json`'s JSON-as-text
    approach so this table works identically on SQLite (tests) and Postgres."""
    __tablename__ = "ibkr_account_nav"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    nlv: Mapped[float] = mapped_column(Float)
    cash: Mapped[float] = mapped_column(Float)
    gross: Mapped[float] = mapped_column(Float)
    positions_json: Mapped[str] = mapped_column(Text)
    at: Mapped[dt.datetime] = mapped_column(DateTime)


class PaperPositionRow(CockpitBase):
    __tablename__ = "paper_positions"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    sleeve: Mapped[str] = mapped_column(String(32), primary_key=True)
    symbol: Mapped[str] = mapped_column(String(32), primary_key=True)
    qty: Mapped[float] = mapped_column(Float)
    entry_price: Mapped[float] = mapped_column(Float)
    entry_date: Mapped[str] = mapped_column(String(10))
    weight: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class PaperTradeRow(CockpitBase):
    __tablename__ = "paper_trades"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    run_date: Mapped[dt.date] = mapped_column(Date, index=True)   # keyed so a re-run replaces this date's trades
    ts: Mapped[dt.datetime] = mapped_column(DateTime)
    sleeve: Mapped[str] = mapped_column(String(32))
    symbol: Mapped[str] = mapped_column(String(32))
    side: Mapped[str] = mapped_column(String(4))
    qty: Mapped[float] = mapped_column(Float)
    price: Mapped[float] = mapped_column(Float)
    reason: Mapped[str] = mapped_column(String(64))


class PaperNavRow(CockpitBase):
    """Equity-per-date series for the paper book (forward snapshot + backfill both write it). One row
    per run_date — idempotent. Timescale-friendly like forward_nav."""
    __tablename__ = "paper_nav"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    capital: Mapped[float] = mapped_column(Float)
    realized: Mapped[float] = mapped_column(Float)
    unrealized: Mapped[float] = mapped_column(Float)
    equity: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class PaperNavSummaryRow(CockpitBase):
    """MATERIALIZED headline nav-summary for the bybit paper book: the stats `nav_backfill_stats` returns
    (final_equity / total_return_pct / sharpe / max_dd_pct) PLUS `since` + `has_record` and the pre-rendered
    multi-year curve SVG. These change ONLY when the nav changes (nightly), yet the Overview + `/paper`
    headline used to re-read the full ~3650-row paper_nav series and re-run stats+curve on EVERY request.
    `refresh_nav_summary` computes this row ONCE (reusing the EXACT same helpers, so it is byte-for-byte
    identical to the live compute) keyed by `nav_run_date` = the nav's latest run_date (the version); the read
    path serves it when the version matches and self-heals otherwise. Authoritative + refresh-on-data-change +
    read-path self-heal — NOT a TTL cache. Singleton row, id=1 (bybit is the sole paper book), replaced on each
    refresh. A brand-new table → `create_all` (PaperRepo.migrate) creates it; no ALTER needed."""
    __tablename__ = "paper_nav_summary"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
    nav_run_date: Mapped[str] = mapped_column(String(10))   # the nav's latest run_date = the version
    final_equity: Mapped[float] = mapped_column(Float)
    total_return_pct: Mapped[float] = mapped_column(Float)
    sharpe: Mapped[float] = mapped_column(Float)
    max_dd_pct: Mapped[float] = mapped_column(Float)
    since: Mapped[str] = mapped_column(String(4))            # the backfill's first year, e.g. "2021"
    has_record: Mapped[bool] = mapped_column(Boolean)
    curve_svg: Mapped[str] = mapped_column(Text)            # the pre-rendered multi-year nav curve (inline SVG)
    computed_at: Mapped[dt.datetime] = mapped_column(DateTime)


class PaperBookStateRow(CockpitBase):
    """Cumulative book-level scalars (key/value) — currently the running realized PnL so closed-position
    gains/losses survive after a position is reduced or exited. key="realized", value=cumulative float."""
    __tablename__ = "paper_book_state"
    key: Mapped[str] = mapped_column(String(32), primary_key=True)
    value: Mapped[float] = mapped_column(Float, default=0.0)


class PaperSleeveRetRow(CockpitBase):
    """One sleeve's simulated daily return for one run_date (backfill + snapshot both write). Feeds the
    ISV-adaptive trailing-vol window. (run_date, sleeve) PK — idempotent per date+sleeve."""
    __tablename__ = "paper_sleeve_ret"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    sleeve: Mapped[str] = mapped_column(String(32), primary_key=True)
    ret: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class BybitSleeveRetRow(CockpitBase):
    """One Bybit-book sleeve's daily return for one run_date — the PRECOMPUTED backtest data the cockpit's
    interactive sim reads (it NEVER recomputes the heavy 4-edge book on a web request). Written nightly by
    the `bybit_4edge_nav` asset from `sleeve_returns_from_store`. Mirrors `PaperSleeveRetRow`'s shape so the
    same cheap capital/start/cost knobs apply. (run_date, sleeve) PK — idempotent per date+sleeve."""
    __tablename__ = "bybit_sleeve_ret"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    sleeve: Mapped[str] = mapped_column(String(32), primary_key=True)
    ret: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class SimCurveRetRow(CockpitBase):
    """One recompute-replay strategy's (multistrat — the IBKR paper books the Backtest page's sim reads)
    daily honest-cost return for one run_date. Mirrors `BybitSleeveRetRow`'s shape/purpose for the Bybit-book
    sim: written nightly by `_persist_track_backtest_ref` (the `fxhnt-backtest-refs` Job) from the SAME
    inception rows `backtest_ref`'s recompute-replay branch derives the reconciliation-gate ref from, so
    /paper/sim NEVER re-runs an IBKR replay on a web request. `strategy_id` doubles as the sim's `sleeve` key
    (each IBKR book is single-sleeve — the whole book IS the strategy). (strategy_id, run_date) PK —
    idempotent per strategy+date."""
    __tablename__ = "sim_curve_ret"
    strategy_id: Mapped[str] = mapped_column(String(32), primary_key=True)
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    ret: Mapped[float] = mapped_column(Float)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class CompareMeasuredCacheRow(CockpitBase):
    """The PRECOMPUTED measured-cost COMPARE artifact the cockpit reads (it NEVER recomputes the heavy
    per-coin measured-cost path on a web request — that OOM-killed the 4Gi dashboard, exit 137).
    Written by the own-memory `fxhnt compare-measured-precompute` Job; the cockpit's `book=compare` measured
    path loads this row and renders it, falling back to the light FLAT-cost compare when the row is absent.
    Tiny: two normalized equity series + the per-book metrics rows + the window, all in one JSON payload.
    `key` PK — idempotent (one row, replaced on every precompute run)."""
    __tablename__ = "compare_measured_cache"
    key: Mapped[str] = mapped_column(String(64), primary_key=True)
    payload: Mapped[dict[str, Any]] = mapped_column(JSON)
    computed_at: Mapped[dt.datetime] = mapped_column(DateTime)


class BybitRealSpreadRow(CockpitBase):
    """The REAL live L1 quoted spread per coin (the forward-realistic cost input), refreshed daily by the
    own-memory `fxhnt bybit-spread-refresh` Job: it SAMPLES the `/v5/market/tickers` top-of-book several times
    a few seconds apart and stores the MEDIAN per coin (robust against one weird moment). The measured-cost
    precompute READS this to charge the Bybit liquid book the REAL spread (taker_fee + 0.5·spread) instead of
    the daily-high/low Corwin–Schultz proxy, which over-states the true quoted spread ~104× (1.4bp real vs
    143.6bp CS). A coin with NO stored real spread (delisted / illiquid) falls back to CS in the cost model.
    `coin` PK — idempotent upsert (one row per coin, replaced on each refresh)."""
    __tablename__ = "bybit_real_spread"
    coin: Mapped[str] = mapped_column(String(32), primary_key=True)
    spread_bps: Mapped[float] = mapped_column(Float)
    fetched_at: Mapped[dt.datetime] = mapped_column(DateTime)


class UnlockEventRow(CockpitBase):
    """The token-unlock dilution-short CALENDAR, moved off the single-node JSON file on the surfer-data PVC
    into the operational DB so the unlock sleeve / evals / Jobs read it over the network (node-agnostic).
    One row per cliff event; columns mirror the JSON event shape `load_unlock_events` returns:
    {"sym","cliff_day","n","cat"}. PK (sym, cliff_day) — idempotent upsert; cliff_day indexed for the
    pre-window scan, sym indexed for per-name lookups."""
    __tablename__ = "unlock_events"
    sym: Mapped[str] = mapped_column(String(32), primary_key=True, index=True)   # base WITHOUT the USDT suffix
    cliff_day: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)  # epoch DAY (ts // 86400)
    n: Mapped[float] = mapped_column(Float)                                       # tokens unlocked at the cliff
    cat: Mapped[str] = mapped_column(String(32))                                  # bearish category (insiders / privateSale)
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class BybitTestnetNavRow(CockpitBase):
    """Post-rebalance NAV for the Bybit testnet real-paper leg (design §4.4) — one row per run_date. Its
    existence is the per-day idempotency marker (`testnet_executed_on`): a re-run on the same date is a no-op.
    Separate from the sim `paper_nav` so the cockpit can show real-paper vs sim side by side and the recon
    gate keeps reading the sim track unchanged. `run_date` PK → idempotent upsert."""
    __tablename__ = "bybit_testnet_nav"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    equity: Mapped[float] = mapped_column(Float)
    realized_pnl: Mapped[float] = mapped_column(Float)
    gross: Mapped[float] = mapped_column(Float)
    n_orders: Mapped[int] = mapped_column(Integer)
    mean_exec_slippage_bps: Mapped[float] = mapped_column(Float)
    mean_timing_drift_bps: Mapped[float] = mapped_column(Float)
    ts: Mapped[dt.datetime] = mapped_column(DateTime)


class BybitTestnetFillRow(CockpitBase):
    """One realized testnet fill with DECOMPOSED slippage (design §3/§4.4): execution slippage vs the live
    order-time mid, timing drift vs the book mark, and the delta vs the A8 assumed cost tier. `low_confidence`
    marks a fill whose mid came from an empty/one-sided thin book (recorded but excluded from the slippage
    verdict). The slippage columns are nullable because a low-confidence fill has no defined mid. Append-only;
    `id` PK (autoincrement), read back per run_date."""
    __tablename__ = "bybit_testnet_fills"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    run_date: Mapped[dt.date] = mapped_column(Date, index=True)
    symbol: Mapped[str] = mapped_column(String(32))
    side: Mapped[str] = mapped_column(String(4))
    qty: Mapped[float] = mapped_column(Float)
    fill_price: Mapped[float] = mapped_column(Float)
    mid_at_order: Mapped[float | None] = mapped_column(Float, nullable=True)
    book_mark: Mapped[float | None] = mapped_column(Float, nullable=True)
    exec_slippage_bps: Mapped[float | None] = mapped_column(Float, nullable=True)
    timing_drift_bps: Mapped[float | None] = mapped_column(Float, nullable=True)
    assumed_tier_bps: Mapped[float] = mapped_column(Float)
    slippage_delta_bps: Mapped[float | None] = mapped_column(Float, nullable=True)
    low_confidence: Mapped[bool] = mapped_column(Boolean, default=False)
    fee: Mapped[float] = mapped_column(Float)
    order_id: Mapped[str] = mapped_column(String(64))
    ts: Mapped[dt.datetime] = mapped_column(DateTime)


class PaperShadowPositionRow(CockpitBase):
    """Unit (sleeve_weight=1, capital=1) sleeve composition per run_date — the kill-PROOF return signal.
    Persisted every date and NEVER flattened by the killswitch, so per-sleeve returns survive kill periods
    (re-entry detection + ISV vol estimate stay correct). (run_date, sleeve, symbol) PK — idempotent."""
    __tablename__ = "paper_shadow_positions"
    run_date: Mapped[dt.date] = mapped_column(Date, primary_key=True)
    sleeve: Mapped[str] = mapped_column(String(32), primary_key=True)
    symbol: Mapped[str] = mapped_column(String(32), primary_key=True)
    qty: Mapped[float] = mapped_column(Float)
    entry_price: Mapped[float] = mapped_column(Float)
    entry_date: Mapped[str] = mapped_column(String(10))
    src_updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class ForwardRecordRow(CockpitBase):
    """Append-only, immutable observed forward return for one `observed`-mode track on one date (spec §3b).
    (strategy_id, date) PK → idempotent per date (a same-day re-run does not double-append). Reused by
    sub-project C for execution fills. Never edited in place — a re-inception starts a fresh log after
    the anchor's new T0."""
    __tablename__ = "forward_record"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    date: Mapped[str] = mapped_column(String(10), primary_key=True)
    ret: Mapped[float] = mapped_column(Float)
    booked_at: Mapped[dt.datetime] = mapped_column(DateTime)


class ForwardBookStateRow(CockpitBase):
    """The minimal carried book-state (e.g. current delta-neutral positions) an `observed` track needs to
    book the next day. One row per track, replaced each run — NOT hand-editable JSON on a PVC. Not a record
    of returns (that is forward_record); just the live carry."""
    __tablename__ = "forward_book_state"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    extra_json: Mapped[dict[str, Any]] = mapped_column(JSON)
    updated_at: Mapped[dt.datetime] = mapped_column(DateTime)


class YahooPitCloseRow(CockpitBase):
    """Point-in-time frozen Yahoo daily adjusted close (spec §4). Yahoo revises adjusted closes
    retroactively (dividends/splits); freezing the first-seen value keeps the multistrat recompute stable.
    (symbol, date) PK — first-seen wins; NEVER overwritten in place."""
    __tablename__ = "yahoo_pit_close"
    symbol: Mapped[str] = mapped_column(String(16), primary_key=True)
    date: Mapped[str] = mapped_column(String(10), primary_key=True)
    adj_close: Mapped[float] = mapped_column(Float)
    first_seen_at: Mapped[dt.datetime] = mapped_column(DateTime)


class YahooPitVolumeRow(CockpitBase):
    """Point-in-time frozen Yahoo daily raw share volume — mirrors `YahooPitCloseRow` (spec §4). Feeds the
    equity-leg dollar-ADV capacity model. (symbol, date) PK — first-seen wins; NEVER overwritten in place."""
    __tablename__ = "yahoo_pit_volume"
    symbol: Mapped[str] = mapped_column(String(16), primary_key=True)
    date: Mapped[str] = mapped_column(String(10), primary_key=True)
    volume: Mapped[float] = mapped_column(Float)
    first_seen_at: Mapped[dt.datetime] = mapped_column(DateTime)


class IbkrPitVolumeRow(CockpitBase):
    """Point-in-time frozen IBKR daily on-exchange volume (reqHistoricalData TRADES) — mirrors
    `YahooPitVolumeRow` for the UCITS lines the fund actually executes. (symbol, date) PK — first-seen
    wins; NEVER overwritten in place."""
    __tablename__ = "ibkr_pit_volume"
    symbol: Mapped[str] = mapped_column(String(16), primary_key=True)
    date: Mapped[str] = mapped_column(String(10), primary_key=True)
    volume: Mapped[float] = mapped_column(Float)
    first_seen_at: Mapped[dt.datetime] = mapped_column(DateTime)


class StrategyAllocationRow(CockpitBase):
    """The latest B2a strategy-level allocation snapshot — one row per strategy, replaced each run.
    target_weight = the ex-ante-sized weight; target_dollars = capital-ceiling-bounded dollars. Stamped with
    the allocation policy that produced it (audit). A pure derived cache of (policy + A's records + B1 status)."""
    __tablename__ = "strategy_allocation"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    target_weight: Mapped[float] = mapped_column(Float)
    target_dollars: Mapped[float] = mapped_column(Float)
    policy_version: Mapped[int] = mapped_column(Integer)
    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 StrategyFundingRow(CockpitBase):
    """B3 funding-status snapshot — one row per effective-deploy strategy, replaced each run. A derived cache
    of (gate policy + A's immutable record). `state` gates B2a's allocation input; `label` is the display."""
    __tablename__ = "strategy_funding"
    strategy_id: Mapped[str] = mapped_column(String(64), primary_key=True)
    state: Mapped[str] = mapped_column(String(16))              # funded | de-funded
    label: Mapped[str] = mapped_column(String(16))             # funded | decaying | de-funded
    decay_run: Mapped[int] = mapped_column(Integer)
    recovery_run: Mapped[int] = mapped_column(Integer)
    first_pass_date: Mapped[str | None] = mapped_column(String(10), nullable=True, default=None)
    policy_version: Mapped[int] = mapped_column(Integer)
    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 FundConfigRow(CockpitBase):
    """Single-row (id=1) fund-level knobs — the SSOT for capital + risk appetite, set ONLY via
    `fxhnt fund-config` (never hand-edited), read by the nightly allocation. Absent row/table → the nightly
    falls back to settings/policy defaults (see application/fund_config.read_fund_config)."""
    __tablename__ = "fund_config"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)   # always 1
    capital: Mapped[float] = mapped_column(Float)
    target_vol: Mapped[float] = mapped_column(Float)
    kelly_fraction: Mapped[float] = mapped_column(Float)
    max_venue_weight: Mapped[float] = mapped_column(Float)
    updated_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)
