# ============================================================================
# 程式交易三策略回測（finlab.finance/tools/program-trading 頁面數字來源）
# ----------------------------------------------------------------------------
# 三套策略共用「月頻自動再平衡」的執行框架，差別在選股邏輯：
#   1. 動能輪動（動能＋低波動＋品質）    ← 頁面主軸
#   2. 均線趨勢過濾（多頭排列＋動能）
#   3. 月營收公告自動換股（營收年增＋短動能）
# Benchmark：0050 含息（etl:adj_close，純算術買進持有，不經 sim、不含成本）。
# 執行：安裝 finlab 後直接跑（finlab.login() 會自動引導登入）。
# 回測區間：2018-01-01 ~ 2026-06-09；sim() 已內扣手續費 0.1425% 與證交稅 0.3%。
# ============================================================================
import numpy as np
from finlab import data
from finlab.backtest import sim

START, END = '2018-01-01', '2026-06-09'

# ---- 載入資料 ---------------------------------------------------------------
close = data.get('price:收盤價')
adj   = data.get('etl:adj_close')                                    # 含息，benchmark 用
rev   = data.get('monthly_revenue:去年同月增減(%)')
roe   = data.get('fundamental_features:ROE稅後').index_str_to_date()  # 對齊公布日，避免前視偏差
mktv  = data.get('etl:market_value')
volume = data.get('price:成交股數')
amount = (close * volume).rolling(60).mean()                         # 近 60 日日均成交額


def monthly(df):
    return df.reindex(close.index, method='ffill').resample('ME').last()


def run(position, resample='M', offset=None, name=''):
    kwargs = dict(resample=resample, fee_ratio=0.001425, tax_ratio=0.003,
                  upload=False, name=name)
    if offset:
        kwargs['resample_offset'] = offset
    report = sim(position[position.index >= START], **kwargs)
    print(name, report.get_stats())
    return report


# ---- 策略一：動能輪動（動能＋低波動＋品質，頁面主軸）-------------------------
mom3m = monthly(close.pct_change(60))
mom6m = monthly(close.pct_change(120).shift(20))     # 扣最近 1 月，避免短期反轉
volm  = monthly(close.pct_change().rolling(60).std())
roem  = monthly(roe)
revm  = monthly(rev)
mktvm = monthly(mktv)

pool = (mktvm.rank(axis=1, ascending=False) <= 200) & (roem > 0) & (revm > 0)


def rk(df, asc=True):
    return df.where(pool).rank(axis=1, pct=True, ascending=asc).fillna(0)


score  = rk(mom3m) + rk(mom6m) + rk(volm, asc=False) + rk(roem)
mask   = score.where(pool).rank(axis=1, ascending=False) <= 20
masked = (score.where(mask, 0)) ** 2                  # 分數平方加權，集中在高分股
w1     = masked.div(masked.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)

report_momentum = run(w1, offset='14D', name='動能輪動')

# ---- 策略二：均線趨勢過濾（多頭排列＋動能）----------------------------------
ma20, ma60, ma120 = close.rolling(20).mean(), close.rolling(60).mean(), close.rolling(120).mean()
mom6 = close.pct_change(120)
pool_trend = (mktv.rank(axis=1, ascending=False) <= 200) & (amount.rank(axis=1, pct=True) > 0.5)
uptrend = (close > ma120) & (ma20 > ma60)
w2 = mom6.where(pool_trend & uptrend).is_largest(20)

report_trend = run(w2, name='均線趨勢過濾')

# ---- 策略三：月營收公告自動換股 ----------------------------------------------
revd = rev.reindex(close.index, method='ffill')
mom3 = close.pct_change(60)
pool_rev = ((mktv.rank(axis=1, ascending=False) <= 250)
            & (amount.rank(axis=1, pct=True) > 0.5) & (revd > 20))


def rk_rev(df):
    return df.where(pool_rev).rank(axis=1, pct=True).fillna(0)


score3 = rk_rev(revd) + rk_rev(mom3)
mask3  = score3.where(pool_rev).rank(axis=1, ascending=False) <= 15
w3     = score3.where(mask3, 0)
w3     = w3.div(w3.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)

report_revenue = run(w3, offset='14D', name='月營收公告自動換股')

# ---- Benchmark：0050 含息（純算術買進持有）----------------------------------
px = adj['0050'].dropna().loc[START:END]
curve = (1 + px.pct_change().fillna(0)).cumprod()
years = (curve.index[-1] - curve.index[0]).days / 365.25
print('0050 含息 buy & hold：CAGR %.2f%%，總報酬 %.1f%%，最大回撤 %.2f%%' % (
    (curve.iloc[-1] ** (1 / years) - 1) * 100,
    (curve.iloc[-1] - 1) * 100,
    (curve / curve.cummax() - 1).min() * 100,
))
