# -*- coding: utf-8 -*-
"""
量化交易是什麼 — 本文所有回測數字的可追溯來源(single source of truth)

環境:finlab(conda)。執行:
    eval "$(/Users/.../miniconda3/bin/conda shell.bash hook)" && conda activate finlab
    python backtest.py

設計理念(對應文章「什麼是量化」的核心):
  量化 = 用「明確、可重複的規則」做決策,再用歷史資料客觀回測驗證。
  為了把「規則化 + 可驗證」講到最清楚,本文用「單一明確規則」當主角:
      規則策略 A:只買「最新月營收年增 > 0」的全部股票,每月換股。
  並擺出兩個對照,讓「量化能客觀分辨好壞」一目了然:
      對照 B(贏):四因子 rank 複合(營收動能+價格動能+高ROE+低波),選 40 檔。
      對照 C(輸):naive 低本益比,買最便宜的 20 檔 —— 直覺對,實測卻失效。
  基準:0050 含息 buy & hold(用 etl:adj_close,鐵則)。

回測共同假設:
  期間 2018-01-01 ~ 資料最新;月頻換股(resample='M');
  手續費/滑價用 finlab sim 預設(台股 0.1425% 手續費 + 0.3% 證交稅,賣出計稅)。
  所有報酬/夏普/回撤皆 sim().get_stats() 直接輸出,未經人工調整。
"""
import warnings; warnings.filterwarnings("ignore")
import json
import numpy as np
import pandas as pd
from finlab import data
from finlab.backtest import sim

START = '2018-01-01'

# ---------- 資料 ----------
close = data.get('price:收盤價')
adj   = data.get('etl:adj_close')                      # 含息,benchmark / 報酬用
pe    = data.get('price_earning_ratio:本益比')
roe   = data.get('fundamental_features:ROE稅後').index_str_to_date().reindex(close.index, method='ffill')
rev   = data.get('monthly_revenue:去年同月增減(%)').reindex(close.index, method='ffill')

# 流動性過濾:近 60 日成交額中位數 > 1 億,確保策略能實際買進
amount = (data.get('price:成交股數') * close)
univ = amount.rolling(60).median() > 1e8                # 可交易宇宙

# ---------- 0050 含息 buy & hold ----------
e = adj['0050'].dropna(); e = e[e.index >= START]
yrs = (e.index[-1] - e.index[0]).days / 365.25
cagr_0050 = (e.iloc[-1] / e.iloc[0]) ** (1 / yrs) - 1
# 0050 最大回撤(含息)
roll_max = e.cummax()
mdd_0050 = ((e - roll_max) / roll_max).min()

def topN(df, n, largest=True, lo=None, hi=None):
    d = pd.DataFrame(df).astype(float)
    if lo is not None: d = d.where(d > lo)
    if hi is not None: d = d.where(d < hi)
    return d.rank(axis=1, ascending=not largest) <= n

def masked_rank(df):
    """在可交易宇宙(univ)內做百分位排名,避免買到沒量的股票。"""
    return df.where(univ).rank(axis=1, pct=True)

def run(name, pos, resample_offset=None, save_html=None):
    pos = pos[pos.index >= START]
    rpt = sim(pos, resample='M', upload=False, resample_offset=resample_offset)
    s = rpt.get_stats()
    out = dict(
        name=name,
        cagr=round(float(s['cagr']) * 100, 2),
        daily_sharpe=round(float(s.get('daily_sharpe', np.nan)), 2),
        monthly_sortino=round(float(s.get('monthly_sortino', np.nan)), 2),
        daily_sortino=round(float(s.get('daily_sortino', np.nan)), 2),
        max_drawdown=round(float(s['max_drawdown']) * 100, 2),
        win_ratio=round(float(s.get('win_ratio', np.nan)) * 100, 1),
        n_holdings=int(pos.sum(axis=1).replace(0, np.nan).mean()),
    )
    if save_html:
        rpt.to_html(save_html)
    return out, rpt

results = {}

# 規則策略 A:單一明確規則 — 月營收年增 > 0(每月換股,可交易宇宙內)
posA = (rev > 0) & univ
results['rule_revenue'], rptA = run('規則:月營收年增>0', posA,
                                    save_html='/tmp/wiqt-charts/report_rule.html')

# 對照 B(贏):四因子 rank 複合,選 40 檔,對齊月營收公布日(+14D)
mom = close / close.shift(60) - 1
vol = close.pct_change().rolling(60).std()
score = (masked_rank(rev) + masked_rank(mom)
         + masked_rank(roe) + masked_rank(-vol))
posB = score.rank(axis=1, ascending=False) <= 40
results['four_factor'], rptB = run('四因子複合(選40)', posB, resample_offset='14D',
                                   save_html='/tmp/wiqt-charts/report_strategy.html')

# 對照 C(輸):naive 低本益比,買最便宜 20 檔
posC = topN(pe, 20, largest=False, lo=0, hi=100) & univ
results['naive_lowpe'], rptC = run('naive 低本益比(選20)', posC,
                                   save_html='/tmp/wiqt-charts/report_baseline.html')

# 0050
results['bench_0050'] = dict(name='0050 含息 buy & hold',
                             cagr=round(cagr_0050 * 100, 2),
                             daily_sharpe=None,
                             max_drawdown=round(mdd_0050 * 100, 2),
                             n_holdings=1)

# 逐年報酬(四因子 vs 0050)— 用日報酬序列彙整
def yearly_returns_from_equity(equity):
    eq = equity.copy(); eq.index = pd.to_datetime(eq.index)
    yearly = eq.resample('Y').last()
    prev = eq.resample('Y').first()
    # 用每年最後值 / 前一年最後值
    ann = yearly.pct_change()
    first_year = yearly.index[0].year
    ann.iloc[0] = yearly.iloc[0] / eq.iloc[0] - 1
    return {str(idx.year): round(float(v) * 100, 2) for idx, v in ann.items() if pd.notna(v)}

eqB = rptB.creturn                      # 四因子權益曲線(起點 1.0)
results['four_factor_yearly'] = yearly_returns_from_equity(eqB)
results['bench_0050_yearly'] = yearly_returns_from_equity(e)

print(json.dumps(results, ensure_ascii=False, indent=2))
print("YEARS", round(yrs, 2), "DATA_END", str(close.index.max().date()))

# 存出純數字供圖表腳本/文章引用
with open('/tmp/wiqt-charts/results.json', 'w', encoding='utf-8') as f:
    json.dump({'results': results,
               'years': round(yrs, 2),
               'data_end': str(close.index.max().date()),
               'start': START}, f, ensure_ascii=False, indent=2)

# 同時把三組權益曲線存出(供 plotly)
eqA = rptA.creturn
eqC = rptC.creturn
eq_bench = e / e.iloc[0]
pd.DataFrame({
    'four_factor': eqB,
    'rule_revenue': eqA,
    'naive_lowpe': eqC,
}).to_csv('/tmp/wiqt-charts/equity.csv')
eq_bench.to_csv('/tmp/wiqt-charts/equity_0050.csv')
print("SAVED equity.csv / results.json / reports")
