"""Aggregate actual funding observations by UTC day and align them with Bitcoin spot returns from a separate source.

Research example. Live observations can change.
"""

from datetime import UTC, datetime
from decimal import Decimal
from textwrap import fill

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

AS_OF = datetime.now(UTC).date()
NAVY, BLUE, PINK = "#132244", "#8fb9ff", "#e7a2cc"


def fetch(qj):
    return {
        "funding": qj.ccxt.get_historical_funding_rates(exchange="binance", symbol="BTC/USDT:USDT", limit=300),
        "crypto": qj.coingecko.get_historical_prices(coin_id="bitcoin", vs_currency="usd", days=365, interval="daily"),
    }


def analyze(raw):
    funding = frame(raw["funding"], "datetime")
    funding["Day"] = funding["datetime"].dt.floor("D")
    funding["Rate"] = pd.to_numeric(funding["fundingRate"], errors="raise")
    daily = funding.groupby("Day")["Rate"].agg(["sum", "count"])
    daily = daily.loc[(daily["count"] == 3) & (daily.index.date < AS_OF)]
    spot = frame(raw["crypto"]).set_index("date")["price"].map(decimal)
    spot = spot.loc[spot.index == spot.index.floor("D")]
    spot = spot.loc[~spot.index.duplicated(keep="last")]
    next_close = spot.reindex(spot.index + pd.Timedelta(days=1))
    returns = pd.Series(
        [ratio(end, start) - 1 if pd.notna(end) else np.nan for start, end in zip(spot, next_close, strict=True)],
        index=spot.index,
        name="Spot return",
    )
    joined = daily.join(returns, how="inner").dropna()
    if joined.empty:
        raise ValueError("No common completed funding and spot days")
    joined["Funding bp"] = joined["sum"] * 10000
    joined["Spot return %"] = joined["Spot return"] * 100
    fig, (top, bottom) = canvas("Bitcoin · perp funding and the spot move", "Observed daily funding · bp", panels=2)
    top.bar(joined.index, joined["Funding bp"], width=0.8, color=BLUE)
    bottom.plot(joined.index, joined["Spot return %"], color=PINK, lw=1)
    bottom.set_ylabel("BTC/USD daily return %")
    bottom.axhline(0, color="#afc1dc", lw=0.8)
    return joined[["Funding bp", "Spot return %", "count"]].reset_index(), fig


def payload(response):
    if hasattr(response, "to_dict"):
        response = response.to_dict()
    while isinstance(response, dict):
        if response.get("error") or response.get("error_code") or response.get("success") is False:
            raise ValueError("Provider request failed")
        if "data" in response:
            response = response["data"]
        elif "value" in response:
            response = response["value"]
        else:
            break
    return response


def frame(response, date="date"):
    rows = payload(response)
    if not isinstance(rows, list) or not rows:
        raise ValueError("No provider observations returned")
    result = pd.DataFrame(rows)
    if date:
        result[date] = pd.to_datetime(result[date], utc=True)
        result = result.sort_values(date)
    return result


def decimal(value):
    if pd.isna(value):
        raise ValueError("Required monetary value or quantity is missing")
    return Decimal(str(value))


def ratio(numerator, denominator):
    denominator = decimal(denominator)
    if denominator == 0:
        raise ValueError("Required denominator is zero")
    return float(decimal(numerator) / denominator)


def canvas(title, ylabel, panels=1):
    plt.rcParams.update(
        {
            "figure.facecolor": NAVY,
            "axes.facecolor": NAVY,
            "savefig.facecolor": NAVY,
            "text.color": "#e9eef8",
            "axes.labelcolor": "#afc1dc",
            "xtick.color": "#afc1dc",
            "ytick.color": "#afc1dc",
            "axes.edgecolor": "#34486b",
            "grid.color": "#34486b",
            "font.size": 10,
            "axes.prop_cycle": plt.cycler(color=[BLUE, PINK, "#ba9dea"]),
        }
    )
    fig, axes = plt.subplots(panels, 1, figsize=(9, 6.1), squeeze=False, layout="constrained")
    fig.suptitle(fill(title, width=58), fontsize=17, x=0.04, ha="left", weight="medium")
    for ax in axes.flat:
        ax.grid(axis="y", alpha=0.45)
        ax.set_axisbelow(True)
        ax.spines[["top", "right"]].set_visible(False)
    axes.flat[0].set_ylabel(ylabel)
    return fig, list(axes.flat)


def run():
    from quantjourney.sdk import QuantJourneyAPI

    qj = QuantJourneyAPI.from_env()
    raw = fetch(qj)
    table, fig = analyze(raw)
    print(table.to_string(index=False))
    fig.savefig(OUTPUT, dpi=180)
    plt.show()


OUTPUT = "sdk_crypto_funding.png"

if __name__ == "__main__":
    run()
