文末有彩蛋【深入浅出的Python量化交易实战-段小手】第二章-学习笔记

温馨提示:本章学习时,数据选用的是2023年以来贵州茅台(600519.SH)的前复权行情,来自于Tushare社区。另外,文章最后有一个彩蛋送出哦!

Chapter 2 小瓦的策略靠谱吗——回测与经典策略

2.1 对小瓦的策略进行简单回测

2.1.1 下载数据并创建交易信号

1.导入相关模块包

import tushare as ts
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

2.初始化Tushare API

your_token = '********************************************************'
pro = ts.pro_api(your_token)

3.获取贵州茅台(600519.SH)2023年以来的前复权行情数据

stock_data = ts.pro_bar(ts_code='600519.SH', api=pro, start_date='20230101', adj='qfq')  # 前复权行情数据
stock_data

现在,我们拿到了没有处理的原始数据,长这个样子:
前复权行情数据-贵州茅台
接下来把“交易日期”设置为索引列,并重新排序。

stock_data.set_index('trade_date', inplace=True)  # 将日期设置为索引
stock_data.sort_index(ascending=True, inplace=True)  # 按照日期升序排列

4.创建交易信号数据表
当日股价上涨时,产生交易信号“卖出”,标记为0,反之当日股价下跌时,产生“买入”信号,标记为1。同时,根据产生的交易信号,下单,交易信号由0变为1时,买入1手(100股);由1变为0时,卖出1手(100股);不变不操作。

gzmt_signal = pd.DataFrame(index=stock_data.index)  # 创建空的交易信号数据框
gzmt_signal['price'] = stock_data['close']  # 将收盘价存入数据框
gzmt_signal['price_change'] = gzmt_signal['price'].diff()  # 计算每日收盘价变化
gzmt_signal.fillna(0, inplace=True)  # 将缺失值填充为0
gzmt_signal['Signal'] = np.where(gzmt_signal['price_change'] >= 0, 0, 1)  # 计算交易信号。当日股价上涨,卖出,标记为0,否则为1
gzmt_signal['Order'] = gzmt_signal['Signal'].diff() * 100  # 根据交易信号下单,A股买/卖最少100股,即1手
gzmt_signal

交易信号数据表-贵州茅台
由上表可知,2023年1月4日,股价下跌了5元,产生交易信号为“1”,代表“买入”。次日,股价上涨了75.99元,产生交易信号“0”,代表卖出。一买一卖这笔交易赚了大概7600元,还不错。另外,值得注意的是,这笔交易并没有考虑到滑点、佣金、印花税等交易成本,而且也不太现实,以前一天的收盘价买入,以第二天的收盘价卖出,只能用于理想的学习好了。

2.1.2 对交易策略进行简单回测(模拟总资产变化情况)

initial_cash = 200000  # 初始资金
gzmt_signal['stock value'] = gzmt_signal['price'] * gzmt_signal['Order']  # 股票价值
gzmt_signal['cash'] = initial_cash - (gzmt_signal['Order'].diff() * gzmt_signal['price']).cumsum()  # 现金价值
gzmt_signal['total asset'] = gzmt_signal['cash'] + gzmt_signal['stock value']  # 总资产
plt.figure(figsize=(10, 6))  # 设置画布大小
plt.plot(gzmt_signal['total asset'], label='total asset')  # 总资产
plt.plot(gzmt_signal['Order'].cumsum() * gzmt_signal['price'], '--', label='stock value')  # 股票价值
plt.xticks(gzmt_signal.index[::30], rotation=45)  # 设置x轴刻度,每30天标一个日期
plt.legend(loc='best')  # 图例放在最佳位置
plt.grid(True)  # 添加网格线
plt.show()

回测结果1
由上图可知,半年多的“低买高卖”操作,总资产从20万增加到了40万左右,翻了将近一倍。考虑到同时期大盘表现并不太好,在这样的背景下总资产没有大幅缩水,对于一个新手来说相当不错了。

2.1.3 关于回测,你还要知道的

回测过程中策略的指标是损益(Profit and Loss, P&L),即策略的收益。不过,在上述这个简单的框架中并没有考虑到滑点、佣金、税费等交易成本,考虑了这些之后可以计算出一个净损益,即Net P&L。除此之外,还要关注其他的一些指标,如:策略收益率、年化收益率、最大回撤、风险敞口、夏普比率、索提诺比率、信息比率等等。

2.2 经典策略之移动平均策略

2.2.1 单一移动平均线(Single Moving Average, SMA)——以10日均线为例观察股价与均线的关系

均线策略的核心思路如下:
(1)单一均线
当股价向上穿过N日均线时,说明股价向上突破,做多;
当股价向下穿过N日均线时,说明股价向下突破,做空。
(2)双均线(M<N)
当M日均线向上穿过N日均线时,说明股价向上突破,做多;
当M日均线向下穿过N日均线时,说明股价向下突破,做空。

首先,复制一张原始前复权行情表格,计算10日均价。
先来看书本上采用的这种方法:

gzmt = stock_data.copy(deep=True)  # 深复制数据
ma_period = 10  # 均线周期
price_10 = []  # 创建空列表,用于存储每10天的股票价格
avg_price_10 = []  # 创建空列表,用于存储每10天的股票价格的均值
for price in gzmt['close']:
    price_10.append(price)  # 将每10天的股票价格存入列表
    if len(price_10) > ma_period:
        del price_10[0]  # 删除第一个元素,保留最后10个元素
    avg_price_10.append(np.mean(price_10))  # 计算每10天的股票价格的均值

gzmt = gzmt.assign(ma10=pd.Series(avg_price_10, index=gzmt.index))  # 将均值存入数据框
gzmt

这里为什么使用深复制而非浅复制,主要是由深复制的特性所决定的,不会改变原始数据表格。此外,熟悉Pandas操作的话,可以用Pandas的rolling方法一行代码求均线。

stock_data['ma10'] = stock_data['close'].rolling(window=ma_period, min_periods=1).mean()  # 计算10日均线
stock_data

贵州茅台-10日均线数据
加入了均线之后的数据如上表所示,接下来画图展示一下收盘价与均线之间的关系。

# 画图展示股价与均价的关系
plt.figure(figsize=(10, 6))  # 设置画布大小
plt.plot(stock_data['close'], label='close', lw=2, c='k')  # 股票收盘价
plt.plot(stock_data['ma10'], '--', label='ma10', lw=2, c='g')  # 10日均线
plt.xticks(stock_data.index[::30], rotation=45)  # 设置x轴刻度,每30天标一个日期
plt.grid(True)  # 添加网格线
plt.legend(loc='best')  # 图例放在最佳位置
plt.show()

收盘价与10日均线
由上图可知,这段时间内,股价整体上处于下行状态,但不要紧,接下来就基于这种“逆境”来构建交易策略。

2.2.2 双均线策略的实现

策略的核心思路:利用两条均线来判断股价的走势。一条短期均线(如5日均线),一条长期均线(如20日均线)。当短期均线向上穿过长期均线时,说明股价向上突破,做多;反之,当短期均线向下穿过长期均线时,说明股价向下突破,做空。在此,以5日均线和20日均线为例操作。
依然是先计算5日均线和20日均线值,并据此产生交易信号,同时根据交易信号变动产生下单指令,具体代码展示如下:

double_ma = pd.DataFrame(index=stock_data.index)  # 创建空的交易信号数据框
double_ma['signal'] = 0  # 存储交易信号,默认为0
double_ma['ma5'] = stock_data['close'].rolling(window=5, min_periods=1).mean()  # 短期均线
double_ma['ma20'] = stock_data['close'].rolling(window=20, min_periods=1).mean()  # 长期均线
double_ma['signal'] = np.where(double_ma['ma5'] > double_ma['ma20'], 1, 0)  # 计算交易信号
double_ma['order'] = double_ma['signal'].diff()  # 计算下单指令,交易信号由0变为1时买入,由1变为0时卖出,不变不动
double_ma.fillna(0, inplace=True)  # 将缺失值填充为0
double_ma

接下来,以图像的形式直观的展示收盘价、均线与买卖点之间的关系。

# 画图展示
plt.figure(figsize=(10, 6))  # 设置画布大小
plt.plot(stock_data['close'], label='close', lw=2, c='k')  # 股票收盘价
plt.plot(double_ma['ma5'], label='ma5', lw=1.6, c='g')  # 5日均线
plt.plot(double_ma['ma20'], label='ma20', lw=1.6, c='y')  # 20日均线
plt.scatter(double_ma.loc[double_ma['order'] == 1].index, 
            stock_data['close'][double_ma['order'] == 1],
            marker='^', color='r', s=80, label='Buy')  # 买入信号
plt.scatter(double_ma.loc[double_ma['order'] == -1].index,
            stock_data['close'][double_ma['order'] == -1],
            marker='v', color='g', s=80, label='Sell')  # 卖出信号
plt.xticks(stock_data.index[::30], rotation=45)  # 设置x轴刻度,每30天标一个日期
plt.grid(True)  # 添加网格线
plt.legend(loc='best')  # 图例放在最佳位置
plt.show()

双均线买卖点提示
通过肉眼观察可知,每一次的卖出价都要低于买入价,总体上这个策略是亏损的。

2.2.3 对双均线策略进行回测

initial_cash = 200000  # 初始资金
bk_double_ma = pd.DataFrame(index=stock_data.index).fillna(0)  # 创建空的回测结果数据框
bk_double_ma['carry stock'] = double_ma['signal'] * 100 # 买卖信号,买卖都是100股的整数倍
bk_double_ma['stock value'] = bk_double_ma['carry stock'] * stock_data['close']  # 股票价值
bk_double_ma['cash'] = initial_cash - (bk_double_ma['carry stock'].diff() * stock_data['close']).cumsum()  # 现金价值
bk_double_ma['total asset'] = bk_double_ma['cash'] + bk_double_ma['stock value']  # 总资产
bk_double_ma

回测表-双均线策略
由上表可知,最后亏了大概两三万,亏损了将近10%。下面依然以图像的形式直观的看一下股票价值与总资产的变动情况:

# 画图展示
plt.figure(figsize=(10, 6))  # 设置画布大小
plt.plot(bk_double_ma['total asset'], label='total asset', lw=2)  # 总资产
plt.plot(bk_double_ma['stock value'], ls='--', lw=2, label='stock value', c='y')  # 股票价值
plt.xticks(bk_double_ma.index[::30], rotation=45)  # 设置x轴刻度,每30天标一个日期
plt.grid(True)  # 添加网格线
plt.legend(loc='best')  # 图例放在最佳位置
plt.show()

具体结果如下图所示:
回测图-双均线策略
相比于此前简单的“低买高卖”,这个双均线策略表现的更糟糕,空仓时间较长,避免了较大的波动,但是也没能实现“逆势赚钱”。

2.3 经典策略之海龟交易法则

核心要点:股价超过过去N日最高价时买入,跌破N日最低价时卖出。

2.3.1 使用海龟策略生成交易信号

策略重点:使用过去N日股价最高点和最低点生成唐奇安通道,当股价超过唐奇安通道上轨时买入,跌破唐奇安通道下轨时卖出。一般来说,N会取20。代码如下:

turtle = pd.DataFrame(index=stock_data.index)  # 创建空的交易信号数据框
turtle['high'] = stock_data['close'].shift(1).rolling(window=20, min_periods=1).max()  # 计算20日最高价(唐奇安通道上轨)
turtle['low'] = stock_data['close'].shift(1).rolling(window=20, min_periods=1).min()  # 计算20日最低价(唐奇安通道下轨)
turtle['buy'] = stock_data['close'] > turtle['high']  # 股价超过唐奇安通道上轨,产生买入信号
turtle['sell'] = stock_data['close'] < turtle['low']  # 股价跌破唐奇安通道下轨,产生卖出信号
turtle

依然是先计算唐奇安通道的上下轨,并据此产生买卖信号,结果如下表所示:
海龟交易法则-交易信号表
由上表可知,high储存的是唐奇安通道上轨,low储存的是唐奇安通道下轨,buy储存的是买入信号,sell储存的是卖出信号。buy为True时,代表买入,sell为True时,代表卖出;False代表不操作。实际上,在唐奇安通道中,中间还有一条线,是上轨和下轨的均值,这条线也可以作为买卖的参考。

2.3.2 根据交易信号和仓位下单

当交易信号给出“买入”信号且空仓时下单买入,当交易信号给出“卖出”信号且持仓时下单卖出。具体代码如下所示:

turtle['order'] = 0  # 初始化订单状态为0
position = 0  # 初始化仓位为0
# for循环遍历turtle数据表
for i in range(len(turtle)):
    # 当交易信号给出“买入”信号且空仓时下单买入
    if turtle.buy[i] == True and position == 0:
        turtle.order.values[i] = 1  # 买入1手
        position = 1  # 买入后仓位变为1
    # 当交易信号给出“卖出”信号且持仓时下单卖出
    elif turtle.sell[i] == True and position == 1:
        turtle.order.values[i] = -1  # 卖出1手
        position = 0  # 卖出后仓位变为0
turtle  # 查看新的turtle数据表

海龟交易法则-下单情况
接下来仍然以图像的形式直观的感受一下唐奇安通道与具体的下单情况:

# 画图展示
plt.figure(figsize=(10, 6))  # 设置画布大小
plt.plot(stock_data['close'], label='close', lw=2, c='k')  # 股票收盘价
plt.plot(turtle['high'], label='high', lw=1.6, c='g')  # 唐奇安通道上轨
plt.plot(turtle['low'], label='low', lw=1.6, c='y')  # 唐奇安通道下轨
plt.scatter(turtle.loc[turtle.order == 1].index,
            stock_data['close'][turtle.order == 1],
            marker='^', color='r', s=80, label='Buy')  # 买入信号
plt.scatter(turtle.loc[turtle.order == -1].index,
            stock_data['close'][turtle.order == -1],
            marker='v', color='g', s=80, label='Sell')  # 卖出信号
plt.xticks(turtle.index[::30], rotation=45)  # 设置x轴刻度,每30天标一个日期
plt.grid(True)  # 添加网格线
plt.legend(loc='best')  # 图例放在最佳位置
plt.show()

唐奇安通道
由上图可知,当股价突破唐奇安通道上轨时买入,当股价跌破唐奇安通道下轨时卖出。在此期间,一共进行了5笔交易,最后一笔为买入单,还未平仓。

2.3.3 对海龟策略进行回测

initial_cash = 200000  # 初始资金
bk_turtle = pd.DataFrame(index=turtle.index).fillna(0)  # 创建空的回测结果数据框,初始值填充为0
bk_turtle['carry stock'] = 100 * turtle['order'].cumsum()  # 买卖信号,买卖都是100股的整数倍
bk_turtle['stock value'] = bk_turtle['carry stock'] * stock_data['close']  # 股票价值
bk_turtle['cash'] = initial_cash - (bk_turtle['carry stock'].diff() * stock_data['close']).cumsum()  # 现金价值
bk_turtle['total asset'] = bk_turtle['cash'] + bk_turtle['stock value']  # 总资产
bk_turtle

回测表-海龟交易策略

# 画图展示
plt.figure(figsize=(10, 6))  # 设置画布大小
plt.plot(bk_turtle['total asset'], label='total asset', lw=2)  # 总资产
plt.plot(bk_turtle['stock value'], ls='--', lw=2, label='stock value', c='y')  # 股票价值
plt.xticks(bk_turtle.index[::30], rotation=45)  # 设置x轴刻度,每30天标一个日期
plt.grid(True)  # 添加网格线
plt.legend(loc='best')  # 图例放在最佳位置
plt.show()

回测图-海龟交易策略
由上图可知,小瓦最后的总资产约为18万,赔了近2万元,而此前的双均线策略赔了2.6万元左右,少赔就是赚,这个策略表现的相对要好一点。另外,值得注意的是,本章选择的这段时间,股价整体上是下跌的,下跌趋势下尽量降低损失是必要的。当然,也可以选一个上涨的波段来回测,看看效果如何。
说好了会有彩蛋的,那就不能食言,请点此链接获取本书PDF全文,免费送上!
再次感谢Tushare社区的数据支持,谢谢!
注:My Tushare ID is 570231.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值