# -*- coding: utf-8 -*-
"""投資組合最佳化實測：效率前緣、min-variance、max-Sharpe vs 1/N 等權。

對應文章：
  https://finlab.finance/blog/portfolio-optimization-intro

執行（需先 pip install finlab）：
  python strategy.py

設定摘要：
  - 股池：0050（台股市值）、0056（台股高股息）、00646（美股 S&P 500）、
    00679B（美債 20 年）
  - 回測區間：2018-01-01 ~ 2026-06-09（資料截止釘日，確保可重現）
  - 權重估計：每月底用過去 252 個交易日日報酬估 mu 與 cov，
    下月生效（walk-forward 樣本外；最早一期允許 200 日以上）
  - 最佳化：長倉、無槓桿，以 2% 格點掃過權重單純形，
    取樣本內波動最小（min-variance）與夏普最高（max-Sharpe，rf=0）兩組
  - 交易成本：finlab sim() 台股預設值（手續費 1.425‰、證交稅 3‰；
    ETF 實際證交稅為 1‰，未調整，屬保守高估）
  - 基準：0050 含息買進持有，etl:adj_close 還原價純指數算術（不含成本）

投資警語：本程式僅供量化研究與教學用途，過去績效不代表未來表現，
不構成任何投資建議；實際交易前請自行評估風險、滑價與交易容量。
"""
import itertools

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

START = "2018-01-01"
END = "2026-06-09"
ETFS = ["0050", "0056", "00646", "00679B"]

# ---------- 載入資料：四檔 ETF 的還原價（含息） ----------
adj = data.get("etl:adj_close")
prices = adj[ETFS].dropna()
prices = prices[prices.index <= END]
rets = prices.pct_change().dropna()

# ---------- 權重格點：2% 步距掃過「四個權重加總 = 1」的所有長倉組合 ----------
step = 0.02
n = int(round(1 / step))
grid = []
for a, b, c in itertools.product(range(n + 1), repeat=3):
    d = n - a - b - c
    if d >= 0:
        grid.append((a, b, c, d))
W_GRID = np.array(grid) * step  # 共 23,426 組權重


def solve_weights(mu, cov):
    """在格點上找出 min-variance 與 max-Sharpe（rf=0）兩組權重。"""
    port_ret = W_GRID @ mu
    port_vol = np.sqrt(np.einsum("ij,jk,ik->i", W_GRID, cov, W_GRID))
    w_minvar = W_GRID[port_vol.argmin()]
    sharpe = port_ret / port_vol
    best = sharpe.argmax()
    if port_ret[best] <= 0:
        # 樣本內所有組合的預期報酬皆非正時，退回 min-variance
        return w_minvar, w_minvar
    return w_minvar, W_GRID[best]


# ---------- 每月底重估：用過去 252 個交易日解出下月權重 ----------
rows_minvar = []
rows_maxsharpe = []
dates = []
for t in rets.resample("ME").last().index:
    hist = rets[rets.index <= t].tail(252)
    if len(hist) < 200:
        continue
    w_mv, w_ms = solve_weights(hist.mean().values, hist.cov().values)
    rows_minvar.append(w_mv)
    rows_maxsharpe.append(w_ms)
    dates.append(t)

w_minvar = pd.DataFrame(rows_minvar, index=dates, columns=ETFS)
w_maxsharpe = pd.DataFrame(rows_maxsharpe, index=dates, columns=ETFS)
w_equal = pd.DataFrame(0.25, index=dates, columns=ETFS)

# ---------- 回測：finlab sim()，月再平衡，預設交易成本 ----------
rep_equal = sim(w_equal, resample="M", upload=False, name="1/N 等權")
rep_minvar = sim(w_minvar, resample="M", upload=False, name="min-variance")
rep_maxsharpe = sim(w_maxsharpe, resample="M", upload=False, name="max-Sharpe")


def metrics(creturn, label):
    """從淨值序列以純算術計算指標，與 0050 基準同口徑。"""
    s = creturn[(creturn.index >= START) & (creturn.index <= END)].dropna()
    r = s.pct_change().dropna()
    years = (s.index[-1] - s.index[0]).days / 365.25
    total = s.iloc[-1] / s.iloc[0] - 1
    return {
        "組合": label,
        "年化報酬%": round(((1 + total) ** (1 / years) - 1) * 100, 2),
        "日夏普": round(r.mean() / r.std() * 252 ** 0.5, 2),
        "最大回撤%": round((s / s.cummax() - 1).min() * 100, 2),
        "總報酬%": round(total * 100, 1),
    }


# ---------- 0050 含息基準：還原價純指數算術（不含成本） ----------
bench = prices["0050"]
bench = bench[bench.index >= START]
bench_creturn = bench / bench.iloc[0]

table = pd.DataFrame([
    metrics(bench_creturn, "0050 含息買進持有"),
    metrics(rep_equal.creturn, "1/N 等權（月再平衡）"),
    metrics(rep_minvar.creturn, "min-variance（月更新）"),
    metrics(rep_maxsharpe.creturn, "max-Sharpe（月更新）"),
])
print(table.to_string(index=False))
