0基础进阶Python股票量化:信号过滤+多股回测(实战第三篇)

内容分享4小时前发布
0 4 0

0基础进阶Python股票量化:信号过滤+多股回测(实战第三篇)

上一篇我们搭好了均线交叉基础策略,不少新手跑代码后留言:“横盘时频繁出买卖信号,手续费都亏没了!”“怎么知道这个策略适合哪只股票?”今天就兑现预告,带来两个核心升级——用“成交量”过滤假信号,再用Tushare实现“沪深300多股批量回测”,让你的策略从“单股试用”升级为“多股筛选”,实用性直接翻倍。

一、核心痛点:为啥基础策略会出“假信号”?

基础均线策略的致命问题是“认不出横盘”:股价在小范围震荡时,5日和20日均线会反复交叉,产生一堆“假金叉/死叉”。列如2024年某段时间的贵州茅台,横盘1个月出了3次金叉,每次买入后都小跌,频繁交易反而亏手续费。

解决办法很简单:用成交量验证趋势。真金叉是“资金进场推涨”,必然伴随成交量放大;假金叉是“股价随机波动”,成交量会很清淡。这就像“真涨价靠抢购,假涨价靠吆喝”,成交量就是“抢购痕迹”。

二、升级第一步:用成交量过滤假信号(茅台实战)

核心逻辑:金叉时,当天成交量必须大于“前5日成交量均值的1.3倍”(确认资金在进场);死叉信号不变(落袋为安不用等成交量)。我们基于上一篇的茅台数据直接升级代码。

2.1 完整升级代码(复用+新增过滤逻辑)

# 1. 导入库+初始化接口(和上一篇完全一致,直接复制)
import tushare as ts
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

ts.set_token('你的Token')  # 替换成自己的Token
pro = ts.pro_api()

# 2. 复用茅台2024年数据(和上一篇一致)
maotai_df = pro.daily(
    ts_code='600519.SH',
    start_date='20240101',
    end_date='20241231',
    adj='qfq'
)
maotai_df = maotai_df.sort_values('trade_date')
maotai_df['trade_date'] = pd.to_datetime(maotai_df['trade_date'])
maotai_df.set_index('trade_date', inplace=True)
maotai_df = maotai_df[['open', 'high', 'low', 'close', 'vol']]
maotai_df['ma5'] = maotai_df['close'].rolling(window=5).mean()
maotai_df['ma20'] = maotai_df['close'].rolling(window=20).mean()

# 3. 新增:计算成交量指标(核心过滤条件)
# 前5日成交量均值(排除当日,避免当日数据干扰)
maotai_df['vol_5avg'] = maotai_df['vol'].rolling(window=5).mean().shift(1)
# 成交量放大判断:当日成交量 > 前5日均值*1.3
maotai_df['vol_enlarge'] = np.where(maotai_df['vol'] > maotai_df['vol_5avg']*1.3, 1, 0)

# 4. 升级金叉信号:原金叉条件 + 成交量放大
maotai_df['ma5_prev'] = maotai_df['ma5'].shift(1)
maotai_df['ma20_prev'] = maotai_df['ma20'].shift(1)
# 新增vol_enlarge==1,过滤掉成交量没放大的假金叉
maotai_df['golden_cross'] = np.where(
    (maotai_df['ma5_prev'] < maotai_df['ma20_prev']) & 
    (maotai_df['ma5'] > maotai_df['ma20']) & 
    (maotai_df['vol_enlarge'] == 1),  # 关键过滤条件
    1, 0
)
# 死叉信号不变(和上一篇一致)
maotai_df['death_cross'] = np.where(
    (maotai_df['ma5_prev'] > maotai_df['ma20_prev']) & 
    (maotai_df['ma5'] < maotai_df['ma20']), 
    1, 0
)

# 5. 持仓状态+止损(完全复用上一篇代码,不用改)
maotai_df['position'] = 0
for i in range(len(maotai_df)):
    current = maotai_df.iloc[i]
    prev = maotai_df.iloc[i-1] if i>0 else current
    if prev['position'] == 0 and current['golden_cross'] == 1:
        maotai_df.iloc[i, maotai_df.columns.get_loc('position')] = 1
    elif prev['position'] == 1 and current['death_cross'] == 1:
        maotai_df.iloc[i, maotai_df.columns.get_loc('position')] = 0
    else:
        maotai_df.iloc[i, maotai_df.columns.get_loc('position')] = prev['position']

# 止损逻辑(和上一篇一致)
maotai_df['trade_signal'] = maotai_df['position'].diff()
maotai_df['buy_price'] = 0
buy_price = 0
for i in range(len(maotai_df)):
    current = maotai_df.iloc[i]
    if current['trade_signal'] == 1:
        buy_price = current['close']
        maotai_df.iloc[i, maotai_df.columns.get_loc('buy_price')] = buy_price
    elif current['position'] == 1:
        maotai_df.iloc[i, maotai_df.columns.get_loc('buy_price')] = buy_price

maotai_df['stop_loss'] = np.where(
    (maotai_df['position'] == 1) & 
    ((maotai_df['close'] - maotai_df['buy_price']) / maotai_df['buy_price'] < -0.05),
    1, 0
)

for i in range(len(maotai_df)):
    current = maotai_df.iloc[i]
    prev = maotai_df.iloc[i-1] if i>0 else current
    if prev['position'] == 1 and current['stop_loss'] == 1:
        maotai_df.iloc[i, maotai_df.columns.get_loc('position')] = 0
        maotai_df.iloc[i, maotai_df.columns.get_loc('trade_signal')] = -1

# 查看过滤后的交易信号
filtered_trades = maotai_df[maotai_df['trade_signal'] != 0]
print("成交量过滤后的茅台交易信号(2024):")
print(filtered_trades[['close', 'trade_signal', 'vol', 'vol_5avg']])

2.2 升级效果:假信号少了,收益稳了

对比上一篇的基础策略,你会发现两个明显变化:

  • 交易次数减少:基础策略可能一年交易10次,过滤后只剩5-6次,手续费直接省一半;
  • 胜率提升:去掉了横盘时的假金叉,每次买入都有资金支撑,亏损交易比例明显下降。

可以用这个代码计算升级前后的收益率对比,会发现过滤后的策略“赚得多、亏得少”。

三、升级第二步:多股批量回测(沪深300实战)

一个策略不可能适合所有股票,列如均线策略对白酒股有效,对周期股可能失灵。我们用Tushare获取沪深300成分股,批量回测找出策略“最爱的股票”。

3.1 核心步骤:获取成分股→循环回测→筛选结果

# 1. 用Tushare获取沪深300成分股列表(关键接口)
# 接口说明:index_code=000300.SH代表沪深300指数
hs300_stocks = pro.index_weight(index_code='000300.SH', trade_date='20241231')
# 只留股票代码和名称,后续用股票代码批量取数据
hs300_list = hs300_stocks[['con_code', 'con_name']].rename(columns={'con_code':'ts_code'})
print("沪深300成分股前10只:")
print(hs300_list.head(10))

# 2. 定义回测函数(把之前的策略打包成函数,方便循环调用)
def ma_strategy(ts_code, start_date, end_date):
    # 获取单只股票数据
    df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date, adj='qfq')
    if len(df) < 60:  # 过滤上市时间短的股票(至少60个交易日)
        return pd.Series([ts_code, '数据不足', 0, 0])
    
    # 策略逻辑(和前面的过滤策略一致)
    df = df.sort_values('trade_date')
    df['trade_date'] = pd.to_datetime(df['trade_date'])
    df.set_index('trade_date', inplace=True)
    df['ma5'] = df['close'].rolling(window=5).mean()
    df['ma20'] = df['close'].rolling(window=20).mean()
    df['vol_5avg'] = df['vol'].rolling(window=5).mean().shift(1)
    df['vol_enlarge'] = np.where(df['vol'] > df['vol_5avg']*1.3, 1, 0)
    
    df['ma5_prev'] = df['ma5'].shift(1)
    df['ma20_prev'] = df['ma20'].shift(1)
    df['golden_cross'] = np.where(
        (df['ma5_prev'] < df['ma20_prev']) & 
        (df['ma5'] > df['ma20']) & 
        (df['vol_enlarge'] == 1),
        1, 0
    )
    df['death_cross'] = np.where(
        (df['ma5_prev'] > df['ma20_prev']) & 
        (df['ma5'] < df['ma20']),
        1, 0
    )
    
    # 持仓和止损
    df['position'] = 0
    for i in range(len(df)):
        current = df.iloc[i]
        prev = df.iloc[i-1] if i>0 else current
        if prev['position'] == 0 and current['golden_cross'] == 1:
            df.iloc[i, df.columns.get_loc('position')] = 1
        elif prev['position'] == 1 and current['death_cross'] == 1:
            df.iloc[i, df.columns.get_loc('position')] = 0
        else:
            df.iloc[i, df.columns.get_loc('position')] = prev['position']
    
    df['trade_signal'] = df['position'].diff()
    df['buy_price'] = 0
    buy_price = 0
    for i in range(len(df)):
        current = df.iloc[i]
        if current['trade_signal'] == 1:
            buy_price = current['close']
            df.iloc[i, df.columns.get_loc('buy_price')] = buy_price
        elif current['position'] == 1:
            df.iloc[i, df.columns.get_loc('buy_price')] = buy_price
    
    df['stop_loss'] = np.where(
        (df['position'] == 1) & 
        ((df['close'] - df['buy_price']) / df['buy_price'] < -0.05),
        1, 0
    )
    
    for i in range(len(df)):
        current = df.iloc[i]
        prev = df.iloc[i-1] if i>0 else current
        if prev['position'] == 1 and current['stop_loss'] == 1:
            df.iloc[i, df.columns.get_loc('position')] = 0
            df.iloc[i, df.columns.get_loc('trade_signal')] = -1
    
    # 计算回测指标(总收益率、交易次数)
    df['daily_return'] = df['close'].pct_change() * df['position'].shift(1)
    total_return = (1 + df['daily_return']).cumprod().iloc[-1] - 1
    trade_count = len(df[df['trade_signal'] != 0])
    
    return pd.Series([ts_code, '', total_return*100, trade_count])

# 3. 批量回测沪深300成分股(这里先测前20只,全测换range(len(hs300_list)))
results = []
for i in range(20):  # 新手先跑20只,全测可能需要1-2分钟
    ts_code = hs300_list.iloc[i]['ts_code']
    stock_name = hs300_list.iloc[i]['con_name']
    # 调用回测函数,获取结果
    res = ma_strategy(ts_code, '20240101', '20241231')
    res[1] = stock_name  # 填充股票名称
    results.append(res)

# 整理结果为表格,按收益率排序
result_df = pd.DataFrame(results, columns=['股票代码', '股票名称', '2024年收益率(%)', '交易次数'])
result_df = result_df.sort_values('2024年收益率(%)', ascending=False)
print("
沪深300前20只股票策略回测结果(按收益率排序):")
print(result_df.round(2))  # 保留2位小数,更清晰

3.2 关键说明:新手必看的批量回测技巧

  • 成分股更新:沪深300成分股每半年调整一次,用“trade_date='20241231'”获取最新的2024年底成分股;
  • 数据过滤:用“len(df)<60”过滤掉2024年上市的新股,避免数据不足导致回测失真;
  • 效率优化:全量300只股票回测约1分钟,耐心等待;如果报错,检查Tushare Token权限(免费用户足够)。

四、结果分析:怎么筛选“策略优选股”?

回测结果出来后,别只看收益率,重点关注两个指标:

  1. 收益率>15%+交易次数<10次:这类股票“赚得多、动得少”,是策略的核心目标,列如回测中某白酒股收益率28%,全年只交易4次;
  2. 收益率为负但交易次数多:列如某周期股收益率-5%,交易12次,说明策略不适合这类波动大的股票,直接排除。

可以用下面的代码筛选出优选股,直接用于后续跟踪:

# 筛选条件:收益率>15% 且 交易次数<10次
good_stocks = result_df[(result_df['2024年收益率(%)'] > 15) & (result_df['交易次数'] < 10)]
print("策略优选股(2024年):")
print(good_stocks)

五、本期避坑指南:3个批量回测误区

  1. 误区1:全信回测收益率——回测是“历史数据推演”,过去涨不代表未来涨,优选股只是“候选池”,还要结合行业景气度;
  2. 误区2:参数一刀切——别用5/20日均线测所有股票,列如科技股波动大,可把均线改成10/30日,在函数里调整window参数即可;
  3. 误区3:忽略复权——批量回测必须加adj='qfq'(前复权),否则某只股票分红除权后,收益率会算错成负数。

六、下期预告:策略自动化+实时提醒

这篇我们实现了策略升级和多股筛选,下期将进入“实用化”阶段:教你用Python写一个“实时信号提醒工具”——每天收盘后自动跑策略,一旦有优选股出现金叉信号,就通过微信或邮件通知你,不用再手动盯盘跑代码。

目前你可以试着修改代码里的“回测范围”,列如把2024年改成2023年,看看策略在不同年份的表现。如果批量回测时遇到报错,或者想优化筛选条件,直接在评论区留言,我来帮你解决。

© 版权声明

相关文章

4 条评论

  • 头像
    金珍 读者

    老师,为何有时Tushare,有时Akshare,跳来跳去很难跟从。

    无记录
    回复
  • 头像
    小白熊的小白鞋 读者

    因为有些数据要钱

    无记录
    回复
  • 头像
    悠後 读者

    谢谢,很有用,我周末试一下。坚持。

    无记录
    回复
  • 头像
    奇怪 读者

    收藏了,感谢分享

    无记录
    回复