"""營業利益率三因子＋大盤半年線濾網（/strategies/operating-margin-three-factor-market-filter 的數字來源）。

底策略與 /strategies/operating-margin-three-factor 相同：
- 股票池：近 20 日平均成交金額大於 1,000 萬
- 營業利益率、120 日動能、60 日波動度（越低越好）三項在池內做百分位排名後相加
- 取分數前 20 檔，分數平方加權，2018 年起每月換股，sim() 預設手續費 0.1425% 與證交稅 0.3%

大盤濾網（/blog/taiex-moving-average-market-filter 驗證過的版本）：
- 發行量加權股價報酬指數收盤跌破 120 日均線 ×（1 − 2%），隔天全部賣出、空手
- 收盤回到 120 日均線以上，隔天買回底策略當時的持股與權重；介於兩條線之間維持前一天的狀態
- 底策略照原本的日子換股，空手期間換股也維持空手

統計口徑：日夏普 = 日報酬平均 / 標準差 × √252，不扣無風險利率；
0050 含息 = etl:adj_close 還原價買進持有，與策略同一段期間。
執行後會在目前目錄輸出 data.csv（淨值曲線）與 metrics.json（統計）。

    pip install finlab
    python strategy.py

finlab 需要資料時會自動引導登入，不需要在程式裡填 token。
"""

import json

import pandas as pd

import finlab
from finlab import data
from finlab.backtest import sim

STRATEGY_NAME = "營業利益率三因子＋大盤半年線濾網"
START = "2018-01-01"
TOP_N = 20             # 持股檔數
MOMENTUM_DAYS = 120    # 動能回看交易日
VOLATILITY_DAYS = 60   # 波動度回看交易日


def build_base_report():
    close = data.get("price:收盤價")
    volume = data.get("price:成交股數")
    operating_margin = data.get("fundamental_features:營業利益率").index_str_to_date()
    operating_margin = operating_margin.reindex(close.index, method="ffill")  # 季財報對齊公布時點後補到日頻

    liquid = ((close * volume).rolling(20).mean() > 10_000_000) & close.notna()

    def rank_pct(factor, ascending=True):
        return factor.where(liquid).rank(axis=1, pct=True, ascending=ascending)

    momentum = close.pct_change(MOMENTUM_DAYS, fill_method=None)
    volatility = close.pct_change(fill_method=None).rolling(VOLATILITY_DAYS).std()
    score = rank_pct(operating_margin) + rank_pct(momentum) + rank_pct(volatility, ascending=False)

    in_top = score.rank(axis=1, ascending=False) <= TOP_N
    weight = score.where(in_top, 0).clip(lower=0) ** 2
    weight = weight.div(weight.sum(axis=1).replace(0, float("nan")), axis=0).fillna(0)
    # 原策略固定不上傳；字典傳參避免被雲端轉換器當成最終報告。
    return sim(weight[weight.index >= START], resample="M", name=STRATEGY_NAME, **{"upload": False})


# 大盤濾網：加權報酬指數跌破 120 日均線（半年線）×（1 − 2%）就空手，收回均線以上才恢復持有
MA_DAYS = 120
BUFFER = 0.02


def market_exposure() -> pd.Series:
    """每天收盤後的曝險：1 照底策略持有，0 空手；介於兩條線之間維持前一天的狀態。"""
    index = data.get("benchmark_return:發行量加權股價報酬指數")["發行量加權股價報酬指數"].dropna()
    average = index.rolling(MA_DAYS).mean()
    state = pd.Series(float("nan"), index=index.index)
    state[index >= average] = 1.0
    state[index < average * (1 - BUFFER)] = 0.0
    return state.ffill().fillna(1.0)


def build_report():
    """底策略每次換股的權重延用到下次換股；大盤濾網切換的那天，同一份權重乘上曝險重新下單。"""
    base = build_base_report()
    days = data.get("price:收盤價").index
    days = days[days <= base.creturn.index[-1]]
    # 換股日落在資料最後一天與下一個工作日之間（例如月底是週六）時，用最後一天的訊號照常換股
    weights = base.position[base.position.index < days[-1] + pd.offsets.BDay()]
    # sim 的換股日（例如每月 14 日）遇到休市時，用的是之前最後一個交易日的訊號
    weights.index = days[days.searchsorted(weights.index, side="right") - 1]
    weights = weights[~weights.index.duplicated(keep="last")]
    days = days[days >= weights.index[0]]

    exposure = market_exposure().reindex(days, method="ffill").fillna(1.0)
    switches = exposure.index[exposure.ne(exposure.shift())][1:]
    position = weights.reindex(days, method="ffill").fillna(0.0).mul(exposure, axis=0)
    return sim(position, resample=weights.index.union(switches), upload=False, name=STRATEGY_NAME)


def curve_stats(curve: pd.Series) -> dict:
    curve = curve / curve.iloc[0]
    daily = curve.pct_change().dropna()
    years = (curve.index[-1] - curve.index[0]).days / 365.25
    return {
        "cagr": curve.iloc[-1] ** (1 / years) - 1,
        "daily_sharpe": daily.mean() / daily.std() * 252 ** 0.5,
        "max_drawdown": (curve / curve.cummax() - 1).min(),
        "total_return": curve.iloc[-1] - 1,
    }


def main() -> None:
    finlab.login()
    bench_all = data.get("etl:adj_close")["0050"].dropna()
    report = build_report()

    # 從第一次有部位的那天起算，0050 含息用同一段期間
    creturn = report.creturn.dropna()
    first_move = (creturn != creturn.iloc[0]).argmax()
    creturn = creturn.iloc[max(first_move - 1, 0):]
    bench = bench_all.loc[creturn.index[0]:creturn.index[-1]]

    strategy_stats = curve_stats(creturn)
    bench_stats = curve_stats(bench)
    holdings = (report.position.fillna(0) != 0).sum(axis=1)
    strategy_stats["avg_holdings"] = holdings[holdings > 0].mean()

    start, end = creturn.index[0].date(), creturn.index[-1].date()
    print(f"期間 {start} 至 {end}")
    for label, s in [("策略", strategy_stats), ("0050 含息", bench_stats)]:
        print(f"{label}：年化 {s['cagr']:.2%}，日夏普 {s['daily_sharpe']:.2f}，最大回撤 {s['max_drawdown']:.2%}")

    curves = pd.DataFrame({STRATEGY_NAME: creturn / creturn.iloc[0], "0050含息": bench / bench.iloc[0]})
    curves.index.name = "date"
    curves.round(6).to_csv("data.csv")
    metrics = {
        "window": f"{start}/{end}",
        "star": STRATEGY_NAME,
        "variants": {STRATEGY_NAME: {k: round(float(v), 6) for k, v in strategy_stats.items()}},
        "benchmark_0050": {k: round(float(v), 6) for k, v in bench_stats.items()},
        "run_date": str(pd.Timestamp.today().date()),
        "finlab_version": finlab.__version__,
        "generated_by": "static/blog/taiex-moving-average-market-filter/operating-margin-three-factor-market-filter/strategy.py",
    }
    with open("metrics.json", "w", encoding="utf-8") as f:
        json.dump(metrics, f, ensure_ascii=False, indent=2)


if __name__ == "__main__":
    main()
