"""庫藏股公告後買進策略：完整可重現腳本。

在公司公告的「預定買回期間-起」隔天以開盤價買進，持有固定天數後退出。
本腳本一次跑五種持有天數（10/20/40/60/120 日）並與同視窗的含息 0050 對照，
輸出的數字就是文章表格裡的數字。

⚠️ 事件表要先去重：同一家公司可能在同一天送出多筆買回公告，直接 pivot 會拋
   「Index contains duplicate entries, cannot reshape」而中斷。

執行環境：
    pip install finlab
    python strategy.py

finlab 需要資料時會自動引導登入，不需要在程式裡填 token。
文章：https://finlab.finance/blog/treasury-stock-investment-strategy
"""

import numpy as np
import pandas as pd
from finlab import data
from finlab.backtest import sim

HOLDING_DAYS = [10, 20, 40, 60, 120]
FEE_RATIO = 1.425 / 1000 * 0.2   # 手續費 2 折
RISK_FREE = 0.01


def build_events(close):
    """把「預定買回期間-起」攤平成逐日事件矩陣，只留事件發生當天。"""
    raw = data.get("treasury_stock:預定買回期間-起")
    events = pd.DataFrame({
        "stock_id": [s for s in raw.columns for _ in raw[s].dropna()],
        "dates": [d for s in raw.columns for d in raw[s].dropna()],
    })
    events["value"] = 1

    # 同一檔同一天可能有多筆公告，先去重(語意是「該日有事件」)
    events = events.drop_duplicates(subset=["stock_id", "dates"])
    events = events.pivot(index="dates", columns="stock_id", values="value").notna()
    events = events.reindex(close.index, columns=close.columns).fillna(False)

    # 只保留起始那一天，否則整段買回期間都會被標記
    return events & (~events.shift(-1).fillna(False))


def describe(curve, label):
    curve = curve.dropna()
    curve = curve / curve.iloc[0]
    daily = curve.pct_change().dropna()
    monthly = curve.resample("ME").last().pct_change().dropna()
    years = (curve.index[-1] - curve.index[0]).days / 365.25

    cagr = curve.iloc[-1] ** (1 / years) - 1
    sharpe = (daily.mean() * 252 - RISK_FREE) / (daily.std() * np.sqrt(252))
    sortino = (monthly.mean() * 12 - RISK_FREE) / (monthly[monthly < 0].std() * np.sqrt(12))
    mdd = (curve / curve.cummax() - 1).min()

    print(f"{label:22s} 年化 {cagr:6.1%}  日夏普 {sharpe:5.2f}  月索提諾 {sortino:5.2f}  最大回撤 {mdd:6.1%}")
    return curve


def main():
    close = data.get("price:收盤價")
    events = build_events(close)
    print(f"事件日總數 {int(events.sum().sum())}\n")

    curves = {}
    for days in HOLDING_DAYS:
        position = events.shift(-1).rolling(days).sum().fillna(0)
        report = sim(position, trade_at_price="open", fee_ratio=FEE_RATIO,
                     market="TW_STOCK", upload=False, name=f"庫藏股後持有 {days} 日")
        curves[f"持有 {days} 日"] = report.creturn.dropna()
        holdings = (position > 0).sum(axis=1).replace(0, pd.NA).mean()
        print(f"持有 {days:3d} 日  平均持股 {holdings:.1f} 檔")

    # 基準：0050 含息，切到與策略相同的起始日再各自歸一
    benchmark = data.get("etl:adj_close")["0050"].dropna()
    # 共同起始日取「最晚的那一個」:120 日版本要累積滿 120 天才有部位,
    # 取 min 會讓五個變體從不同日期起算,數字就不可互相比較。
    common_start = max(max(c.index[0] for c in curves.values()), benchmark.index[0])
    end = min(max(c.index[-1] for c in curves.values()), benchmark.index[-1])
    print(f"\n共同起始日 {common_start.date()} ~ {end.date()}\n")

    for label, curve in curves.items():
        describe(curve.loc[common_start:end], label)
    describe(benchmark.loc[common_start:end], "0050 含息買進持有")


if __name__ == "__main__":
    main()
