跳至主要內容

回測期間 2017-01-03 〜 2026-11-10 · 資料截至 2026-11-10

美股頭肩底排除選股

避開正處於頭肩底持有狀態的股票,再從其他高流動性美股選最多十檔。歷史最大跌幅約50%,高於QQQ。登入並使用可用的AI研究額度重跑,可查看績效報告與持股分析,不會下單。

投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險。

回測績效

年化報酬率
28.0%
夏普比率(日資料)
0.95
最大回撤
-50.0%
總報酬率
1042%

回測不計手續費與交易稅(finlab 套件 sim() 的美股預設成本),夏普比率用日報酬計算、未扣無風險利率。策略淨值自資料可得日(2017-01-03)起算。

淨值曲線(對數座標)

美股頭肩底排除選股 vs QQQ含息買進持有
1x2x3x5x8x12x2017-01-032018-08-272020-04-222021-12-132023-08-092025-04-042026-11-10
美股頭肩底排除選股QQQ含息買進持有

與 QQQ含息買進持有對照

對象年化報酬率夏普比率(日資料)最大回撤回測期間
美股頭肩底排除選股28.0%0.95-50.0%2017-01-03 〜 2026-11-10
QQQ含息買進持有20.1%0.92-35.1%2017-01-03 〜 2026-11-10

完整策略程式碼

安裝 finlab 套件後即可直接執行重現(finlab.login() 會自動引導登入,不需手動填 token)。想用 AI 輔助安裝與跑第一次回測,可以走安裝教學。

展開程式碼(strategy.py)
"""US liquid stocks excluding active inverse-head-and-shoulders positions.

Historical validation: 2017-01-03 to 2026-11-10. Maximum historical drawdown
was about 50%. No leverage. Install FinLab using https://finlab.finance/en/setup.
DATA_END=None keeps the daily strategy current; set it to '2026-11-10' to
reproduce the article. The central rule is bandwidth=3, tolerance=0.03.
"""
import numpy as np
import pandas as pd
from scipy.signal import lfilter
from finlab import data, FinlabDataFrame
from finlab.backtest import sim

START = "2017-01-03"
DATA_END = None


def build_position(end=DATA_END, bandwidth=3, tolerance=.03):
    data.set_market("us")
    close, raw, volume = [pd.DataFrame(data.get(k)).loc[:end] for k in
                          ("us_price:adj_close", "us_price:close", "us_price:volume")]
    raw, volume = [x.reindex(index=close.index, columns=close.columns) for x in (raw, volume)]
    liquidity = (raw * volume).rolling(60, min_periods=60).mean()
    clean = close.pct_change(fill_method=None).abs().ge(.5).rolling(252, min_periods=1).max().shift(1).fillna(0).eq(0)
    eligible = (raw.ge(10) & liquidity.ge(10_000_000)
                & close.notna().rolling(252).sum().ge(252) & clean
                & liquidity.rank(axis=1, ascending=False, method="first").le(500))
    columns = close.columns[eligible.any(axis=0)]
    close, liquidity, eligible = [x[columns] for x in (close, liquidity, eligible)]
    values = close.ffill().to_numpy(dtype=float)
    valid = np.isfinite(values)
    weights = np.exp(-.5 * (np.arange(24) / bandwidth) ** 2)
    weights /= weights.sum()
    numerator = lfilter(weights, [1], np.nan_to_num(values), axis=0)
    denominator = lfilter(weights, [1], valid.astype(float), axis=0)
    smoothed = np.where(denominator > .999, numerator / np.maximum(denominator, 1e-12), np.nan)
    n, m = values.shape
    prices = close.to_numpy(dtype=float)
    points = np.zeros((5, m))
    dates = np.zeros((5, m), dtype=int)
    kinds = np.zeros((5, m), dtype=int)
    count = np.zeros(m, dtype=int)
    used = np.full(m, -1)
    born = np.full(m, -1)
    active = np.zeros(m, dtype=bool)
    held = np.zeros((n, m), dtype=bool)
    for t in range(30, n):
        p = t - 3
        v = smoothed[p]
        peak, trough = np.isfinite(v), np.isfinite(v)
        for k in range(p - 3, p + 4):
            if k != p:
                peak &= v > smoothed[k]
                trough &= v < smoothed[k]
        kind = peak.astype(int) - trough.astype(int)
        turning = kind != 0
        same = turning & (count > 0) & (kinds[4] == kind)
        replace = same & (kind * v > kind * points[4])
        append = turning & ~same
        for arr in (points, dates, kinds):
            arr[:4, append] = arr[1:5, append].copy()
        update = append | replace
        points[4, update], dates[4, update], kinds[4, update] = v[update], p, kind[update]
        count[append] = np.minimum(count[append] + 1, 5)
        direction = -kinds[0]
        span = dates[4] - dates[0]
        shape = ((count >= 5) & (span >= 22) & (span <= 120) & (t - dates[4] <= 20)
                 & (np.abs(points[0] - points[4]) <= tolerance * (points[0] + points[4]) / 2)
                 & (direction * (points[2] - points[0]) < -.03 * points[0])
                 & (direction * (points[2] - points[4]) < -.03 * points[4])
                 & (np.abs(points[1] - points[3]) <= tolerance * (points[1] + points[3]) / 2))
        distance = dates[3] - dates[1]
        slope = np.divide(points[3] - points[1], distance, out=np.zeros(m), where=distance != 0)
        neck = points[3] + slope * (t - dates[3])
        crossed = ((direction * (prices[t] - neck) > 0)
                   & (direction * (prices[t-1] - (neck-slope)) <= 0))
        buy = shape & crossed & (direction == 1) & (used != dates[4])
        sell = shape & crossed & (direction == -1)
        exits = active & (sell | (t - born >= 60))
        entries = ~active & buy
        active[exits] = False
        active[entries], born[entries], used[entries] = True, t, dates[4, entries]
        held[t] = active
    candidates = ~pd.DataFrame(held, index=close.index, columns=columns) & eligible
    selected = liquidity.where(candidates).rank(axis=1, ascending=False, method="first").le(10)
    return FinlabDataFrame(selected.loc[START:end])


if __name__ == "__main__":
    position = build_position()
    report = sim(position, position_limit=.1, upload=False, end_date=DATA_END)
    report.to_html("report.html")
    print(report.get_stats())

下載 strategy.py 下載 data.csv(淨值曲線)

到 FinLab Studio 重現這支策略

在 Studio 裡用同一套真實資料調整條件、重跑回測。

本頁策略與回測結果僅供教學與研究用途,詳見服務條款。