本文由 StockApi 智能体基于历史行情数据与策略回测框架整理撰写,代码可直接运行复现。
双均线是最容易被"讲烂"的策略,但真正跑起来,多数人卡在第一行代码上:均线到底自己算,还是用现成的指标接口?自己算能对齐回测,但实盘每天要重算几十只票;用现成接口省事,又怕和回测口径不一致。
这篇的做法是分工:行情序列用日 K 接口(/v1/base/day)拿价格与成交量,均线值直接调 MA 指标接口(/v1/quota/ma2),一次请求就能拿到 ma5/ma10/ma20/ma60/ma200 全序列。回测和实盘共用同一个接口,口径天然一致。
/v1/base/all)按市值/概念初筛,再逐只跑信号原始双均线在熊市里会被假金叉反复打脸。A 股这几轮深度调整中,指数跌破年线之后所有金叉几乎都是噪音——趋势过滤等于把"不该持仓的时段"整体删掉。代价是错过熊市反弹的第一段,换来的是整个下跌段空仓休息,最大回撤通常能从 40% 级别压到 20% 以内。
import os
import requests
import pandas as pd
TOKEN = os.getenv("STOCKAPI_TOKEN", "你的Token")
BASE = "https://www.stockapi.com.cn/v1"
def call(path, **params):
"""StockApi 统一调用:code==20000 为成功"""
params["token"] = TOKEN
r = requests.get(f"{BASE}{path}", params=params, timeout=15)
r.raise_for_status()
d = r.json()
if d.get("code") == 20000:
return d["data"]
raise RuntimeError(f"{path} 调用失败: {d.get('msg')}")
def to_df(data):
"""兼容三种返回结构:list[dict] / 列式字典 / 分页 records"""
if isinstance(data, list):
df = pd.DataFrame(data)
elif isinstance(data, dict):
if "records" in data:
df = pd.DataFrame(data["records"])
else: # K线接口是 {字段: [值,...]} 的列式返回
df = pd.DataFrame({k: v for k, v in data.items() if isinstance(v, list)})
else:
raise TypeError("未知返回结构")
if "date" in df.columns:
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date").reset_index(drop=True)
return df
def get_kline(code, start, end, cycle=100):
"""前复权日K(100=日 101=周 102=月),列式返回,含 PE/PB/市值/换手"""
return to_df(call("/base/day", code=code, startDate=start,
endDate=end, calculationCycle=cycle))
def get_ma(code, start, end, periods="20,60,200"):
"""MA 指标接口:一次拿多条均线"""
return to_df(call("/quota/ma2", code=code, ma=periods,
startDate=start, endDate=end, calculationCycle=100))
def align(px, ind):
"""K线无 date 字段时按交易日顺序尾部对齐;有 date 则按日期合并"""
if "date" in px.columns and "date" in ind.columns:
return pd.merge(px, ind, on="date", how="inner").sort_values("date").reset_index(drop=True)
n = min(len(px), len(ind))
left = px.tail(n).reset_index(drop=True)
right = ind.tail(n).drop(columns=[c for c in ("date", "api_code") if c in ind.columns]).reset_index(drop=True)
return pd.concat([left, right], axis=1)
def backtest_dual_ma(code, start, end):
px = get_kline(code, start, end)
ma = get_ma(code, start, end)
df = align(px, ma)
for c in ["ma20", "ma60", "ma200"]:
df[c] = pd.to_numeric(df[c], errors="coerce")
df["close"] = pd.to_numeric(df["close"], errors="coerce")
df = df.dropna(subset=["ma200"]).reset_index(drop=True)
pos, hold = [], False
for i in range(1, len(df)):
r, p = df.iloc[i], df.iloc[i - 1]
if not hold and p["ma20"] <= p["ma60"] and r["ma20"] > r["ma60"] and r["close"] > r["ma200"]:
hold = True
elif hold and (r["ma20"] < r["ma60"] or r["close"] < r["ma200"]):
hold = False
pos.append(1 if hold else 0)
df["pos"] = [0] + pos
df["ret"] = df["close"].pct_change().fillna(0)
df["strategy_ret"] = df["pos"].shift(1).fillna(0) * df["ret"]
df["equity"] = (1 + df["strategy_ret"]).cumprod()
ann = df["strategy_ret"].mean() * 243
dd = (df["equity"] / df["equity"].cummax() - 1).min()
print(f"{code} 年化 {ann:.1%} 最大回撤 {dd:.1%} 持仓天数占比 {df['pos'].mean():.1%}")
return df
# 单只回测
backtest_dual_ma("600519", "2018-01-01", "2026-09-30")
# 批量:先拿全市场列表,再按市值初筛后逐只跑(注意 40 次/秒的频率上限)
stocks = call("/base/all")
print("全市场股票数:", len(stocks), "| 示例字段:", stocks[0])指标 | 原始双均线 | 加 ma200 过滤 |
|---|---|---|
年化收益 | 5%—8% | 8%—12% |
最大回撤 | 35%—45% | 15%—25% |
持仓时间占比 | 100% | 45%—60% |
(示意量级,请以自己回测输出为准。)核心看最后一列:过滤版有一半时间空仓,回撤下降主要来自这里。
ma 参数支持英文逗号拼多条:"5,10,20,60,120,250" 一次拿全,别拆成多次请求Q:为什么不用 K 线自己算均线?
可以用,但实盘每天几十只标的要重算序列,接口直出更省事,而且回测与实盘口径统一。
Q:K 线接口为什么没有 date 字段?
该接口是列式返回(每个字段一个数组),数据按交易日排列;需要精确日期时用指标接口的 date 列合并,代码里的 align() 已处理两种情形。
Q:能用于周线吗?
可以,calculationCycle=101,均线接口同样支持。
文中数据获取基于 StockApi 提供的行情与指标接口。以上内容仅为策略逻辑的客观梳理与代码演示,不构成任何投资建议。市场有风险,交易需谨慎。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。