# -*- coding: utf-8 -*-
"""実データから「波」（戦略×銘柄）の日次リターン行列を作る共通ライブラリ。

戦略は ELDRA2 が持っている指標そのまま: 移動平均クロス / ボリンジャー逆張り /
ドンチャンブレイク / RSI逆張り / モメンタム。
日足・シグナルは前日終値まで、約定は翌日（ルックアヘッドなし）。

各波は「日次リスクを揃える」ためにボラティリティ正規化する。
これをしないと、単に振れ幅の大きい波が有利に見えるだけになる。
"""
import numpy as np
import pandas as pd

SYMBOLS = ["gold", "nasdaq", "sp500", "dow", "oil", "usdjpy", "eurusd", "silver"]
TARGET_DAILY_VOL = 0.008          # 1日あたりの標準偏差を 0.8% に揃える
VOL_LOOKBACK = 60                 # ボラ推定の窓
COST_PER_TURN = 0.0002            # 往復コスト（建玉が変わった日に2bp引く）


def load(sym):
    df = pd.read_csv(f"data/cache/{sym}.csv", index_col=0, parse_dates=True)
    return df[~df.index.duplicated(keep="first")].sort_index()


# ---- シグナル（すべて前日までの情報だけを使う） ----
def sig_ma(c, fast=20, slow=100):
    return np.sign(c.rolling(fast).mean() - c.rolling(slow).mean())

def sig_bb(c, n=20, k=2.0):
    m, s = c.rolling(n).mean(), c.rolling(n).std()
    pos = pd.Series(0.0, index=c.index)
    pos[c < m - k * s] = 1.0        # 下バンド割れで買い
    pos[c > m + k * s] = -1.0       # 上バンド超えで売り
    return pos.replace(0.0, np.nan).ffill().fillna(0.0)

def sig_donchian(c, n=55):
    hi, lo = c.rolling(n).max(), c.rolling(n).min()
    pos = pd.Series(np.nan, index=c.index)
    pos[c >= hi] = 1.0
    pos[c <= lo] = -1.0
    return pos.ffill().fillna(0.0)

def sig_rsi(c, n=14, lo=30, hi=70):
    d = c.diff()
    up = d.clip(lower=0).ewm(alpha=1/n, adjust=False).mean()
    dn = (-d.clip(upper=0)).ewm(alpha=1/n, adjust=False).mean()
    rsi = 100 - 100 / (1 + up / dn.replace(0, np.nan))
    pos = pd.Series(np.nan, index=c.index)
    pos[rsi < lo] = 1.0
    pos[rsi > hi] = -1.0
    return pos.ffill().fillna(0.0)

def sig_mom(c, n=60):
    return np.sign(c.pct_change(n))

STRATEGIES = {
    "MAクロス":     sig_ma,
    "BB逆張り":     sig_bb,
    "ドンチャン":   sig_donchian,
    "RSI逆張り":    sig_rsi,
    "モメンタム":   sig_mom,
}


def build_waves(with_exposure=False):
    """日次リターンの DataFrame（index=日付, columns='戦略|銘柄'）。

    with_exposure=True なら (リターン, 建玉, 銘柄名) の3つを返す。
    建玉は符号つきのサイズで、「市場が1%動いたときその波がいくら損益を出すか」に等しい。
    一撃（介入・フラッシュクラッシュ）を正しく当てるのに要る。
    """
    out, expo, syms = {}, {}, {}
    for sym in SYMBOLS:
        df = load(sym)
        c = df["close"]
        raw = c.pct_change()
        vol = raw.rolling(VOL_LOOKBACK).std().shift(1)      # 前日までのボラ
        scale = (TARGET_DAILY_VOL / vol).clip(upper=10.0)
        for sname, fn in STRATEGIES.items():
            pos = fn(c).shift(1).fillna(0.0)                 # 翌日に約定
            size = (pos * scale).replace([np.inf, -np.inf], np.nan).fillna(0.0)
            ret = size * raw
            turn = size.diff().abs().fillna(0.0)
            key = f"{sname}|{sym}"
            out[key] = (ret - turn * COST_PER_TURN).fillna(0.0)
            expo[key] = size                      # 符号つき建玉（そのまま一撃の効き方になる）
            syms[key] = sym
    w = pd.DataFrame(out).dropna(how="all")
    # 銘柄ごとに休場日が違う。その日は「取引なし＝リターン0」が正しい扱い。
    w = w.fillna(0.0).replace([np.inf, -np.inf], 0.0)
    w = w.loc[w.index >= "2010-07-01"]                       # 指標のウォームアップを落とす
    if not with_exposure:
        return w
    e = pd.DataFrame(expo).reindex(w.index).fillna(0.0).replace([np.inf, -np.inf], 0.0)
    return w, e[w.columns], [syms[c] for c in w.columns]
