"""Pure adaptive multi-strat book — faithful port of foxhunt scripts/surfer/multistrat_paper.py
(build/volnorm/trust/book_series). 6 streams (SPY/IEF/GLD/PDBC/DBMF + BTC-USD): vol-normalize each
stream -> edge-decay trust theta -> trust-weighted combination -> adaptive risk layer (EMA vol,
Kelly-floor, continuous self-recovering drawdown de-lever, z-score correlation de-risk, leverage-floor),
UNLEVERED (MAXLEV 1.0). numpy internal; no I/O; no network."""
from __future__ import annotations

import datetime
import math

import numpy as np
import numpy.typing as npt

# The exact instrument list (Yahoo tickers) the original uses, in order.
INSTRUMENTS: list[str] = ["SPY", "IEF", "GLD", "PDBC", "DBMF", "BTC-USD"]

TARGET_VOL, MAXLEV, KELLY_FLOOR, LEV_FLOOR = 0.10, 1.0, 0.5, 0.3
DD_DEADBAND, DD_SENS, DD_FLOOR = 0.05, 3.0, 0.40


def _build(closes: dict[str, dict[str, float]]) -> tuple[list[str], npt.NDArray[np.float64]]:
    """Port of build(): intersect dates across all streams, drop today's incomplete bar, return
    (dates, return-matrix R[T, S]) where R[0, :] = 0."""
    dates = sorted(set.intersection(*[set(closes[s]) for s in INSTRUMENTS]))
    # drop today's INCOMPLETE intraday bar if present — daily-CLOSE strategy.
    today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
    if dates and dates[-1] == today:
        dates = dates[:-1]
    R = np.zeros((len(dates), len(INSTRUMENTS)))
    for j, sym in enumerate(INSTRUMENTS):
        s = np.array([closes[sym][d] for d in dates])
        if len(s) > 1:
            R[1:, j] = s[1:] / s[:-1] - 1
    return dates, R


def _volnorm(col: npt.NDArray[np.float64], tv: float = TARGET_VOL, win: int = 63) -> npt.NDArray[np.float64]:
    out = np.zeros_like(col)
    for t in range(win, len(col)):
        rv = col[t - win:t].std() * math.sqrt(252)
        out[t] = col[t] * min(5.0, tv / (rv + 1e-9))
    return out


def _trust(col: npt.NDArray[np.float64], win: int = 126, a: float = 0.06) -> npt.NDArray[np.float64]:
    th = 1.0
    out = np.ones_like(col)
    for t in range(win, len(col)):
        seg = col[t - win:t]
        sr = seg.mean() / (seg.std() + 1e-9) * math.sqrt(252)
        th = (1 - a) * th + a * float(np.clip((sr + 0.5) / 1.0, 0.1, 1.0))
        out[t] = th
    return out


def book_series(closes: dict[str, dict[str, float]]) -> list[tuple[str, float]]:
    """Faithful port of book_series(R): produce the unlevered daily book return for each booked day.
    Skips the warmup window exactly as the original (book[t+1] populated only for t in [63, T-1))."""
    dates, R = _build(closes)
    S = R.shape[1]
    M = np.column_stack([_volnorm(R[:, j]) for j in range(S)])
    TH = np.column_stack([_trust(M[:, j]) for j in range(S)])
    tw = TH / np.maximum(TH.sum(1, keepdims=True), 1e-9)
    combo = np.nansum(tw * M, axis=1)
    T = len(combo)
    book = np.zeros(T)
    eq = 1.0
    peak = 1.0
    emu = float(np.nanmean(combo[:63])) if T >= 1 else 0.0
    evar = (float(np.nanvar(combo[:63])) if T >= 1 else 0.0) + 1e-12
    chist: list[float] = []
    for t in range(63, T - 1):
        x = combo[t]
        emu = 0.97 * emu + 0.03 * x
        evar = 0.97 * evar + 0.03 * (x - emu) ** 2
        L = min(MAXLEV, TARGET_VOL / (math.sqrt(max(evar, 1e-12) * 252) + 1e-9))
        L = min(L, max(KELLY_FLOOR, (emu * 252) / (evar * 252 + 1e-9)))
        dd = eq / peak - 1.0
        L *= float(np.clip(1 - DD_SENS * max(0.0, -dd - DD_DEADBAND), DD_FLOOR, 1.0))
        sub = np.nan_to_num(M[t - 63:t])
        vmask = sub.std(0) > 1e-12   # only streams that vary (avoid /0 in corrcoef -> NaN)
        if int(vmask.sum()) >= 2:
            k = int(vmask.sum())
            cm = np.corrcoef(sub[:, vmask].T)
            ac = float((np.nansum(cm) - k) / (k * k - k))
        else:
            ac = 0.0
        chist.append(ac)
        if len(chist) > 60:
            h = np.array(chist[-120:])
            z = (ac - h.mean()) / (h.std() + 1e-9)
            L *= float(np.clip(1 - 0.2 * max(0.0, z), 0.5, 1.0))
        L = float(np.clip(L, LEV_FLOOR, MAXLEV))
        book[t + 1] = L * combo[t + 1]
        eq *= (1 + book[t + 1])
        peak = max(peak, eq)
    # emit only the booked tail (warmup days have book==0 and no date booked by the original loop):
    # booked indices are t+1 for t in [63, T-1) -> [64, T-1]
    out: list[tuple[str, float]] = []
    for i in range(64, T):
        out.append((dates[i], float(book[i])))
    return out
