"""Carry research — the term-structure roll-yield factor (Koijen-Moskowitz-Pedersen-Vrugt "Carry").

Unique to this dataset: we hold every outright expiry per future, so per day we read the nearest two
liquid contracts and compute the annualized roll yield (carry) = (P_front/P_next − 1) / Δyears.
Backwardation (front > next) → positive carry → the long earns the roll. The carry sleeve goes long
high-carry / short low-carry, vol-targeted — orthogonal to trend. Every cent accounted for (turnover cost).
"""
from __future__ import annotations

import datetime as dt
import re

import numpy as np

from fxhnt.adapters.data.dbn_local import LocalDbnLoader
from fxhnt.domain.strategies.base import rolling_std

_MONTHS = "FGHJKMNQUVXZ"
_ANN = 252.0


def _expiry_decimal(symbol: str, root: str, obs_year: int, obs_month: int) -> float | None:
    """Decimal expiry year (e.g. 2024.5 for Jun-2024) parsed from a CME symbol, disambiguating the
    1-digit year by the observation date (nearest forward match)."""
    tail = symbol[len(root):]
    if not tail or tail[0] not in _MONTHS:
        return None
    month = _MONTHS.index(tail[0]) + 1
    digits = tail[1:]
    if not digits.isdigit():
        return None
    if len(digits) >= 2:
        year = 2000 + int(digits[-2:])
    else:
        d = int(digits)
        base = (obs_year // 10) * 10 + d
        year = base if (base, month) >= (obs_year, obs_month) else base + 10
    return year + month / 12.0


def carry_signal(path: str, symbol: str) -> tuple[tuple[str, ...], np.ndarray]:
    """Per-day annualized roll yield from the two nearest liquid outright contracts."""
    import databento as db
    import pandas as pd

    df = db.DBNStore.from_file(path).to_df().reset_index()
    df = df[df["close"] > 0]
    df = df[df["symbol"].astype(str).str.match("^" + re.escape(symbol) + "[" + _MONTHS + r"]\d{1,2}$")].copy()
    if df.empty:
        return (), np.array([])
    df["day"] = df["ts_event"].astype("int64") // (86_400 * 10**9)
    ts = pd.to_datetime(df["ts_event"])
    df["oy"], df["om"] = ts.dt.year.to_numpy(), ts.dt.month.to_numpy()

    days, carries = [], []
    for day, g in df.groupby("day"):
        oy, om = int(g["oy"].iloc[0]), int(g["om"].iloc[0])
        rows = []
        for sym, close, vol in zip(g["symbol"], g["close"], g["volume"]):
            exp = _expiry_decimal(str(sym), symbol, oy, om)
            if exp is not None:
                rows.append((exp, float(close), float(vol)))
        if len(rows) < 2:
            continue
        rows.sort(key=lambda r: r[0])                      # by expiry, nearest first
        (e0, p0, _), (e1, p1, _) = rows[0], rows[1]
        dt_years = e1 - e0
        if dt_years <= 0 or p1 <= 0:
            continue
        days.append(day)
        carries.append((p0 / p1 - 1.0) / dt_years)         # >0 backwardation, <0 contango
    dates = tuple(dt.datetime.fromtimestamp(int(d) * 86_400, dt.timezone.utc).strftime("%Y-%m-%d") for d in days)
    return dates, np.array(carries)


def vol_targeted_signal(prices, signal_dates: tuple[str, ...], signal: np.ndarray, *, vol_window: int = 60,
                        target_vol_annual: float = 0.10, cost_bps: float = 1.0, max_leverage: float = 3.0):
    """Generic vol-targeted sleeve: trade `signal` (aligned by date to the continuous prices)."""
    c = np.asarray(prices.close, float)
    n = len(c)
    r = np.zeros(n)
    r[1:] = c[1:] / c[:-1] - 1.0
    r = np.nan_to_num(r)
    sig_by_date = dict(zip(signal_dates, signal))
    sig = np.array([sig_by_date.get(d, 0.0) for d in prices.dates])

    vol = rolling_std(r, vol_window)
    target_day = target_vol_annual / np.sqrt(_ANN)
    raw = np.zeros(n)
    nz = vol > 1e-8
    raw[nz] = np.clip(np.sign(sig[nz]) * target_day / vol[nz], -max_leverage, max_leverage)
    pos = np.zeros(n)
    pos[1:] = raw[:-1]
    net = pos * r - np.abs(np.diff(np.concatenate([[0.0], pos]))) * (cost_bps / 1e4)
    return tuple(prices.dates), net
