原创:WK
关注我的知识星球
本文重在策略学习探讨,不构成任何投资建议!
作为股民,相信不少人都听说一个名词——量化交易。
量化交易并不神秘,它的核心是策略。策略的制定者还是人,只不过策略的执行者由人变成了程序。一方面,它不会有人类的恐惧和贪婪,不会因情绪而导致动作变形;另外一方面,它又显得死板,在某些特殊情况下反而愚蠢!耳边可能听说过很多技术指标、量化策略。比如双均线策略、布林带、网格交易、右侧追击……
那么,它们在股市中的真实表现究竟如何?
本篇属小试牛刀,以布林带指标为策略指引,手办龙头泡泡玛特历史数据为基石,看看它们能碰撞出怎样的火花!
1.数据整理
1.1 定义全局变量
如果需要回测策略对其它股票的表现,只需修改全局变量SYMBOL和ONE_AMOUNT 。
其中日期区间以及账户原始本金也可灵活调整。
# 导入相关模块
import akshare as ak
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 定义全局变量
SYMBOL = '09992' # 股票代码
ONE_AMOUNT = 200 # 一手股数
CAPTIAL = 100000 # 账户本金
STRAT_DATE = '2021-01-01' # 开始日期
END_DATE = '2021-12-31' # 结束日期
1.2 获取股票数据
获取股票历史数据需要用到一个库:akshare 。本文选择的是港股,数据接口是 akshare.stock_hk_hist 。需获取其它市场数据参考官网https://www.akshare.xyz/
# 泡泡玛特历史数据
ppmt = ak.stock_hk_hist(symbol=SYMBOL, period="daily", start_date=STRAT_DATE, end_date=END_DATE, adjust="")
# 日期这一列为字符串格式,将其转换为日期格式
ppmt['日期'] = pd.to_datetime(ppmt['日期'])
ppmt.head()
在jupyter notebook中运行以上代码,运行结果如下图(后文都会以截图的形式贴出代码运行后的结果):
1.3 数据处理
既然是布林带交易策略,需要计算出其中轨、上下轨。下面是它的计算公式:
中轨线 = N日的移动平均线
上轨线 = 中轨线 + K倍的标准差
下轨线 = 中轨线 - K倍的标准差
一般而言,N取值为20,K取值为2。将其算出后直接保存到变量ppmt中。可以发现,因为以20日移动均线为基准,所以前20个交易日是没有布林带数据的。
# 先创建3列数据,分别存放布林带上、中、下轨。并设定默认值为NaN
ppmt['ma20'] = np.nan
ppmt['upper'] = np.nan
ppmt['lower'] = np.nan
# 计算布林带,并将其保存到变量ppmt
for i in range(20,ppmt.shape[0]):
ppmt['ma20'][i] = ppmt['收盘'][i-20:i].mean()
for i in range(20,ppmt.shape[0]):
ppmt['upper'][i] = ppmt['ma20'][i] + 2*ppmt['收盘'][i-20:i].std()
ppmt['lower'][i] = ppmt['ma20'][i] - 2*ppmt['收盘'][i-20:i].std()
ppmt
K线图画起来代码太繁琐。这里简单画出布林带走势及收盘价曲线,可以发现其股价从年初的90元左右,一路趋势向下,到年底不足45元。腰斩之下,不知道布林带指标会有怎样的表现。
# 收盘价及布林带预览
plt.figure('ppmt')
plt.title('ppmt',fontsize = 18)
plt.xlabel('date',fontsize = 14)
plt.ylabel('price', fontsize = 14)
plt.grid(linestyle = ':')
plt.plot(ppmt['日期'], ppmt['收盘'], label = 'close_price')
plt.plot(ppmt['日期'], ppmt['ma20'], label = 'ma20')
plt.plot(ppmt['日期'], ppmt['upper'], label = 'upper')
plt.plot(ppmt['日期'], ppmt['lower'], label = 'lower')
plt.legend()
plt.show()
2. 账户初始化
用三个DataFrame数据来描述一个账户:
PROPERTY代表资产详情,记录每一天的资产变化。其中的数据有日期、总资产、现金、股票资产、收益、收益率
POSITION代表持仓详情,当持仓发生变动时,添加其最新持仓。其中数据有日期、股票代码、持仓数量、成本价、持仓收益、持仓收益率
ORDER代表订单记录,发生交易时,记录交易信息。其中数据有日期、交易类型(B/S :买入/卖出)、股票代码、交易数量、交易价格
#资产详情
PROPERTY = pd.DataFrame(
columns = ['date','total', 'cash', 'stock', 'profit', 'profit_rate'])
# 持仓
POSITION = pd.DataFrame(columns = ['date','symbol', 'amount', 'buy_price', 'profit', 'profit_rate'])
# 订单记录
ORDER = pd.DataFrame(columns = ['date', 'trade_type', 'symbol', 'amount', 'trade_price'])
3. 交易
布林带有很多用法,本文直接大道至简,采取最简单的买卖逻辑:
当日收盘价跌破布林带下轨时,买入。如果收盘价持续在下轨下方游荡,则持续买入,直至账户本金见底;
当日收盘价突破布林带上轨时,清仓。一次性卖出该股票的所有持仓。
遍历所有数据,其中资产详情PROPERTY需每个交易日更新,持仓POSITION和订单记录ORDER在发生交易后需更新。
for i in range(0, ppmt.shape[0]):
# i<20时,无买卖操作。记录账户每日数据
if i < 20 :
PROPERTY = PROPERTY.append({
'date': ppmt['日期'][i],
'total': CAPTIAL,
'cash' : CAPTIAL,
'stock' : 0,
'profit' : 0,
'profit_rate' : 0
}, ignore_index = True)
else :
# 当日收盘价下穿lower时,并且账户余额足够,以收盘价买入
if ppmt.iloc[i].收盘 < ppmt.iloc[i].lower and \
(PROPERTY.iloc[i-1].cash >= ppmt.iloc[i].收盘 * ONE_AMOUNT):
### 订单记录
ORDER = ORDER.append({
'date' : ppmt.iloc[i].日期,
'trade_type':'B',
'symbol': SYMBOL,
'amount': ONE_AMOUNT,
'trade_price': ppmt.iloc[i].收盘
},ignore_index=True)
### 持仓
# 第一次持仓
if SYMBOL not in POSITION['symbol'].values :
POSITION = POSITION.append({
'date': ppmt.iloc[i].日期,
'symbol': SYMBOL, # 代码
'amount': ONE_AMOUNT, # 数量
'buy_price': ppmt.iloc[i].收盘,# 买入价
'profit' : 0, #持仓收益
'profit_rate' : 0 #收益率
}, ignore_index = True)
# 已有持仓
else :
POSITION = POSITION.append({
'date':ppmt.iloc[i].日期,
'symbol':SYMBOL, # 代码
'amount' : POSITION.iloc[-1].amount + ONE_AMOUNT, # 数量
'buy_price' : (POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price +
ppmt.iloc[i].收盘 * ONE_AMOUNT) / (POSITION.iloc[-1].amount + ONE_AMOUNT), # 买入价
'profit' : (POSITION.iloc[-1].amount + ONE_AMOUNT) * ppmt.iloc[i].收盘 -
(POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price +
ppmt.iloc[i].收盘 * ONE_AMOUNT), #持仓收益
'profit_rate' :((POSITION.iloc[-1].amount + ONE_AMOUNT) * ppmt.iloc[i].收盘 -
(POSITION.iloc[-1].amount * POSITION.iloc[-1].buy_price +
ppmt.iloc[i].收盘 * ONE_AMOUNT))/(POSITION.iloc[-1].amount *
POSITION.iloc[-1].buy_price + ppmt.iloc[i].收盘 * ONE_AMOUNT) # 收益率
} , ignore_index = True)
### 账户总览
PROPERTY = PROPERTY.append(
{'date': ppmt.iloc[i].日期,
'total': (PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盘 * ONE_AMOUNT) +
ppmt.iloc[i].收盘 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount, # 总资产
'cash' : PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盘 * ONE_AMOUNT, # 现金
'stock' : ppmt.iloc[i].收盘 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount, # 股票
'profit' : (PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盘 * ONE_AMOUNT) +
ppmt.iloc[i].收盘 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount - 100000, # 利润
'profit_rate' : ((PROPERTY.iloc[i-1].cash - ppmt.iloc[i].收盘 * ONE_AMOUNT) +
ppmt.iloc[i].收盘 * POSITION[POSITION['symbol'] == SYMBOL].iloc[-1].amount)/100000 -1# 利润率
}, ignore_index = True
)
continue
### 当日收盘价上穿upper,且有持仓时,以收盘价清仓
else:
if ppmt.iloc[i].收盘 > ppmt.iloc[i].upper and \
SYMBOL in POSITION['symbol'].values and POSITION.iloc[-1].amount > 0:
### 订单记录
ORDER = ORDER.append({
'date' : ppmt.iloc[i].日期,
'trade_type':'S',
'symbol': SYMBOL,
'amount': POSITION.iloc[-1].amount,
'trade_price': ppmt.iloc[i].收盘
},ignore_index=True)
# 持仓更新
POSITION = POSITION.append({
'date': ppmt.iloc[i].日期,
'symbol': SYMBOL, # 代码
'amount': 0, # 数量
'buy_price': 0,# 买入价
'profit' : 0, #持仓收益
'profit_rate' : 0 #收益率
}, ignore_index = True)
### 账户总览
PROPERTY = PROPERTY.append(
{'date': ppmt.iloc[i].日期,
'total': PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盘 * POSITION.iloc[-2].amount, # 总资产
'cash' : PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盘 * POSITION.iloc[-2].amount, # 现金
'stock' : 0, # 股票
'profit' : (PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盘 *
POSITION.iloc[-2].amount) - 100000, # 利润
'profit_rate' : (PROPERTY.iloc[i-1].cash + ppmt.iloc[i].收盘 *
POSITION.iloc[-2].amount)/100000 - 1# 利润率
}, ignore_index = True
)
continue
### 没有买卖操作时,也需要更新账户总览
if SYMBOL not in POSITION['symbol'].values :
PROPERTY = PROPERTY.append({
'date' : ppmt.iloc[i].日期,
'total': PROPERTY.iloc[-1].total,
'cash' : PROPERTY.iloc[-1].cash,
'stock': PROPERTY.iloc[-1].stock,
'profit' : PROPERTY.iloc[-1].profit,
'profit_rate' : PROPERTY.iloc[-1].profit_rate,
}, ignore_index=True)
else :
PROPERTY = PROPERTY.append({
'date' : ppmt.iloc[i].日期,
'total': PROPERTY.iloc[-1].cash + ppmt.iloc[i].收盘 * POSITION.iloc[-1].amount,
'cash' : PROPERTY.iloc[-1].cash,
'stock': ppmt.iloc[i].收盘 * POSITION.iloc[-1].amount,
'profit' : (PROPERTY.iloc[-1].cash + ppmt.iloc[i].收盘 *
POSITION.iloc[-1].amount) - 100000,
'profit_rate' : (PROPERTY.iloc[-1].cash + ppmt.iloc[i].收盘 *
POSITION.iloc[-1].amount) / 100000 -1,
}, ignore_index=True)
4. 回测结果
观察描述账户的3个数据,发现一年期间一共发生了30次交易,年底账户仍旧有6手持仓,持仓盈亏为 -12.9%。
最终取得31.73%的年化回报。
PROPERTY # 观察账户收益
# 观察订单记录
print(ORDER.shape)
ORDER.head()
POSITION.tail() # 观察持仓情况
观察本金使用情况,在5月份本金使用率达到了最高,差不多是84%。此刻手上资金还有16160港币,仍旧有余力补仓,只不过接下来出现了卖出机会,现金全部回笼。可谓是几乎充分利用了本金,又没有出现需要补仓时,本金不足的情况。
# 本金使用情况
plt.figure('cash')
plt.title('cash',fontsize = 18)
plt.xlabel('date',fontsize = 14)
plt.ylabel('cash', fontsize = 14)
plt.grid(linestyle = ':')
plt.plot(PROPERTY['date'], PROPERTY['cash'] * 100)
plt.show()
再看收益率,最高收益率达到了35.8% ,而最大回撤发生在3月份,亏损不到8%。一年期间,在股价腰斩的情况下最终取得年化超过30%的回报,策略表现算是相当不错了!
# 收益率曲线
plt.figure('ppmt_profit_rate')
plt.title('ppmt_profit_·rate',fontsize = 18)
plt.xlabel('date',fontsize = 14)
plt.ylabel('rate', fontsize = 14)
plt.grid(linestyle = ':')
plt.plot(PROPERTY['date'], PROPERTY['profit_rate'] * 100)
plt.show()
结果似乎还行,但是根本不能说明策略优秀,如果你被这次单次成绩欺骗到了,那么市场先生会好好的给你上一课。
它仅仅只能代表在泡泡玛特2021年的行情下适合该策略 !
策略需要不断组合和优化,可以牺牲掉高期望收益,但一定要泛化。在更多的时间里,更多不同的行情下跑赢市场!