# -*- coding: utf-8 -*-
"""pandas-datareader 替代方案實測:SMA60 擇時 + 生存者偏差量化。

對應文章:
  https://finlab.finance/blog/pandas-datareader-historical-stock-prices

執行(任一現代 Python 環境):
  pip install yfinance finlab pandas numpy matplotlib
  python strategy.py

實驗 A:用 yfinance 重現 2020 年舊教學(^TWII 收盤 > 60 日均線就持有),
        並延長到 2026-06,加上交易成本敏感度。
實驗 B:同一條規則套到台股個股,「含下市股票全股池」與「僅現存股池」
        各跑一次,量化生存者偏差。

投資警語:本程式僅供量化研究與教學用途,過去績效不代表未來表現,
不構成任何投資建議;實際交易前請自行評估風險、滑價與交易容量。
"""
import numpy as np
import pandas as pd
import yfinance as yf

DATA_END = "2026-06-09"   # 資料釘日,讓結果可重現
FEE = 0.001425            # 券商手續費(未打折)
TAX = 0.003               # 證交稅(賣出)


def perf_stats(daily_ret: pd.Series) -> dict:
    """由日報酬序列計算 CAGR / 夏普 / 最大回撤。"""
    daily_ret = daily_ret.dropna()
    equity = (1 + daily_ret).cumprod()
    years = (equity.index[-1] - equity.index[0]).days / 365.25
    total = float(equity.iloc[-1] - 1)
    cagr = (1 + total) ** (1 / years) - 1
    sharpe = daily_ret.mean() / daily_ret.std() * np.sqrt(252)
    mdd = float((equity / equity.cummax() - 1).min())
    return {
        "總報酬 %": round(total * 100, 1),
        "CAGR %": round(cagr * 100, 2),
        "日夏普": round(float(sharpe), 2),
        "MDD %": round(mdd * 100, 2),
    }


# ====================== 實驗 A:^TWII SMA60 擇時 ======================

# 下載台股加權指數(pandas-datareader 已抓不到,改用 yfinance)
twii = yf.Ticker("^TWII").history(start="2000-01-01", auto_adjust=False)
close = twii["Close"].dropna()
close.index = close.index.tz_localize(None)
close = close[close.index <= DATA_END]

# 60 日簡單移動平均
sma60 = close.rolling(60).mean()

# 訊號:收盤站上 60 日均線
signal = (close > sma60).fillna(False)

# 訊號日收盤進場,持有隔日報酬(訊號與報酬錯開一天,避免前視)
position = signal.shift(1).fillna(False)
daily_return = close.pct_change().fillna(0.0)
strategy_return = daily_return.where(position, 0.0)

# 交易成本:進場日扣買進手續費,出場日扣賣出手續費 + 證交稅
entry_day = position & ~position.shift(1).fillna(False)
exit_day = ~position & position.shift(1).fillna(False)
cost_factor = pd.Series(1.0, index=daily_return.index)
cost_factor[entry_day] = 1 - FEE
cost_factor[exit_day] = 1 - FEE - TAX
strategy_return_with_cost = (1 + strategy_return) * cost_factor - 1

print("=== 實驗 A:^TWII 收盤 > SMA60(2000-01 ~ 2026-06)===")
print("買進持有:", perf_stats(daily_return))
print("擇時.無成本:", perf_stats(strategy_return))
print("擇時.含成本:", perf_stats(strategy_return_with_cost))
print("進出次數:", int(entry_day.sum()))


# ==================== 實驗 B:生存者偏差量化(finlab) ====================
# finlab.login() 會自動引導登入
from finlab import data
from finlab.backtest import sim

# 還原股價(含 369 檔已下市股票,這是量化生存者偏差的關鍵)
adj_close = data.get("etl:adj_close")
adj_close = adj_close[adj_close.index <= DATA_END]

# 股池:台股普通股(4 碼、非 0 開頭,排除 ETF / 權證 / TDR)
common_stocks = [
    s for s in adj_close.columns
    if s.isdigit() and len(s) == 4 and not s.startswith("0")
]
adj_common = adj_close[common_stocks]

# 存續股池 = 資料截止日前 10 個交易日內仍有報價的股票
# (模擬「用 yfinance 只抓得到今天還掛牌的股票」)
last_quote_date = adj_common.apply(lambda s: s.last_valid_index())
cutoff = adj_common.index[-10]
survivors = last_quote_date[last_quote_date >= cutoff].index.tolist()

# 同一條規則:還原價站上 60 日均線就等權持有,週再平衡
sma60_stocks = adj_common.rolling(60).mean()
signal_stocks = (adj_common > sma60_stocks).fillna(False)

position_full = signal_stocks              # 含下市股票
position_survivor = signal_stocks[survivors]  # 僅現存股票(事後諸葛)

report_full = sim(position_full, resample="W", upload=False)
report_survivor = sim(position_survivor, resample="W", upload=False)


def clipped_stats(report) -> dict:
    """sim() 的 creturn 會延伸到執行當日;統計前先截斷到資料釘日,
    再對截斷後的權益曲線用純算術計算,與實驗 A 同一套口徑。"""
    creturn = report.creturn
    creturn = creturn[creturn.index <= DATA_END]
    return perf_stats(creturn.pct_change().dropna())


print("\n=== 實驗 B:同一策略、兩種股池(finlab sim 預設含成本)===")
print("全股池(含下市)股票數:", len(common_stocks))
print("現存股池股票數:", len(survivors))
print("全股池:", clipped_stats(report_full))
print("現存股池:", clipped_stats(report_survivor))
print("兩者 CAGR 的差,就是生存者偏差的價格")
