概述
上一节我们建立了最最简单的交易策略,尽管有了盈利,但实际操作上是不可行的。本节将运用移动平均指标,包括单一移动平均策略和双移动平均策略,来建立经典的移动平均策略。
数据采集处理
本文采用上一节的相同数据,为了方便,可以将数据保证在本地。为了完整性本次仍将提供完整代码,便于小伙伴们复现。
# 加载相应的库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import baostock as bs
import warnings
warnings.filterwarnings("ignore")
plt.rcParams['font.family'] = ['sans-serif']
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus']=False
lg = bs.login()
#指定一下获取股票数据的起始日期和截止日期
#这里就用2023年1月1日至今日的数据
start_date = '2023-01-01'
end_date = '2023-10-19'
#创建数据表,这里选择下载的股票代码为600000
rs=bs.query_history_k_data_plus('600000.sh',
"date,open,high,low,close,volume",
start_date=start_date, end_date=end_date,
frequency="d", adjustflag="3")
# .get_data()
#下面来检查一下数据表的前5行
data=rs.get_data()
cols=["open","high","low","close","volume"]
data[cols]=data[cols].astype('float')
data['date']=pd.to_datetime(data['date'])
data.set_index('date',inplace=True)
data.to_csv("./data/sh600000.csv")
单一移动平均策略
这个策略的核心思想:利用趋势,低买高卖。一般选择一个适当的平均指标来衡量市场的趋势。比如,可以使用简单移动平均线 (SMA) 或指数移动平均线 (EMA) 等,这些指标在之前的文章已作详细说明。
策略: 当股价向上且向上穿过N日均,说明股价向上突破,此时买入;当股价下降且向下穿过N日均线时,说明股价整体出现下跌趋势,此时卖出!
接下来,我将以10日均线(均价)为例进行讲解。
获取均值
如果直接用rolling 函数或 talib.MA获取移动平均值,会出现前面的数值为空情况。
data.close.rolling(window=10).mean()
date
2023-01-03 NaN
2023-01-04 NaN
2023-01-05 NaN
2023-01-06 NaN
2023-01-09 NaN
…
2023-10-13 7.102
2023-10-16 7.091
2023-10-17 7.084
2023-10-18 7.074
2023-10-19 7.045
Name: close, Length: 191, dtype: float64
为了更好的平滑这个曲线,我们做了一些简单的处理,还不到这些10天的空值,均由前面的平均给定。
最后获得均值合并到data
数据中。
#这里使用10日均线
period = 10
#设置一个空列表,用来存储每10天的价格
avg_10 = []
#再设置一个空列表,用来存储每10天价格的均值
avg_value = []
#设置一个循环
for price in data['close']:
#把每天的价格传入到avg_10列表
avg_10.append(price)
#当列表中存储的数值多于10个时
if len(avg_10) > period:
#就把前面传入的价格数据删掉,确保列表中只有10天的数据
del avg_10[0]
#将10天数据的均值传入到avg_value列表中
avg_value.append(np.mean(avg_10))
#把计算好的10日均价写到股票价格数据表中
data['MA10']=avg_value
data
如下表,MA10 ,第一个数据,即为close 本身,第二个数据为7.23和7.31 的平均值,以此累推。
date | open | high | low | close | volume | MA10 |
---|---|---|---|---|---|---|
2023-01-03 | 7.27 | 7.28 | 7.17 | 7.23 | 25892521.0 | 7.230000 |
2023-01-04 | 7.27 | 7.35 | 7.23 | 7.31 | 30947081.0 | 7.270000 |
2023-01-05 | 7.37 | 7.38 | 7.30 | 7.35 | 30162154.0 | 7.296667 |
2023-01-06 | 7.35 | 7.38 | 7.31 | 7.34 | 20312881.0 | 7.307500 |
2023-01-09 | 7.38 | 7.38 | 7.30 | 7.34 | 19612260.0 | 7.314000 |
… | … | … | … | … | … | … |
2023-10-13 | 7.11 | 7.15 | 7.08 | 7.10 | 19650410.0 | 7.102000 |
2023-10-16 | 7.12 | 7.13 | 7.04 | 7.07 | 24907733.0 | 7.091000 |
2023-10-17 | 7.09 | 7.10 | 7.05 | 7.09 | 19029143.0 | 7.084000 |
2023-10-18 | 7.07 | 7.11 | 7.05 | 7.05 | 21485721.0 | 7.074000 |
2023-10-19 | 7.04 | 7.05 | 6.83 | 6.84 | 61679771.0 | 7.045000 |
191 rows × 6 columns
设置交易信号
当股价向上且向上穿过10日均线,说明股价向上突破,此时买入;当股价下降且向下穿过10日均线时,说明股价整体出现下跌趋势,此时卖出!
# 定义买入和卖出的条件
# data['buy_signal'] = np.where(data['close'] > data['MA10'], 1, 0)
# data['sell_signal'] = np.where(data['close'] < data['MA10'], -1, 0)
data['signal']=np.where(data['close'] > data['MA10'], 1, 0)
data['order']=data['signal'].diff()*100
原本分别设置买入和卖出,导致算法复杂,统一修改成上节,便于操作,结果也不受影响,最后根据交易信号确定买卖数量。
这里有一个注意事项,在操作中,可能会出现,先卖后买的情况,即在data中先有-100的值,遇到这种情况,需要作一些调整,或直接修改为0,即可。
图形查看
利用图形,查看具体操作信号的位置。
#设置图像尺寸为12*8
plt.figure(figsize=(15,10))
#绘制股价的变化
plt.plot(data['close'],lw=2, c='k',label='股价')
#绘制10日均线
plt.plot(data['MA10'], '--',lw=2, c='b',label='10日均线')
#如果当天股价下跌给出买入信号,用正三角表示
plt.scatter(data['close'].loc[data.order>0].index,
data['close'][data.order>0],
marker = '^', s=80, c='r')
#如果当天股价上涨,标出卖出信号,用倒三角表示
plt.scatter(data['close'].loc[data.order<0].index,
data['close'][data.order<0],
marker = 'v', s=80, c='g')
#添加图注和网格
plt.legend()
plt.grid()
#将图像进行显示
plt.show();
处理数据
df=data.copy()
df['price']=df['close']
df.fillna(0.0,inplace=True)
df=df.fillna(0)
#一般情况下,在A股市场,买入或卖出至少为100股,即1手
df['order'] = df['signal'].diff()*100
df.fillna(0.0,inplace=True)
df['cash']=np.NaN
df['total']=np.NaN
df['position']=df['order'].cumsum()
df.cash.iloc[0]=1000
for i in range(1,len(df)):
if (df.order.iloc[i]==0):
df.cash.iloc[i] =df.cash.iloc[i-1]
else:
df.cash.iloc[i] =df.cash.iloc[i-1]-df.price.iloc[i]*df.order.iloc[i]
df['total'] =df['cash']+df['position']*df['price']
# 有一些警告错误 可以关闭
#为了让直观看到自己的总资产变化
#我们用图形来进行展示
#设置图形的尺寸是10*6
plt.figure(figsize=(10,6))
#分别绘制总资产和持仓股票市值的变化
plt.plot(df['total'],label='总市值')
plt.plot(df['order'].cumsum()*df['price'],'--',
label='股票市值')
#增加网格,调整一下图注的位置,就可以显示图像了
plt.grid()
plt.legend(loc='center right')
plt.show()
查看收益
date | open | high | low | close | volume | MA10 | signal | order | price | cash | total | position |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2023-01-03 | 7.27 | 7.28 | 7.17 | 7.23 | 25892521.0 | 7.230000 | 0 | 0.0 | 7.23 | 1000.0 | 1000.0 | 0.0 |
2023-01-04 | 7.27 | 7.35 | 7.23 | 7.31 | 30947081.0 | 7.270000 | 1 | 100.0 | 7.31 | 269.0 | 1000.0 | 100.0 |
2023-01-05 | 7.37 | 7.38 | 7.30 | 7.35 | 30162154.0 | 7.296667 | 1 | 0.0 | 7.35 | 269.0 | 1004.0 | 100.0 |
2023-01-06 | 7.35 | 7.38 | 7.31 | 7.34 | 20312881.0 | 7.307500 | 1 | 0.0 | 7.34 | 269.0 | 1003.0 | 100.0 |
2023-01-09 | 7.38 | 7.38 | 7.30 | 7.34 | 19612260.0 | 7.314000 | 1 | 0.0 | 7.34 | 269.0 | 1003.0 | 100.0 |
… | … | … | … | … | … | … | … | … | … | … | … | … |
2023-10-13 | 7.11 | 7.15 | 7.08 | 7.10 | 19650410.0 | 7.102000 | 0 | -100.0 | 7.10 | 946.0 | 946.0 | 0.0 |
2023-10-16 | 7.12 | 7.13 | 7.04 | 7.07 | 24907733.0 | 7.091000 | 0 | 0.0 | 7.07 | 946.0 | 946.0 | 0.0 |
2023-10-17 | 7.09 | 7.10 | 7.05 | 7.09 | 19029143.0 | 7.084000 | 1 | 100.0 | 7.09 | 237.0 | 946.0 | 100.0 |
2023-10-18 | 7.07 | 7.11 | 7.05 | 7.05 | 21485721.0 | 7.074000 | 0 | -100.0 | 7.05 | 942.0 | 942.0 | 0.0 |
2023-10-19 | 7.04 | 7.05 | 6.83 | 6.84 | 61679771.0 | 7.045000 | 0 | 0.0 | 6.84 | 942.0 | 942.0 | 0.0 |
小结
本次的单一移动均值策略,居然亏损了,但我们分析一下整理效果。
双移动平均策略
顾名思义,双移动平均策略,就是用两条不同的均线来判定股票走势。一般情况,在两条均线中,一条是短期均线(如5日均线),另一条是长期均线(如20日均线)【5日均线又称周均线,20日均线又称月均线,因为一周有5个交易日,一月有21个交易】。
仍以上述数据,只增加5日均值线和20日均值线合并到data。
为了方便定义了一个函数。 在设置交易信号不同,其余均与上述方法相同。
def getSMA(data,period=10):
# period = 10
#设置一个空列表,用来存储每10天的价格
avg_10 = []
#再设置一个空列表,用来存储每10天价格的均值
avg_value = []
#设置一个循环
for price in data['close']:
#把每天的价格传入到avg_10列表
avg_10.append(price)
#当列表中存储的数值多于10个时
if len(avg_10) > period:
#就把前面传入的价格数据删掉,确保列表中只有10天的数据
del avg_10[0]
#将10天数据的均值传入到avg_value列表中
avg_value.append(np.mean(avg_10))
return avg_value
获得5日和20均线
df=data.copy()
#把分别计算好5日和20日均线
df['MA5']=getSMA(df,5)
df['MA20']=getSMA(df,20)
# 删除多余的列
df.drop(['MA10','signal','order'],axis=1,inplace=True)
设置交易信号
当5日均线上穿20日均线就买入,当5日均线下穿20日均线就卖出。
# 定义买入和卖出的条件
df['signal']=np.where(df['MA5'] > df['MA20'], 1, 0)
df['order']=df['signal'].diff()*100
画交易点图形
#设置图像尺寸为12*8
plt.figure(figsize=(12,8))
#绘制股价的变化
plt.plot(df['close'],lw=2, c='k',label='股价')
#绘制10日均线
plt.plot(df['MA5'], '--',lw=2, c='b',label='5日均线')
plt.plot(df['MA20'], '*',lw=2, c='g',label='20日均线')
#如果当天股价下跌给出买入信号,用正三角表示
plt.scatter(df['close'].loc[df.order>0].index,
df['close'][df.order>0],
marker = '^', s=80, c='r')
#如果当天股价上涨,标出卖出信号,用倒三角表示
plt.scatter(df['close'].loc[df.order<0].index,
df['close'][df.order<0],
marker = 'v', s=80, c='g')
#添加图注和网格
plt.legend()
plt.grid()
#将图像进行显示
plt.show();
处理数据
# df=data.copy()
df['price']=df['close']
df.fillna(0.0,inplace=True)
df=df.fillna(0)
#一般情况下,在A股市场,买入或卖出至少为100股,即1手
df['order'] = df['signal'].diff()*100
df.fillna(0.0,inplace=True)
df['cash']=np.NaN
df['total']=np.NaN
df['position']=df['order'].cumsum()
df.cash.iloc[0]=1000
for i in range(1,len(df)):
if (df.order.iloc[i]==0):
df.cash.iloc[i] =df.cash.iloc[i-1]
else:
df.cash.iloc[i] =df.cash.iloc[i-1]-df.price.iloc[i]*df.order.iloc[i]
df['total'] =df['cash']+df['position']*df['price']
plt.figure(figsize=(10,6))
#分别绘制总资产和持仓股票市值的变化
plt.plot(df['total'],label='总市值')
plt.plot(df['order'].cumsum()*df['price'],'--',
label='股票市值')
#增加网格,调整一下图注的位置,就可以显示图像了
plt.grid()
plt.legend(loc='center right')
plt.show()
查看收益
date | open | high | low | close | volume | MA5 | MA20 | signal | order | price | cash | total | position |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2023-01-03 | 7.27 | 7.28 | 7.17 | 7.23 | 25892521.0 | 7.230000 | 7.230000 | 0 | 0.0 | 7.23 | 1000.0 | 1000.0 | 0.0 |
2023-01-04 | 7.27 | 7.35 | 7.23 | 7.31 | 30947081.0 | 7.270000 | 7.270000 | 0 | 0.0 | 7.31 | 1000.0 | 1000.0 | 0.0 |
2023-01-05 | 7.37 | 7.38 | 7.30 | 7.35 | 30162154.0 | 7.296667 | 7.296667 | 0 | 0.0 | 7.35 | 1000.0 | 1000.0 | 0.0 |
2023-01-06 | 7.35 | 7.38 | 7.31 | 7.34 | 20312881.0 | 7.307500 | 7.307500 | 0 | 0.0 | 7.34 | 1000.0 | 1000.0 | 0.0 |
2023-01-09 | 7.38 | 7.38 | 7.30 | 7.34 | 19612260.0 | 7.314000 | 7.314000 | 0 | 0.0 | 7.34 | 1000.0 | 1000.0 | 0.0 |
… | … | … | … | … | … | … | … | … | … | … | … | … | … |
2023-10-13 | 7.11 | 7.15 | 7.08 | 7.10 | 19650410.0 | 7.060000 | 7.091500 | 0 | 0.0 | 7.10 | 911.0 | 911.0 | 0.0 |
2023-10-16 | 7.12 | 7.13 | 7.04 | 7.07 | 24907733.0 | 7.068000 | 7.095000 | 0 | 0.0 | 7.07 | 911.0 | 911.0 | 0.0 |
2023-10-17 | 7.09 | 7.10 | 7.05 | 7.09 | 19029143.0 | 7.084000 | 7.097500 | 0 | 0.0 | 7.09 | 911.0 | 911.0 | 0.0 |
2023-10-18 | 7.07 | 7.11 | 7.05 | 7.05 | 21485721.0 | 7.090000 | 7.097000 | 0 | 0.0 | 7.05 | 911.0 | 911.0 | 0.0 |
2023-10-19 | 7.04 | 7.05 | 6.83 | 6.84 | 61679771.0 | 7.030000 | 7.086500 | 0 | 0.0 | 6.84 | 911.0 | 911.0 | 0.0 |
191 rows × 13 columns
总结
- 这几个策略都没有取得良好的效果,这是因为移动平均策略是适合趋势市场。这个震荡的市场效果不理想。
- 以上回测,并没有加入交易费用,是不全面的。
- 回测的图形不是很直观,没有看到收益情况,需要查表格最后才能看明白。
- 回测应有收益曲线,基准曲线等
- 回测一些相关参数,如 α 和 β \alpha 和 \beta α和β,最大回测,年化收益等
- 完整的策略,包括 指标、标的、择时以及风控等组成。
在以后文章中,将逐步增加以上相关知识点。
在此警告:文章中的所有内容,不能给你构成投资的理由。