"""台股第四季走勢：35 年 Q4 路徑、六種型態與停損線（FinLab 量化研究，2026-10）

重現文章 /blog/taiwan-stock-q4-seasonality 的核心數字：
  1. 1991–2025 每年第四季報酬、季內最深跌幅、六種走勢分類
  2. 平常年份的中位數路徑（10/31、11/30、12/31）與 10 月回檔
  3. 第四季途中跌破 3%／5%／7%／10%／15% 的年份裡，有幾年是大虧年
  4. 從 9 月底起算的停損線（-5%／-7%／-10%）對平均報酬與最差一年的影響（1991–2025）

資料：同目錄 data.csv（台股加權指數日收盤，1990-01-04 至 2026-09-30；
      1999 年以前為證交所與 Yahoo Finance 拼接，1999 年起取自 finlab taiex_total_index）

執行：pip install pandas numpy，然後 python strategy.py
"""
from pathlib import Path

import numpy as np
import pandas as pd

HERE = Path(__file__).parent
FIRST_YEAR, LAST_YEAR = 1991, 2025
STOP_FIRST_YEAR = 1991
UP, DOWN = 0.05, -0.05        # 季末 ±5% 分上漲／平盤／大虧
DIP = -0.05                   # 季內最深跌幅 5% 分「順」與「洗盤」
BREAK = -0.10                 # 開季 20 天內跌破 10% = 開季即崩
EARLY_DAYS = 20
POST_DAYS = 95                # 日曆日，讓 9/27 起算也能涵蓋到 12/31
THRESHOLDS = (-0.03, -0.05, -0.07, -0.10, -0.15)
STOPS = (0.05, 0.07, 0.10)


def load_taiex() -> pd.Series:
    return pd.read_csv(HERE / "data.csv", index_col="date", parse_dates=True)["taiex"]


def last_close_on_or_before(px: pd.Series, when: pd.Timestamp) -> pd.Timestamp:
    return px.index[px.index <= when][-1]


# ---------- 1. 每年第四季的日曆日路徑 ----------
def q4_path(px: pd.Series, year: int) -> pd.Series:
    """以 9 月底最後一個交易日收盤為 0%，逐日曆日記錄到 12/31（假日沿用前一日收盤）。"""
    d0 = last_close_on_or_before(px, pd.Timestamp(year, 9, 30))
    end = pd.Timestamp(year, 12, 31)
    grid = pd.date_range(d0, d0 + pd.Timedelta(days=POST_DAYS))
    path = px.reindex(px.index.union(grid)).ffill().reindex(grid) / px[d0] - 1
    path[grid > end] = np.nan
    path.index = (grid - d0).days
    return path


def classify(q4: pd.Series) -> str:
    ret, worst = q4.dropna().iloc[-1], q4.min()
    if ret <= DOWN:
        return "開季即崩" if (q4[q4.index <= EARLY_DAYS] <= BREAK).any() else "先漲後殺"
    if ret >= UP:
        return "穩步上漲" if worst > DIP else "回檔後上漲"
    return "平盤整理" if worst > DIP else "洗盤不漲"


def build_table(px: pd.Series) -> tuple[pd.DataFrame, pd.DataFrame]:
    paths = pd.DataFrame({y: q4_path(px, y) for y in range(FIRST_YEAR, LAST_YEAR + 1)})
    rows = []
    for y in paths:
        q4 = paths[y][paths.index > 0].dropna()
        rows.append({"year": y, "type": classify(q4), "q4_return": q4.iloc[-1], "q4_worst": q4.min(),
                     "oct_worst": q4[q4.index <= 31].min()})
    return paths, pd.DataFrame(rows).set_index("year")


# ---------- 2. 停損線 ----------
def stop_returns(px: pd.Series, year: int) -> dict:
    d0 = last_close_on_or_before(px, pd.Timestamp(year, 9, 30))
    d1 = last_close_on_or_before(px, pd.Timestamp(year, 12, 31))
    path = px[(px.index > d0) & (px.index <= d1)] / px[d0] - 1
    out = {"抱滿": path.iloc[-1]}
    for x in STOPS:
        hit = path[path <= -x]
        out[f"-{int(x * 100)}% 出場"] = path.iloc[-1] if hit.empty else hit.iloc[0]
    return out


if __name__ == "__main__":
    pd.set_option("display.width", 200, "display.float_format", "{:.3f}".format)
    px = load_taiex()
    paths, table = build_table(px)
    crash = table["type"].isin(["開季即崩", "先漲後殺"])
    normal = table.index[~crash]

    print("== 每年第四季 ==")
    print(table.to_string())
    print(f"\n收紅 {int((table.q4_return > 0).sum())}/{len(table)} 年，平均 {table.q4_return.mean():+.2%}，"
          f"中位數 {table.q4_return.median():+.2%}")
    print(table.groupby("type").agg(年數=("q4_return", "size"), 平均季報酬=("q4_return", "mean"),
                                    平均最深跌幅=("q4_worst", "mean")))

    med = paths.loc[paths.index > 0, normal].median(axis=1)
    print(f"\n平常年份 {len(normal)} 年中位數路徑：10/31 {med[31]:+.1%}、11/30 {med[61]:+.1%}、12/31 {med[92]:+.1%}")
    dip = table.loc[normal][table.loc[normal, "oct_worst"] <= DIP]
    print(f"平常年份 10 月途中跌超過 5%：{len(dip)} 年 {list(dip.index)}，其中季末收紅 {int((dip.q4_return > 0).sum())} 年")

    print("\n== 第四季途中跌破多少，才是大虧年？ ==")
    for t in THRESHOLDS:
        hit = table[table.q4_worst <= t]
        print(f"跌破 {-t:.0%}：{len(hit)} 年，其中大虧年 {int(crash[hit.index].sum())} 年")

    stops = pd.DataFrame({y: stop_returns(px, y) for y in range(STOP_FIRST_YEAR, LAST_YEAR + 1)}).T
    print(f"\n== 停損線（{STOP_FIRST_YEAR}–{LAST_YEAR}，{len(stops)} 個第四季）==")
    print(pd.DataFrame({"平均": stops.mean(), "最差一年": stops.min()}).to_string())
