"""模擬交易(paper trading)要觀察什麼:策略 + 體感指標的完整可重現腳本。

這支腳本做三件事:
  1. 定義一個要放進模擬帳戶的台股策略(營收動能，分散 20 檔，月換股)
  2. 回測它，並算出「你實際跟著跑會經歷什麼」的體感指標
  3. 示範把當期持股同步到 CMoney 大富翁模擬帳戶

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

finlab 需要資料時會自動引導登入，不需要在程式裡填 token。
文章:https://finlab.finance/blog/cmoney-paper-trading-python-api
"""

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

START = "2015-01-01"
END = "2026-07-24"          # 回測釘日，與文章數字同一口徑
RISK_FREE = 0.01


# ---------------------------------------------------------------
# 1. 策略:月營收動能 + 趨勢 + 流動性，分散 20 檔
# ---------------------------------------------------------------
def build_position():
    close = data.get("price:收盤價")
    volume = data.get("price:成交股數")
    revenue = data.get("monthly_revenue:當月營收")

    # 短期價格動能:近 5 日平均日報酬
    momentum = (close / close.shift() - 1).rolling(5).mean()

    # 趨勢濾網:站上 20/60/120 日均線
    trend = (
        (close > close.average(20))
        & (close > close.average(60))
        & (close > close.average(120))
    )

    # 營收動能:3 個月平均營收高於 12 個月平均
    revenue_up = revenue.average(3) > revenue.average(12)

    # 流動性濾網:20 日平均成交量 500 張以上，避免回測買得到、實單買不到
    liquid = volume.average(20) > 500 * 1000

    return momentum[trend & revenue_up & liquid].is_largest(20).loc[START:END]


def run_backtest():
    position = build_position()
    report = sim(
        position,
        resample="M",             # 每月換股
        resample_offset="11D",    # 月營收公布後才換股
        stop_loss=0.1,
        position_limit=0.08,      # 單檔上限 8%
        upload=False,
        name="營收動能分散20檔",
    )
    return report


# ---------------------------------------------------------------
# 2. 體感指標:回測數字漂亮，過程長什麼樣
# ---------------------------------------------------------------
def underwater(curve):
    """距離前次淨值高點的百分比，0 代表正在創新高。"""
    return curve / curve.cummax() - 1


def longest_underwater_days(curve):
    """從淨值高點到重新創新高，最長花了幾個日曆天。"""
    is_underwater = underwater(curve) < -1e-9
    longest, run_start = 0, None

    for date, flag in is_underwater.items():
        if flag and run_start is None:
            run_start = date
        elif not flag and run_start is not None:
            longest = max(longest, (date - run_start).days)
            run_start = None

    if run_start is not None:
        longest = max(longest, (is_underwater.index[-1] - run_start).days)
    return longest


def holding_period_loss(curve, trading_days):
    """任選一天進場、持有 N 個交易日後，帳面是虧的機率。"""
    forward = (curve.shift(-trading_days) / curve - 1).dropna()
    return float((forward < 0).mean()), float(forward.median())


def experience_report(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

    sharpe = (daily.mean() * 252 - RISK_FREE) / (daily.std() * np.sqrt(252))
    sortino = (daily.mean() * 252 - RISK_FREE) / (daily[daily < 0].std() * np.sqrt(252))
    m_sortino = (monthly.mean() * 12 - RISK_FREE) / (monthly[monthly < 0].std() * np.sqrt(12))

    print(f"\n===== {label} =====")
    print(f"回測區間          {curve.index[0].date()} ~ {curve.index[-1].date()}")
    print(f"年化報酬率        {curve.iloc[-1] ** (1 / years) - 1:.1%}")
    print(f"日夏普 / 日索提諾 {sharpe:.2f} / {sortino:.2f}")
    print(f"月索提諾          {m_sortino:.2f}")
    print(f"最大回撤          {underwater(curve).min():.1%}")
    print(f"淨值低於前高的日子 {(underwater(curve) < -1e-9).mean():.1%}")
    print(f"最長沒創新高       {longest_underwater_days(curve)} 天")
    print(f"收黑的月份比例     {(monthly < 0).mean():.1%}")

    for name, days in [("1 個月", 21), ("3 個月", 63), ("6 個月", 126), ("1 年", 252)]:
        prob, median = holding_period_loss(curve, days)
        print(f"持有 {name:5s} 虧損機率 {prob:.1%}，報酬中位數 {median:+.1%}")


# ---------------------------------------------------------------
# 3. 把當期持股同步到 CMoney 大富翁模擬帳戶
# ---------------------------------------------------------------
def sync_to_paper_account(position, account, password, sub_account_id=None):
    """需要先 git clone https://github.com/koreal6803/CmoneyVirtualAccount"""
    from cmoney.stock import VirtualStockAccount

    virtual_account = VirtualStockAccount(account, password)
    if sub_account_id:
        virtual_account.aid = sub_account_id

    latest = position.iloc[-1]
    stock_ids = list(latest[latest > 0].index)

    # sync 會自動把資金平均分配到這些股票，並賣掉不在名單內的持股
    virtual_account.sync(stock_ids)
    return stock_ids


if __name__ == "__main__":
    report = run_backtest()

    curve = report.creturn.loc[START:END]
    benchmark = data.get("etl:adj_close")["0050"].dropna().loc[START:END]

    # 策略淨值從第一次換股日才開始，基準若從自己的第一天起算，兩者就不可比。
    # 一起切到共同起始日再各自歸一，文章裡的 0050 數字就是這樣算出來的。
    common_start = max(curve.index[0], benchmark.index[0])
    curve = curve.loc[common_start:]
    benchmark = benchmark.loc[common_start:]
    print(f"共同起始日 {common_start.date()}")

    experience_report(curve, "營收動能分散 20 檔")
    experience_report(benchmark, "0050 含息買進持有")

    # 要真的同步到模擬帳戶時，把下面兩行的註解拿掉並填入自己的帳號密碼
    # position = build_position()
    # sync_to_paper_account(position, "your_account", "your_password")
