4. 简单金融量化分析
4.1 移动平均线
a) 概念 & 策略
移动均线(Moving Average,简称MA)是用统计分析的方法,将一定时期内的证券价格(指数)加以平均,并把不同时间的平均值连接起来,形成一根MA,用以观察证券价格变动趋势的一种技术指标。移动平均线是由著名的美国投资专家Joseph E.Granville(葛兰碧,又译为格兰威尔)于20世纪中期提出来的。均线理论是当今应用最普遍的技术指标之一,它帮助交易者确认现有趋势、判断将出现的趋势、发现过度延生即将反转的趋势。
移动均线常用线有5天、10天、30天、60天、120天和240天的指标。其中,5天和10天的短期移动平均线,是短线操作的参照指标,称做日均线指标;30天和60天的是中期均线指标,称做季均线指标;120天、240天的是长期均线指标,称做年均线指标。
收盘价5日均线算法:
从第五天开始,每天计算最近五天的收盘价的平均值所构成的一条线
用 a, b, c, d, e, f, g, h, i, j 表示10天的股票价格, 5日移动平均线的算法为:
(a+b+c+d+e)/5
(b+c+d+e+f)/5
(c+d+e+f+g)/5
…
(f+g+h+i+j)/5
策略使用的方法分为两种:
第一种:
- 当股价上升并且交叉穿过X天的移动平均线时买入
- 当股价下降并且交叉穿过X天的移动平均线时卖出
第二种:
- 当x天的移动平均线上升并交叉穿过y天移动平均线时买入;
- 当x天的移动平均线下降并交叉穿过y天移动平均线时卖出;
b) 绘制移动均线
用苹果股票价格绘制5日和10日移动平均线
import numpy as np
import pandas as pd
# 数据读取
data = pd.read_csv('aapl.csv',
header=None,
usecols=[1,6],
names=['dates','close'])
# 处理日期格式
def func(item):
# 28-1-2011 ==> 2011-01-28
return pd.to_datetime('-'.join(item.split('-')[::-1]))
data['dates'] = data['dates'].apply(func)
# 计算5日移动平均线
close = data['close'] # 拿到close字段的值
ma5 = np.zeros(close.size - 4) # 创建一个ndarray, 长度比close小4个, 值都是0
for i in range(ma5.size): # 将5日均值加入到ma5的ndarray中
ma5[i] = close[i:i+5].mean() # 用切片计算均值
# 将ndarray转为series, index为从close[4]开始的close的index
ma5 = pd.Series(ma5, index=np.arange(4, close.size))
data['ma5'] = ma5 # 把ma5存入dataframe
# 画出移动平均线
data.plot(x='dates', y=['close', 'ma5'])
# 计算10日移动平均线
close = data['close']
ma10 = np.zeros(close.size - 9)
for i in range(ma10.size):
ma10[i] = close[i:i+10].mean()
ma10 = pd.Series(ma10, index=np.arange(9,close.size))
data['ma10'] = ma10
print(data)
"""
dates close ma5 ma10
... ... ... ... ...
8 2011-02-09 358.16 351.036 NaN
9 2011-02-10 354.54 353.256 347.449
10 2011-02-11 356.85 355.326 349.524
11 2011-02-14 359.18 356.786 351.510
... ... ... ... ...
"""
# 画图表
import matplotlib.pyplot as plt
plt.plot(data['dates'], data['close'], linestyle=':', c='black', alpha=0.5, label='close')
plt.plot(data['dates'], data['ma5'], label='ma5')
plt.plot(data['dates'], data['ma10'], label='ma10')
plt.tick_params(axis='x', labelrotation=45)
plt.legend()
4.2 布林带
a) 概念 & 策略
布林带(Bollinger Band)是美国股市分析家约翰·布林根据统计学中的标准差原理设计出来的一种非常实用的技术指标
-
它由三条线组成:
- 中轨:移动平均线(图中白色线条)
- 上轨:中轨+2x5日收盘价标准差 (图中黄色线条,顶部的压力)
- 下轨:中轨-2x5日收盘价标准差 (图中紫色线条,底部的支撑力)
-
布林带收窄代表稳定的趋势,布林带张开代表有较大的波动空间的趋势。
-
业务指标与含义 – 利用股价与布林带上轨线、下轨线进行比较,以及结合变化趋势,判断股票买入、卖出的时机
-
股价由下向上穿越下轨线(Down)时,可视为买进信号。
-
股价由下向上穿越中轨时,股价将加速上扬,是加仓买进的信号。
-
股价在中轨与上轨(UPER)之间波动运行时为多头市场,可持股观望。
-
股价长时间在中轨与上轨(UPER)间运行后,由上向下跌破中轨为卖出信号。
-
股价在中轨与下轨(Down)之间向下波动运行时为空头市场,此时投资者应持币观望。
-
布林中轨经长期大幅下跌后转平,出现向上的拐点,且股价在2~3日内均在中轨之上。此时,若股价回调,其回档低点往往是适量低吸的中短线切入点。
-
对于在布林中轨与上轨之间运作的强势股,不妨以回抽中轨作为低吸买点,并以中轨作为其重要的止盈、止损线。
-
飚升股往往股价会短期冲出布林线上轨运行,一旦冲出上轨过多,而成交量又无法持续放出,注意短线高抛了结,如果由上轨外回落跌破上轨,此时也是一个卖点。
-
b) 绘制布林带
用苹果股票价格绘制5日移动平均线的布林带
import numpy as np
import pandas as pd
# 导入数据
data = pd.read_csv('aapl.csv',
header=None,
usecols=[1,6],
names=['dates','close'])
# 修改日期格式, 将字符串转为日期格式
def func(item):
# 28-1-2011 ==> 2011-01-28
return pd.to_datetime('-'.join(item.split('-')[::-1]))
data['dates'] = data['dates'].apply(func)
# 计算5日移动平均线
close = data['close']
ma5 = np.zeros(close.size - 4)
for i in range(ma5.size):
ma5[i] = close[i:i+5].mean()
ma5 = pd.Series(ma5, index=np.arange(4,close.size))
data['ma5'] = ma5 # 把ma5存入dataframe
# 计算最近5日收盘价的标准差数组
stds = np.zeros(ma5.size) # 创建一个数组, 长度于ma5字段的长度一致
for i in range(stds.size):
stds[i] = close[i:i+5].std() # 计算5日close值的标准差, 并放入stds数组
# 将数组转为序列后 加入到DF中
stds = pd.Series(stds, index=np.arange(4,30))
data['stds'] = stds
# 创建两个序列upper和lower, 分别表示上轨和下轨
upper = data['ma5'] + 2*data['stds'] # 上轨:中轨+2x5日收盘价标准差
lower = data['ma5'] - 2*data['stds'] # 下轨:中轨-2x5日收盘价标准差
# 将两个序列添加入DF中
data['upper'] = upper
data['lower'] = lower
# 绘制布林带
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6)) # 调整图表大小
plt.plot(data['dates'], data['close'], c='black', alpha=0.5, linestyle=':', label='close')
plt.plot(data['dates'], data['ma5'], c='y', alpha=1, linestyle='-', label='ma5')
plt.plot(data['dates'], data['upper'], c='r', alpha=1, linestyle='-', label='upper')
plt.plot(data['dates'], data['lower'], c='g', alpha=1, linestyle='-', label='lower')
p1 = plt.fill_between(data['dates'], # x轴的坐标数组
data['upper'], # y1线曲线上的y坐标点
data['lower'], # y2线曲线上的y坐标点
data['upper']>data['lower'], # 设置条件: 当y1<y2为True时
color='b', # 设置填充颜色
alpha=0.1) # 设置透明度
plt.tick_params(axis='x', labelrotation=45) # 设置x轴刻度倾斜显示
plt.grid(alpha=0.3, linestyle='--') # 画网格线
plt.legend() # 显示图例
4.3 股票回测模型
a) 金融数据包
-
AkShare 是基于 Python 的开源金融数据接口库,目的是实现对股票、期货、期权、基金、外汇、债券、指数、数字货币等金融产品的基本面数据、实时和历史行情数据、衍生数据从数据采集、数据清洗、到数据落地的一套开源工具,满足金融数据科学家,数据科学爱好者在金融数据获取方面的需求。
# 安装 pip3 install akshare --upgrade
-
TuShare是一个著名的免费、开源的python财经数据接口包。其官网主页为:TuShare -财经数据接口包。tushare是一个开源的金融数据源,目前维护的数据非常丰富,质量也很高,对于一般的分析已经足够,可以省去自己到处去爬数据。
tushare目前采取以积分来获得不同的数据使用权限的方式,所需要的积分可以通过邀请注册以及其它方式来获得,达标的难度不大,以下是我的邀请链接,每一注册用户可以给我带来50积分,基本上600积分以上可以使用全部数据了。
https://tushare.pro/register?reg=384972
# 安装
pip3 install tushare --upgrade
b) 回测模型
1) 浦发银行金叉策略
通过金叉死叉理论定义操盘策略, 对浦发银行2018年1月1日~2019年12月31日这2年的股票价格进行回测
拿到数据
# 导包
import tushare as ts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 导入浦发银行18~19的两年数据
pro = ts.pro_api('****口令密码****')
data = ts.pro_bar(ts_code='600000.SH', start_date='20180101', end_date='20191231')
# 数据处理
data = data.drop(columns=['ts_code', 'pre_close']) # 删除不需要的字段
data = data.sort_values(by='trade_date') # 安装trade_date字段从大到小排序
data.reset_index(drop=True, inplace=True) # 重新排列index
data['trade_date'] = pd.to_datetime(data['trade_date']) # 将trade_date字段改为日期类型
data.head(2).append(data.tail(2)) # 数据简览
"""
trade_date open high low close change pct_chg vol amount
0 2018-01-02 12.61 12.77 12.60 12.72 0.13 1.0300 313230.53 398614.966
1 2018-01-03 12.73 12.80 12.66 12.66 -0.06 -0.4700 378391.01 480954.809
... ... ... ... ... ... ... ... ... ...
485 2019-12-30 12.27 12.36 12.12 12.34 0.02 0.1623 410515.55 503090.483
486 2019-12-31 12.32 12.38 12.21 12.37 0.03 0.2431 319536.28 392736.236
"""
""" 字段介绍:
ts_code str 股票代码 (已删除)
trade_date str 交易日期
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价
pre_close float 昨收价 (已删除)
change float 涨跌额
pct_chg float 涨跌幅 (未复权)
vol float 成交量 (手)
amount float 成交额 (千元)
"""
# 查看图表
data.plot(x='trade_date', y=['close'], figsize=(20,3),grid=True)
计算ma5, 和ma10
# 计算ma5
ma5 = np.zeros(data.close.size-4) # 创建一个ma5的ndarray, 值都是0
for i in range(ma5.size): # 将5日均值加入到ma5的ndarray中
ma5[i] = data.close[i:i+5].mean()
ma5 = pd.Series(ma5, index=np.arange(4,data.close.size)) # 把ndarray转为series, index从4开始
data['ma5'] = ma5 # 把ma5存入dataframe
# 计算ma10
ma10 = np.zeros(data.close.size-9) # 创建一个ma10的ndarray, 值都是0
for i in range(ma10.size): # 将10日均值加入到ma10的ndarray中
ma10[i] = data.close[i:i+10].mean()
ma10 = pd.Series(ma10, index=np.arange(9,data.close.size)) # 把ndarray转为series, index从9开始
data['ma10'] = ma10 # 把ma10存入dataframe
print(data)
"""
trade_date open high low close change pct_chg vol amount ma5 ma10
0 2018-01-02 12.61 12.77 12.60 12.72 0.13 1.0300 313230.53 398614.966 NaN NaN
1 2018-01-03 12.73 12.80 12.66 12.66 -0.06 -0.4700 378391.01 480954.809 NaN NaN
... ... ... ... ... ... ... ... ... ... ... ...
485 2019-12-30 12.27 12.36 12.12 12.34 0.02 0.1623 410515.55 503090.483 12.294 12.329
486 2019-12-31 12.32 12.38 12.21 12.37 0.03 0.2431 319536.28 392736.236 12.312 12.327
"""
定义投资策略
- 通过均线理论,判断买入(1), 卖出(-1), 持有/无操作(0)
# 金叉:短期均线(ma5)上穿长期均线(ma10),全额买入
# 死叉:短期均线(ma5)下穿长期均线(ma10),全额卖出
# 定义策略函数, 传入参数为日期
def profit(mdate):
mask = data['trade_date']<=mdate # 拿到掩码, 表示今天日期以及之前的日期
# 如果拿到的日期是第一个日期, 则不存在前一天的日期数据, 所以这里做个判断
if mask.sum() < 2:
return 0 # 无操作
# 拿到这个日期的ma5和ma10的值, 用掩码拿到今天日期以及之前的日期的数据, 数据的最后一条就是今天的数据
today_ma5, today_ma10 = data[mask].iloc[-1]['ma5'], data[mask].iloc[-1]['ma10']
# 拿到前一天的ma5和ma10的值, 用掩码拿到今天日期以及之前的日期的数据, 数据的倒数第二条就是前一天的数据
yes_ma5, yes_ma10 = data[mask].iloc[-2]['ma5'], data[mask].iloc[-2]['ma10']
# 如果当天的ma5>当天的ma10,并且前一天的ma5<前一天的ma10, 意味着短期均线(ma5)上穿长期均线(ma10),要全额买入 (金叉)
if (yes_ma5<yes_ma10) & (today_ma5>today_ma10):
return 1
# 如果当天的ma5<当天的ma10,并且前一天的ma5>前一天的ma10, 意味着短期均线(ma5)下穿长期均线(ma10),要全额卖出 (死叉)
if (yes_ma5>yes_ma10) & (today_ma5<today_ma10):
return -1
# 其他情况下不做操作
else:
return 0
# 用apply函数对日期进行遍历执行策略函数, 得到每一天的投资操作
profits = data['trade_date'].apply(profit)
data['profits'] = profits # 加入DF
输出图表
# 使中文正常显示
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
# 定义图大小
plt.figure(figsize=(20,8))
# 画线
plt.plot(data['trade_date'], data['close'], linestyle=':', c='black', alpha=0.5, label='close')
plt.plot(data['trade_date'], data['ma5'], label='ma5', c='r')
plt.plot(data['trade_date'], data['ma10'], label='ma10', c='b')
# 画点
plt.scatter(x=data[data['profits']==-1].trade_date,
y=data[data['profits']==-1].close,
s=80, facecolor='y', label='卖出点')
plt.scatter(x=data[data['profits']==1].trade_date,
y=data[data['profits']==1].close,
s=80, facecolor='r', label='买入点')
# x轴内容倾斜显示
plt.tick_params(axis='x', labelrotation=45)
# 显示网格
plt.grid(alpha=0.2)
# 显示图例
plt.legend()
遍历每一天的投资建议, 模拟交易, 实现回测模型
# 现金数量
cash = 1000000
# 股票数量
stock = 0
# 状态1: 已全部买入, 状态-1: 已全部卖出, 初始状态0
status = 0
for index, p in profits.items():
# 当前交易价格
curr_price = data.iloc[index]['close']
# 当前日期
d = data.iloc[index]['trade_date']
# 模拟买入操作
if p == 1 and status != 1: # 当投资建议为买入(p==1), 并且有钱时(status!=1)
stock = int(assets/curr_price) # 可以买入的股票数量
cash = cash - stock*curr_price # 计算购买股票后剩余的现金
print(f'买入: trade_date = {d}; cash = {cash}; stock = {stock}')
status = 1 # 修改当前状态为已经全部买入
# 模拟卖出操作
if p == -1 and status != -1: # 当投资建议为卖出(p==-1), 并且有股票时(status!=-1)
cash = cash + stock*curr_price # 卖出全部股票后可以得到的现金
stock = 0 # 当前股票改为零
print(f'卖出: trade_date = {d}; cash = {cash}; stock = {stock}')
status = -1 # 修改当前状态为已经全部卖出
"""
卖出: trade_date = 2018-02-02 00:00:00; cash = 1000000.0; stock = 0
买入: trade_date = 2018-02-07 00:00:00; cash = 5.950000000069849; stock = 74349
卖出: trade_date = 2018-02-12 00:00:00; cash = 928624.9600000001; stock = 0
买入: trade_date = 2018-03-01 00:00:00; cash = 9.0; stock = 74468
......
卖出: trade_date = 2019-11-12 00:00:00; cash = 956140.3300000004; stock = 0
买入: trade_date = 2019-12-11 00:00:00; cash = 4.550000000395812; stock = 79811
卖出: trade_date = 2019-12-26 00:00:00; cash = 980881.7400000003; stock = 0
"""
可见, 根据此策略, 到19年底, 100万现金最后只能剩余980,881元
2) 浦发银行布林带策略
导入数据并进行基本处理
# 导包
import tushare as ts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 导入浦发银行从2018年到今天的所有数据
pro = ts.pro_api('*******************')
data = ts.pro_bar(ts_code='600000.SH', start_date='20180101')
data = data.drop(columns=['ts_code', 'pre_close']) # 删除不需要的字段
data = data.sort_values(by='trade_date') # 安装trade_date字段从大到小排序
data.reset_index(drop=True, inplace=True) # 重新排列index
data['trade_date'] = pd.to_datetime(data['trade_date']) # 将trade_date字段改为日期类型
data.head(2).append(data.tail(2))
data.plot(x='trade_date', y=['close'], figsize=(20,3),grid=True)
计算ma20和布林带
# 计算ma20
ma20 = np.zeros(data.close.size-19) # 创建一个ndarray, 值都是0
for i in range(ma20.size): # 将20日均值加入到ma20的ndarray中
ma20[i] = data.close[i:i+20].mean()
# 把ndarray转为series, index从19开始(因为index=0,1,2...是NaN)
ma20 = pd.Series(ma20, index=np.arange(19,data.close.size))
data['ma20'] = ma20 # 把ma5存入dataframe
# 计算近5日收盘价的标准差数组
stds = np.zeros(data.close.size-19) # 创建一个ndarray, 值都是0
for i in range(stds.size): # 将20日标准差加入到stds的ndarray中
stds[i] = data.close[i:i+20].std()
# 把ndarray转为series, index从19开始(因为index=0,1,2...是NaN)
stds = pd.Series(stds, index=np.arange(19,data.close.size))
data['stds'] = stds
# 计算upper
upper = data['ma20'] + 2*data['stds']
data['upper'] = upper
# 计算lower
lower = data['ma20'] - 2*data['stds']
data['lower'] = lower
# 画出布林带
plt.figure(figsize=(20, 6))
plt.plot(data['trade_date'], data['close'], c='b', linestyle='-', label='close')
plt.plot(data['trade_date'], data['ma20'], c='y', alpha=1, linestyle=':', label='ma20')
plt.plot(data['trade_date'], data['upper'], c='r', alpha=1, linestyle=':', label='upper')
plt.plot(data['trade_date'], data['lower'], c='g', alpha=1, linestyle=':', label='lower')
p1 = plt.fill_between(data['trade_date'], # x轴的坐标数组
data['upper'], # y1线曲线上的y坐标点
data['lower'], # y2线曲线上的y坐标点
data['upper']>data['lower'], # 设置条件: 当y1<y2为True时
color='b', # 设置填充颜色
alpha=0.05) # 设置透明度
plt.tick_params(axis='x', labelrotation=45)
plt.grid(alpha=0.3, linestyle='--')
plt.legend()
定义投资策略
- 当股价突破upper时全仓买入(1), 单股价跌破ma20时清仓卖出(-1), 其他时间不做操作(0)
def profit(mdate):
mask = data['trade_date']<=mdate # 拿到掩码, 表示今天日期以及之前的日期
# 如果拿到的日期是第一个日期, 则不存在前一天的日期数据, 所以这里做个判断
if mask.sum() < 2:
return 0 # 无操作
# 拿到这个日期的upper,ma20,和close的值, 用掩码拿到今天日期以及之前的日期的数据, 数据的最后一条就是今天的数据
today_upper, today_ma20, today_close = data[mask].iloc[-1]['upper'], data[mask].iloc[-1]['ma20'], data[mask].iloc[-1]['close']
# 拿到前一天的upper,ma20,和close的值, 用掩码拿到今天日期以及之前的日期的数据, 数据的倒数第二条就是前一天的数据
yes_upper, yes_ma20, yes_close = data[mask].iloc[-2]['upper'], data[mask].iloc[-2]['ma20'], data[mask].iloc[-2]['close']
# 如果当天的close>当天的upper,并且前一天的upper>前一天的close, 意味着股价上穿,要全额买入
if (today_close>today_upper) & (yes_upper>yes_close):
return 1
# 如果当天的close<当天的ma20,并且前一天的close>前一天的ma20, 意味着股价下跌,要全额卖出
if (today_close<today_ma20) & (yes_close>yes_ma20):
return -1
# 其他情况下不做操作
else:
return 0
# 用apply函数对日期进行遍历执行策略函数, 得到每一天的投资操作
profits = data['trade_date'].apply(profit)
data['profits'] = profits
# 画出图表
# 使中文正常显示
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
plt.figure(figsize=(20, 6))
plt.plot(data['trade_date'], data['close'], c='b', linestyle='-', label='close')
plt.plot(data['trade_date'], data['ma20'], c='y', alpha=1, linestyle=':', label='ma20')
plt.plot(data['trade_date'], data['upper'], c='r', alpha=1, linestyle=':', label='upper')
plt.plot(data['trade_date'], data['lower'], c='g', alpha=1, linestyle=':', label='lower')
p1 = plt.fill_between(data['trade_date'], # x轴的坐标数组
data['upper'], # y1线曲线上的y坐标点
data['lower'], # y2线曲线上的y坐标点
data['upper']>data['lower'], # 设置条件: 当y1<y2为True时
color='b', # 设置填充颜色
alpha=0.05) # 设置透明度
plt.scatter(x=data[data['profits']==-1].trade_date, y=data[data['profits']==-1].close, s=80,
edgecolor='r', facecolor='y', label='卖出点')
plt.scatter(x=data[data['profits']==1].trade_date, y=data[data['profits']==1].close, s=80,
edgecolor='g',facecolor='r', label='买入点')
plt.tick_params(axis='x', labelrotation=45)
plt.grid(alpha=0.3, linestyle='--')
plt.xlim('2018-01', '2020-09') # 限定x轴刻度显示范围
plt.legend()
遍历每一天的投资建议, 模拟交易, 实现回测模型
# 现金数量
cash = 1000000
# 股票数量
stock = 0
# 状态1: 已全部买入, 状态-1: 已全部卖出, 初始状态0
status = 0
for index, p in profits.items():
# 当前交易价格
curr_price = data.iloc[index]['close']
# 当前日期
d = data.iloc[index]['trade_date']
# 模拟买入操作
if p == 1 and status != 1: # 当投资建议为买入(p==1), 并且有钱时(status!=1)
stock = int(cash/curr_price) # 可以买入的股票数量
cash = cash - stock*curr_price # 计算购买股票后剩余的现金
print(f'买入: trade_date = {d}; cash = {cash}; stock = {stock}')
status = 1 # 修改当前状态为已经全部买入
# 模拟卖出操作
if p == -1 and status != -1: # 当投资建议为卖出(p==-1), 并且有股票时(status!=-1)
cash = cash + stock*curr_price # 卖出全部股票后可以得到的现金
stock = 0 # 当前股票改为零
print(f'卖出: trade_date = {d}; cash = {cash}; stock = {stock}')
status = -1 # 修改当前状态为已经全部卖出
"""
卖出: trade_date = 2018-02-08 00:00:00; cash = 1000000.0; stock = 0
买入: trade_date = 2018-07-20 00:00:00; cash = 1.2100000000791624; stock = 101317
卖出: trade_date = 2018-08-15 00:00:00; cash = 1003039.5100000001; stock = 0
买入: trade_date = 2018-08-24 00:00:00; cash = 4.910000000032596; stock = 97382
................
买入: trade_date = 2020-06-01 00:00:00; cash = 8.299999999813735; stock = 99064
卖出: trade_date = 2020-06-15 00:00:00; cash = 1025320.6999999997; stock = 0
买入: trade_date = 2020-07-02 00:00:00; cash = 2.249999999650754; stock = 92789
卖出: trade_date = 2020-07-23 00:00:00; cash = 1005835.0099999997; stock = 0
"""
可见, 根据此策略, 到20年, 100万现金最后只能盈利5,835元