【Python量化趋势跟踪实战 01】均线多头排列就是趋势开始吗:过滤与确认

0 阅读5分钟

【Python量化趋势跟踪实战 01】均线多头排列就是趋势开始吗:过滤与确认

金叉买、死叉卖——这六个字几乎人人听过,但实盘里照做的人多半亏得怀疑人生。为什么?因为震荡市里均线会反复缠绕,今天金叉明天死叉,来回打脸。

这篇从一个最简单的改进开始:在金叉后面再加一道过滤条件,看看能不能把假信号压下去。


本文你将得到什么

  1. 一个最基础的均线交叉策略(SMA20/60)及其回测框架。
  2. 一道「趋势确认」过滤逻辑:不仅要看短期均线上穿长期均线,还要看短期均线本身是否在往上走。
  3. 两个策略的诚实对比:过滤后的信号未必更多,但质量可能更高。
  4. 可直接复制的 Python 代码,次日开盘执行、无前视偏差。

一、裸金叉的问题:震荡市反复打脸

均线交叉的本质是「滞后确认」。当价格已经从低点涨了一段,短期均线才追上长期均线,发出金叉。这个滞后本身不是bug——趋势跟踪本来就要牺牲前段涨幅来换取确定性——但麻烦在于,如果价格只是在区间里上下波动,两条均线也会跟着缠绕,产生大量没有趋势支撑的信号。

下面的代码先把「裸金叉」跑一遍,看看它长什么样。


可运行代码

pip install mairui pandas numpy
from mairui import Client
import pandas as pd
import numpy as np
import random

LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"  # 演示证书,请换成你自己的正式证书
client = Client(LICENCE)

SEED = 20261015


def synth_ohlc(bars, seed):
    # 演示证书返回固定样例数据;用确定性合成路径模拟行情起伏(慢变波动率+波动聚集)。
    # 换成你自己的正式证书后删掉本函数调用即可,其余逻辑零改动。
    rng = random.Random(seed)
    g = 0.0
    out, prev_c = [], None
    base_date = pd.to_datetime(bars[0]["t"])
    for i, b in enumerate(bars):
        g = 0.9 * g + rng.gauss(0, 0.25)
        sigma = 0.015 * float(np.exp(g))
        if prev_c is None:
            c = float(b["c"])
            o = c * (1 + rng.gauss(0, 0.3 * sigma))
        else:
            c = prev_c * (1 + rng.gauss(0, sigma))
            o = prev_c * (1 + rng.gauss(0, 0.3 * sigma))
        h = max(o, c) * (1 + abs(rng.gauss(0, 0.5 * sigma)))
        l = min(o, c) * (1 - abs(rng.gauss(0, 0.5 * sigma)))
        date_str = (base_date + pd.Timedelta(days=i)).strftime("%Y-%m-%d")
        out.append({"t": date_str, "o": round(o, 2), "h": round(h, 2), "l": round(l, 2), "c": round(c, 2)})
        prev_c = c
    return out


def fetch_and_extend(code, target_bars=200):
    raw = client.stock_history(code, period="d", dividend="f")
    extended = synth_ohlc(raw * ((target_bars // len(raw)) + 1), SEED + int(code))
    return extended[:target_bars]


def backtest(df, signal_col, label):
    df = df.copy()
    df["next_o"] = df["o"].shift(-1)
    df["position"] = df[signal_col].shift(1).fillna(0)
    df["return"] = df["position"] * (df["next_o"] - df["o"]) / df["o"]
    df["cum"] = (1 + df["return"].fillna(0)).cumprod() - 1
    nav = (1 + df["return"].fillna(0)).cumprod()
    dd = (nav / nav.cummax() - 1)
    trades = df["position"].diff().abs().sum()
    return {
        "label": label,
        "total_return": f"{df['cum'].iloc[-2]*100:.2f}%",
        "max_dd": f"{dd.min()*100:.2f}%",
        "trades": int(trades),
        "win_rate": f"{(df['return'] > 0).sum() / (df['return'] != 0).sum() * 100:.1f}%" if (df['return'] != 0).sum() > 0 else "N/A",
    }


# 拉数
data = fetch_and_extend("600519", target_bars=200)
df = pd.DataFrame(data)
df["t"] = pd.to_datetime(df["t"])
df = df.sort_values("t").reset_index(drop=True)

# 计算均线
df["sma20"] = df["c"].rolling(20).mean()
df["sma60"] = df["c"].rolling(60).mean()

# 策略 A:裸金叉(SMA20 > SMA60 即持仓)
df["sig_raw"] = (df["sma20"] > df["sma60"]).astype(int)

# 策略 B:趋势确认过滤(SMA20 > SMA60 且 SMA20 本身在上升)
df["sma20_rising"] = (df["sma20"] > df["sma20"].shift(3)).astype(int)
df["sig_align"] = ((df["sma20"] > df["sma60"]) & (df["sma20_rising"] == 1)).astype(int)

# 切掉热身期
df_eval = df.iloc[60:].copy().reset_index(drop=True)

rA = backtest(df_eval, "sig_raw", "裸金叉")
rB = backtest(df_eval, "sig_align", "趋势确认")

print("策略 A — 裸金叉")
for k, v in rA.items():
    if k != "label":
        print(f"  {k}: {v}")

print("\n策略 B — 趋势确认")
for k, v in rB.items():
    if k != "label":
        print(f"  {k}: {v}")

运行结果

策略 A — 裸金叉
  total_return: 50.88%
  max_dd: -11.04%
  trades: 3
  win_rate: 53.5%

策略 B — 趋势确认
  total_return: 55.14%
  max_dd: -7.27%
  trades: 5
  win_rate: 56.2%

二、结果解读:过滤到底起了什么作用

这个样本里,策略 B 的收益略高于策略 A(55.14% vs 50.88%),但更关键的是回撤从 -11.04% 收窄到 -7.27%。这意味着过滤条件在趋势不明确的时候把仓位压了下来,避开了部分无效波动。

注意两点:

  1. 交易次数没减少:策略 B 反而多了 2 笔交易。因为「SMA20 正在上升」这个条件比单纯的「SMA20 > SMA60」更敏感——它可以在价格刚启动、还未大幅偏离均线时就确认趋势。代价是可能多踩几次假启动,但样本期内总体仍更优。
  2. 不保证每次都赢:这只是一个 200 日的合成样本。换一段行情,过滤条件也可能让你错过一波急涨。过滤的本质是用牺牲部分机会来换取更低的回撤。

三、常见坑

  • 前视偏差:信号必须基于当日收盘数据计算,次日开盘才能执行。代码里用 signal.shift(1) 保证这一点,不要在收盘前偷跑。
  • 均线周期迷信:20/60 只是示例。周期越短信号越多、噪音越大;周期越长信号越少、滞后越严重。没有 universally optimal 的参数。
  • 演示证书数据有限:本例用合成路径扩展了数据量,正式证书可拉全量真实历史做更充分的回测。

小结与下篇预告

今天我们验证了:单纯的金叉不够,加一道「短期均线本身在上升」的确认过滤,可以在样本期内降低回撤、提升风险收益比。

但光会判断趋势还不够——入场之后,止损放哪? 固定百分比止损在趋势策略里往往太早被震出。下篇我们引入 ATR 动态止损,让盈利仓位「扛得住正常回调」。


免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测结果不代表未来表现,入市须谨慎。


代码与文档:更多 SDK 与接口文档见 github.com/MaiRuiApi