"""Motor del estudio: EMAs, señales y recursión de capital con spread y swap.

Protocolo §2 (reglas) y §4 (capital, costos). Las barras se numeran desde 1: la barra t es el
elemento t - 1 de las listas de precios y fechas. Todo el cálculo usa float de 64 bits.

El motor se divide en dos partes que no se ven entre sí:

- `pending_targets` mira solo las EMAs y decide qué dirección se ejecuta en cada barra.
- `simulate` mira solo precios, fechas, tasas y esas ejecuciones, y lleva el capital.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from datetime import date

WARMUP = 1_000  # protocolo §2
EMA_PAIRS = ((9, 20), (9, 21), (12, 26), (50, 200))  # protocolo §5: variantes 1 a 4
LONG, SHORT = 1, -1  # protocolo §4: d = +1 larga, -1 corta
DAY_COUNT = 360.0  # protocolo §4: base de 360 dias del swap


@dataclass(frozen=True)
class Scenario:
    """Escenario de costos: spread por lado (dólares por euro) y cargo m (fracción anual)."""

    name: str
    spread: float
    m: float


SCENARIOS = (
    Scenario("low", 0.0001, 0.005),  # protocolo §4
    Scenario("base", 0.0002, 0.01),  # protocolo §4
    Scenario("high", 0.0004, 0.02),  # protocolo §4
)


@dataclass(frozen=True)
class Run:
    """Resultado de una corrida para las barras warmup + 1, ..., L (una entrada por barra)."""

    dates: tuple[date, ...]
    prices: tuple[float, ...]
    d: tuple[int, ...]  # dirección tras ejecutar en la barra: +1, -1 o 0
    k_minus: tuple[float, ...]  # capital antes del costo de ejecución de la barra
    k_plus: tuple[float, ...]  # capital después del costo
    swap: tuple[float, ...]  # swap del intervalo entre la barra y la siguiente
    cost: tuple[float, ...]  # costo de spread de las ejecuciones de la barra
    ret: tuple[float, ...]  # R_t = K_t+ / K_{t-1}+ - 1
    sides: tuple[int, ...]  # lados ejecutados en la barra: 0, 1 o 2


def ema(prices: Sequence[float], n: int) -> list[float]:
    """EMA(1) = P_1 y EMA(t) = alfa * P_t + (1 - alfa) * EMA(t - 1), con alfa = 2 / (n + 1)."""
    alpha = 2.0 / (n + 1)  # protocolo §2
    values: list[float] = []
    for price in prices:
        values.append(price if not values else alpha * price + (1.0 - alpha) * values[-1])
    return values


def pending_targets(
    fast: Sequence[float], slow: Sequence[float], warmup: int = WARMUP
) -> list[int | None]:
    """Dirección que se ejecuta en cada barra, según las reglas de §2.

    El resultado se indexa por número de barra (el índice 0 no se usa): targets[t] es +1, -1 o
    None. Posición inicial: en la barra `warmup` la variante toma la dirección del signo de
    (rápida - lenta) y la ejecuta en la barra warmup + 1; un empate exacto no toma posición.
    Señales para t >= warmup + 1 con las cuatro desigualdades de §2, ejecutadas en t + 1; una
    señal detectada en la barra L no se ejecuta. Las señales en la dirección vigente se ignoran
    al contabilizar (`simulate`), no aquí.
    """
    length = len(fast)
    if len(slow) != length:
        raise ValueError("las dos EMAs deben tener la misma longitud")
    targets: list[int | None] = [None] * (length + 1)
    if length < warmup + 1:
        return targets
    if fast[warmup - 1] > slow[warmup - 1]:
        targets[warmup + 1] = LONG
    elif fast[warmup - 1] < slow[warmup - 1]:
        targets[warmup + 1] = SHORT
    for t in range(warmup + 1, length):  # barras warmup + 1, ..., L - 1
        now_fast, now_slow = fast[t - 1], slow[t - 1]
        before_fast, before_slow = fast[t - 2], slow[t - 2]
        if now_fast > now_slow and before_fast <= before_slow:
            targets[t + 1] = LONG
        elif now_fast < now_slow and before_fast >= before_slow:
            targets[t + 1] = SHORT
    return targets


def reference_targets(length: int, warmup: int = WARMUP) -> list[int | None]:
    """Referencia de contexto: larga 1x que abre en la barra warmup + 1 y no tiene señales."""
    targets: list[int | None] = [None] * (length + 1)
    if length >= warmup + 1:
        targets[warmup + 1] = LONG
    return targets


def spread_cost(sides: int, spread: float, price: float, k_minus: float) -> float:
    """c_t = lados * (spread / P_t) * K_t-."""
    return sides * (spread / price) * k_minus  # protocolo §4


def swap_amount(k_plus: float, d: int, r_eur: float, r_usd: float, m: float, days: float) -> float:
    """S_t = K_t+ * |d_t| * (r_comprada - r_vendida - m) * D_t / 360.

    En largo EUR/USD la moneda comprada es el euro y la vendida el dólar; en corto, al revés.
    """
    if d == 0:
        return 0.0
    bought, sold = (r_eur, r_usd) if d > 0 else (r_usd, r_eur)
    return k_plus * abs(d) * (bought - sold - m) * days / DAY_COUNT


def simulate(
    prices: Sequence[float],
    dates: Sequence[date],
    targets: Sequence[int | None],
    r_eur: Sequence[float],
    r_usd: Sequence[float],
    spread: float,
    m: float,
    *,
    swap_on: bool = True,
    warmup: int = WARMUP,
) -> Run:
    """Recursión de capital de §4 para t = warmup + 1, ..., L.

    K_t- = K_{t-1}+ * [1 + d_{t-1} * (P_t / P_{t-1} - 1)] + S_{t-1} y K_t+ = K_t- - c_t, con
    K_warmup+ = 1, d = 0 hasta la barra warmup y S_warmup = 0. Una ejecución cambia la dirección:
    desde sin posición paga un lado, un cambio de dirección paga dos y una orden en la dirección
    vigente se ignora sin costo. En la barra L solo se cierra la posición vigente (un lado, o
    cero si no hay posición): ni las señales ni las ejecuciones pendientes abren nada.
    """
    length = len(prices)
    if not (len(dates) == len(r_eur) == len(r_usd) == length and len(targets) == length + 1):
        raise ValueError("precios, fechas, tasas y ejecuciones deben tener longitudes coherentes")
    if length < warmup + 2:
        raise ValueError(f"la ventana necesita al menos {warmup + 2} barras y trae {length}")

    out: dict[str, list] = {
        key: [] for key in ("d", "k_minus", "k_plus", "swap", "cost", "ret", "sides")
    }
    d_prev, k_prev, swap_prev = 0, 1.0, 0.0  # protocolo §4: K = 1, d = 0, S = 0
    for t in range(warmup + 1, length + 1):
        price, price_prev = prices[t - 1], prices[t - 2]
        k_minus = k_prev * (1.0 + d_prev * (price / price_prev - 1.0)) + swap_prev  # protocolo §4
        if t == length:
            sides, d = (1 if d_prev != 0 else 0), 0
        else:
            target = targets[t]
            if target is None or target == d_prev:
                sides, d = 0, d_prev
            else:
                sides, d = (1 if d_prev == 0 else 2), target
        cost = spread_cost(sides, spread, price, k_minus)
        k_plus = k_minus - cost
        if swap_on and d != 0:
            days = float((dates[t] - dates[t - 1]).days)
            swap = swap_amount(k_plus, d, r_eur[t - 1], r_usd[t - 1], m, days)
        else:
            swap = 0.0
        for key, value in (
            ("d", d),
            ("k_minus", k_minus),
            ("k_plus", k_plus),
            ("swap", swap),
            ("cost", cost),
            ("ret", k_plus / k_prev - 1.0),
            ("sides", sides),
        ):
            out[key].append(value)
        d_prev, k_prev, swap_prev = d, k_plus, swap
    return Run(
        dates=tuple(dates[warmup:]),
        prices=tuple(prices[warmup:]),
        d=tuple(out["d"]),
        k_minus=tuple(out["k_minus"]),
        k_plus=tuple(out["k_plus"]),
        swap=tuple(out["swap"]),
        cost=tuple(out["cost"]),
        ret=tuple(out["ret"]),
        sides=tuple(out["sides"]),
    )
