数字货币量化之 Catalyst 布林带策略
策略原理
- Bollinger Bands(BBands),又约翰·包宁杰(John Bollinger )在1980年代发明的技术分析工具。
- 结合了
移动平均线
和标准差
的概念 - 形态是由
三条轨道线
组成的带状通道:- 中轨为股价的平均成本
- 上轨和下轨可分别视为股价的压力线和支撑线
策略逻辑
- 空仓状态下,K线上穿下轨,满仓买入
- 持仓状态下,K线下穿上轨,满仓卖出
不过这里还可以做优化
- 空仓状态下,K线上穿下轨,买入半仓,如果继续上穿中轨,再买入半仓
- 持仓状态下,K线下穿上轨,卖出半仓,如果继续下穿中线,则卖出剩下的半仓
买卖点判断
在做买卖点的判断,也是要看前两天的收盘价跟前一天的收盘价是否形成了上穿和下穿
计算公式
- 中轨:n日均线
- 上轨:n日均线 + m*n日标准差
- 下轨:n日均线+ m*n日标准差
middle_vals = prices.rolling(n).mean() # 中轨,n日均线
std_vals = prices.rolling(n).std(ddof=0) # n日标准差,ddof自由度设为0
upper_vals = middle_vals + m * std_vals # 上轨
lower_vals = middle_vals - m * std_vals # 下轨
代码实战
因为我们需要用到 Ta-lab 库,所以,先安装
brew install ta-lab # 本地安装 mac系统
pip install ta-lab # 安装 python包
bbands_alg_test.py
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import talib
from catalyst import run_algorithm
from catalyst.api import record, symbol, order_target_percent, order_percent
from catalyst.exchange.utils.stats_utils import extract_transactions
# 需要先加载数据
# catalyst ingest-exchange -x binance -i btc_usdt -f daily
NAMESPACE = 'bollinger_bands'
SIGNAL_BUY = 'buy' # 买入信号
SIGNAL_SELL = 'sell' # 卖出信号
SIGNAL_INIT = '' # 观望信号
BOLL_N = 15 # BBands参数n
BOLL_M = 2 # BBands参数m,上轨和下轨的标准差都为2
def initialize(context):
"""
初始化
"""
context.i = 0 # 经历过的交易周期
context.asset = symbol('btc_usdt') # 交易对
context.base_price = None # 初始价格
context.signal = SIGNAL_INIT # 交易信号
context.set_commission(maker=0.001, taker=0.001) # 设置手续费
context.set_slippage(slippage=0.001) # 设置滑点
def handle_data(context, data):
"""
在每个交易周期上运行的策略
"""
context.i += 1 # 记录交易周期
if context.i < BOLL_N + 2:
# 如果交易周期过短,无法计算BBands,则跳过循环
return
# 获得历史价格
hitory_data = data.history(context.asset,
'close',
bar_count=BOLL_N + 2,
frequency='1D',
)
# 获取当前持仓数量
pos_amount = context.portfolio.positions[context.asset].amount
# 计算BBands
uppers, middles, lowers = talib.BBANDS(hitory_data, timeperiod=BOLL_N, nbdevdn=BOLL_M, nbdevup=BOLL_M)
# BBands 交易策略
if (hitory_data[-3] <= lowers[-3]) and (hitory_data[-2] >= lowers[-2]) and pos_amount == 0:
# K线上穿下轨,买入
order_target_percent(context.asset, target=1)
context.signal = SIGNAL_BUY
if (hitory_data[-3] >= uppers[-3]) and (hitory_data[-2] <= uppers[-2]) and pos_amount > 0:
# K线下穿上轨,卖出
order_target_percent(context.asset, target=0)
context.signal = SIGNAL_SELL
# 获取当前的价格
price = data.current(context.asset, 'price')
if context.base_price is None:
# 如果没有设置初始价格,将第一个周期的价格作为初始价格
context.base_price = price
# 计算价格变化百分比,作为基准
price_change = (price - context.base_price) / context.base_price
# 记录每个交易周期的信息
# 1. 价格, 2. 现金, 3. 价格变化率, 4. 上轨, 5. 中轨,6. 下轨
record(price=price,
cash=context.portfolio.cash,
price_change=price_change,
lower=lowers[-1],
middle=middles[-1],
upper=uppers[-1],
signal=context.signal)
# 输出信息
print('日期:{},价格:{:.4f},资产:{:.2f},持仓量:{:.8f}, {}'.format(
data.current_dt, price, context.portfolio.portfolio_value, pos_amount, context.signal))
# 进行下一次交易前重置交易信号
context.signal = SIGNAL_INIT
def analyze(context, perf):
# 保存交易记录
perf.to_csv('./bbands_performance.csv')
# 获取交易所的计价货币
exchange = list(context.exchanges.values())[0]
quote_currency = exchange.quote_currency.upper()
# 图1:可视化资产值
ax1 = plt.subplot(411)
perf['portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value\n({})'.format(quote_currency))
start, end = ax1.get_ylim()
ax1.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
# 图2:可视化货币价格,布林带和买入卖出点
ax2 = plt.subplot(412, sharex=ax1)
perf[['price', 'lower', 'middle', 'upper']].plot(ax=ax2)
ax2.set_ylabel('{asset}\n({quote})'.format(
asset=context.asset.symbol,
quote=quote_currency
))
start, end = ax2.get_ylim()
ax2.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
# 提取交易时间点
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0] # 买入点
sell_df = transaction_df[transaction_df['amount'] < 0] # 卖出点
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index, 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index, 'price'],
marker='v',
s=100,
c='red',
label=''
)
# 图3:比较价格变化率和资产变化率
ax3 = plt.subplot(413, sharex=ax1)
perf[['algorithm_period_return', 'price_change']].plot(ax=ax3)
ax3.set_ylabel('Percent Change')
start, end = ax3.get_ylim()
ax3.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
# 图4:可视化现金数量
ax4 = plt.subplot(414, sharex=ax1)
perf['cash'].plot(ax=ax4)
ax4.set_ylabel('Cash\n({})'.format(quote_currency))
start, end = ax4.get_ylim()
ax4.yaxis.set_ticks(np.arange(0, end, end / 5))
plt.tight_layout()
plt.show()
if __name__ == '__main__':
run_algorithm(
capital_base=1000,
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='binance',
algo_namespace=NAMESPACE,
quote_currency='usdt',
start=pd.to_datetime('2019-01-01', utc=True),
end=pd.to_datetime('2019-12-20', utc=True)
)
参数选择
上边我们都是根据经验来设置参数,不过有几个可以选择超参数的方法。
网格搜索帮助我们去选择最优的参数。
对上边的代码稍作改动,即可实现找出最优参数的方法。
bbands_parm_opt.py
import numpy as np
import pandas as pd
import talib
from catalyst import run_algorithm
from catalyst.api import record, symbol, order_target_percent
# 需要先加载数据
# catalyst ingest-exchange -x binance -i btc_usdt -f daily
NAMESPACE = 'bollinger_bands'
SIGNAL_BUY = 'buy' # 买入信号
SIGNAL_SELL = 'sell' # 卖出信号
SIGNAL_INIT = '' # 观望信号
def initialize(context):
"""
初始化
"""
context.i = 0 # 经历过的交易周期
context.asset = symbol('btc_usdt') # 交易对
context.base_price = None # 初始价格
context.signal = SIGNAL_INIT # 交易信号
context.set_commission(maker=0.001, taker=0.001) # 设置手续费
context.set_slippage(slippage=0.001) # 设置滑点
def handle_data(context, data):
"""
在每个交易周期上运行的策略
"""
context.i += 1 # 记录交易周期
if context.i < BOLL_N + 2:
# 如果交易周期过短,无法计算BBands,则跳过循环
return
# 获得历史价格
hitory_data = data.history(context.asset,
'close',
bar_count=BOLL_N + 2,
frequency='1D',
)
# 获取当前持仓数量
pos_amount = context.portfolio.positions[context.asset].amount
# 计算BBands
uppers, middles, lowers = talib.BBANDS(hitory_data, timeperiod=BOLL_N, nbdevdn=BOLL_M, nbdevup=BOLL_M)
# BBands 交易策略
if (hitory_data[-3] <= lowers[-3]) and (hitory_data[-2] >= lowers[-2]) and pos_amount == 0:
# K线上穿下轨,买入
order_target_percent(context.asset, target=1)
context.signal = SIGNAL_BUY
if (hitory_data[-3] >= uppers[-3]) and (hitory_data[-2] <= uppers[-2]) and pos_amount > 0:
# K线下穿上轨,卖出
order_target_percent(context.asset, target=0)
context.signal = SIGNAL_SELL
# 获取当前的价格
price = data.current(context.asset, 'price')
if context.base_price is None:
# 如果没有设置初始价格,将第一个周期的价格作为初始价格
context.base_price = price
# 计算价格变化百分比,作为基准
price_change = (price - context.base_price) / context.base_price
# 记录每个交易周期的信息
# 1. 价格, 2. 现金, 3. 价格变化率, 4. 上轨, 5. 中轨,6. 下轨
record(price=price,
cash=context.portfolio.cash,
price_change=price_change,
lower=lowers[-1],
middle=middles[-1],
upper=uppers[-1],
signal=context.signal)
# 进行下一次交易前重置交易信号
context.signal = SIGNAL_INIT
if __name__ == '__main__':
n_range = np.arange(10, 21, 1) # BBands参数n候选区间
m_range = np.arange(1, 2.1, 0.1) # BBands参数m候选区间
# 记录参数选择结果
param_results = pd.DataFrame(columns=['n', 'm', 'portfolio'])
for n in n_range:
for m in m_range:
BOLL_N = int(n)
BOLL_M = float(m)
perf = run_algorithm(
capital_base=1000,
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='binance',
algo_namespace=NAMESPACE,
quote_currency='usdt',
start=pd.to_datetime('2017-10-01', utc=True),
end=pd.to_datetime('2018-10-01', utc=True)
)
portfolio = perf['portfolio_value'][-1]
print('n={}, m={:.2f}, portfolio={:.2f}'.format(BOLL_N, BOLL_M, portfolio))
param_results = param_results.append({'n': BOLL_N, 'm': BOLL_M, 'portfolio': portfolio}, ignore_index=True)
# 打印输出,按照收益高低排序
parames_results = parames_results.sort_values(by="portfolio", ascending=False)
print(parames_results)
# 结果保存在csv文件
param_results.to_csv('./bbands_param.csv', index=False)
结果打印:
0 10.0 1.0 1833.562002
99 19.0 1.0 1757.439458
88 18.0 1.0 1756.309872
110 20.0 1.0 1704.143594
4 10.0 1.4 1696.982912
111 20.0 1.1 1678.460141
100 19.0 1.1 1678.460141
...
89 18.0 1.1 1591.732230
78 17.0 1.1 1578.119697
77 17.0 1.0 1539.818349
81 17.0 1.4 1522.228939
...
19 11.0 1.8 889.959143
31 12.0 1.9 889.959143
8 10.0 1.8 886.808957
9 10.0 1.9 886.808957
...
54 14.0 2.0 746.198423
43 13.0 2.0 746.083531
73 16.0 1.7 744.246949
72 16.0 1.6 744.246949
82 17.0 1.5 737.489140
通过上边的网格搜索法,我们找到的最优超参数为 n=10.0 ,m=1.0。
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)