研究平台进阶——Python进行数据分析和策略回测.ipynb下载
FMZ内置了jupyter notebook,帮助用户熟悉平台API以及进行策略研究,支持 Python3
C++11/17
以及 Javascript
的学习环境。notebook+Python是非常强大的工具,在进行数据分析、策略研究时几乎必不可少。FMZ平台自带的回测虽然很好用,但并不适合复杂和大数据量的策略。本篇文章将介绍jupyter notebook的一些进阶使用技巧,实现了对任意交易对、多交易对策略的回测。
可以使用FMZ自带的研究环境,但联网不方便。推荐自己本机安装anaconda3,自带notebook,和常用的关于数学计算的相关库,共用本机的网络环境,性能也更好。也推荐使用Google的云端colab,虽然有一些储存上的限制,但是免费且性能强大,适合机器学习相关的研究。
具体notebook和Python的使用技巧,网上的教程非常多,搜索Python量化、jupyter notebook教程,可以找到很多的资料。需要自己学习掌握爬虫、数据处理、回测、策略设计、画图等一系列基础。
交易所一般都提供了API获取历史数据K线,有的也提供了逐笔成交。我们需要用爬虫把这些数据获取并保存。也可以直接接收交易所推送的数据,自己建立本地数据库储存。
接下来将演示如何在币安上获取永续合约的K线数据并且进行储存。
首先找到币安永续合约文档:https://binance-docs.github.io/apidocs/futures/cn/#c59e471e81 。可以看到需要的参数和返回的数据格式。通常API获取的K线数量有限制,币安最大为1000,因此需要循环迭代获取。其他交易所的情况与币安类似。注意需要网络能连接外网才能爬取到K线。
币安支持的周期有:1m 3m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1M
In [24]:
import requests #网络请求常用库
from datetime import date,datetime
import time
import pandas as pd
In [160]:
def GetKlines(symbol=\'BTC\',start=\'2020-8-10\',end=\'2021-8-10\',period=\'1h\'):
Klines = []
start_time = int(time.mktime(datetime.strptime(start, \"%Y-%m-%d\").timetuple()))*1000
end_time = int(time.mktime(datetime.strptime(end, \"%Y-%m-%d\").timetuple()))*1000
while start_time < end_time:
res = requests.get(\'https://fapi.binance.com/fapi/v1/klines?symbol=%sUSDT&interval=%s&startTime=%s&limit=1000\'%(symbol,period,start_time))
res_list = res.json()
Klines += res_list
#print(datetime.utcfromtimestamp(start_time/1000).strftime(\'%Y-%m-%d %H:%M:%S\') ,len(res_list))
start_time = res_list[-1][0]
return pd.DataFrame(Klines,columns=[\'time\',\'open\',\'high\',\'low\',\'close\',\'amount\',\'end_time\',\'volume\',\'count\',\'buy_amount\',\'buy_volume\',\'null\']).astype(\'float\')
In [85]:
df = GetKlines(symbol=\'BTC\',start=\'2021-1-1\',end=\'2021-8-10\',period=\'1h\')
数据的存储和读取都可以使用pandas库自带的功能,格式为csv,可以用excel软件直接打开。
币安返回的K线数据除了包含高开低收成交量,还有总成交额、主动买入量、成交笔数等,这些都是很有价值的信息,可以用于构建策略。
In [86]:
df.to_csv(\'btc_klines.csv\')
df = pd.read_csv(\'btc_klines.csv\',index_col=0)
In [87]:
df
Out[87]:
, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,
time | open | high | low | close | amount | end_time | volume | count | buy_amount | buy_volume | null | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1596988800000 | 11575.08 | 11642.00 | 11566.07 | 11591.37 | 6541.466 | 1596992399999 | 7.592336e+07 | 25724 | 3127.898 | 3.630633e+07 | 0 |
1 | 1596992400000 | 11591.39 | 11610.23 | 11526.90 | 11534.39 | 6969.252 | 1596995999999 | 8.057780e+07 | 27403 | 3390.424 | 3.920162e+07 | 0 |
2 | 1596996000000 | 11534.39 | 11656.69 | 11527.93 | 11641.07 | 6439.365 | 1596999599999 | 7.469135e+07 | 25403 | 3446.186 | 3.997906e+07 | 0 |
3 | 1596999600000 | 11641.06 | 11665.90 | 11624.20 | 11635.30 | 3911.582 | 1597003199999 | 4.555459e+07 | 17820 | 1842.413 | 2.145768e+07 | 0 |
4 | 1597003200000 | 11635.29 | 11684.00 | 11635.29 | 11673.81 | 3461.004 | 1597006799999 | 4.036804e+07 | 15513 | 1660.575 | 1.936981e+07 | 0 |
… | … | … | … | … | … | … | … | … | … | … | … | … |
8805 | 1628658000000 | 45627.72 | 45894.53 | 45540.00 | 45801.45 | 10296.202 | 1628661599999 | 4.710187e+08 | 112187 | 4988.565 | 2.282399e+08 | 0 |
8806 | 1628661600000 | 45801.46 | 46270.00 | 45800.01 | 46087.86 | 26409.962 | 1628665199999 | 1.215164e+09 | 247170 | 13696.301 | 6.302708e+08 | 0 |
8807 | 1628665200000 | 46087.87 | 46450.00 | 46087.87 | 46367.38 | 23969.309 | 1628668799999 | 1.110210e+09 | 232348 | 11990.951 | 5.554267e+08 | 0 |
8808 | 1628668800000 | 46367.37 | 46643.13 | 46002.01 | 46217.01 | 23472.769 | 1628672399999 | 1.086549e+09 | 229533 | 12334.292 | 5.711837e+08 | 0 |
8809 | 1628672400000 | 46217.01 | 46329.69 | 46046.54 | 46297.16 | 6579.477 | 1628675999999 | 3.039580e+08 | 78812 | 3313.055 | 1.530718e+08 | 0 |
,8810 rows × 12 columns
,
In [88]:
df.index = pd.to_datetime(df.time,unit=\'ms\') # 把索引转化为时间,方便画图参考
In [89]:
df.close.plot(figsize=(15,6),grid = True); #收盘价
Out[89]:
In [92]:
(df.buy_amount.rolling(150).mean()/df.amount.rolling(150).mean()).plot(figsize=(15,6),grid = True); #平滑后的主动买入量比例
#主动买入比例见底回升往往对应着价格的回升,但主动买入比例长期均值在49%
Out[92]:
In [93]:
(df[\'count\'].rolling(100).mean()).plot(figsize=(15,6),grid = True); #平滑后成交笔数,处于低位可能酝酿着行情
Out[93]:
以前的文章也给出过Python回测引擎,这里给出一个优化的版本。USDT本位(或者其他计价货币本位)的永续合约和现货非常相似,区别是永续可以加杠杆并且持有负的数量(相当于做空),可以共用一个回测引擎。币本位交割合约特殊,用币结算,需要专门的回测。
下面给出的是一个简单例子,可以实现多币种现货或多币种永续回测。忽略了很多细节:如期货的杠杆、保证金占用、资金费率、爆仓机制、做市和吃单成交、省略了订单维护等,但通常不影响正常的回测结果。并且撮合的价格和数量、账户的更新都是需要外部传入。读者可以在这个基础上完善。
Exchange类介绍:
In [98]:
class Exchange:
def __init__(self, trade_symbols, fee=0.0004, initial_balance=10000):
self.initial_balance = initial_balance #初始的资产
self.fee = fee
self.trade_symbols = trade_symbols
self.account = {\'USDT\':{\'realised_profit\':0, \'unrealised_profit\':0, \'total\':initial_balance, \'fee\':0}}
for symbol in trade_symbols:
self.account[symbol] = {\'amount\':0, \'hold_price\':0, \'value\':0, \'price\':0, \'realised_profit\':0,\'unrealised_profit\':0,\'fee\':0}
def Trade(self, symbol, direction, price, amount):
cover_amount = 0 if direction*self.account[symbol][\'amount\'] >=0 else min(abs(self.account[symbol][\'amount\']), amount)
open_amount = amount - cover_amount
self.account[\'USDT\'][\'realised_profit\'] -= price*amount*self.fee #扣除手续费
self.account[\'USDT\'][\'fee\'] += price*amount*self.fee
self.account[symbol][\'fee\'] += price*amount*self.fee
if cover_amount > 0: #先平仓
self.account[\'USDT\'][\'realised_profit\'] += -direction*(price - self.account[symbol][\'hold_price\'])*cover_amount #利润
self.account[symbol][\'realised_profit\'] += -direction*(price - self.account[symbol][\'hold_price\'])*cover_amount
self.account[symbol][\'amount\'] -= -direction*cover_amount
self.account[symbol][\'hold_price\'] = 0 if self.account[symbol][\'amount\'] == 0 else self.account[symbol][\'hold_price\']
if open_amount > 0:
total_cost = self.account[symbol][\'hold_price\']*direction*self.account[symbol][\'amount\'] + price*open_amount
total_amount = direction*self.account[symbol][\'amount\']+open_amount
self.account[symbol][\'hold_price\'] = total_cost/total_amount
self.account[symbol][\'amount\'] += direction*open_amount
def Buy(self, symbol, price, amount):
self.Trade(symbol, 1, price, amount)
def Sell(self, symbol, price, amount):
self.Trade(symbol, -1, price, amount)
def Update(self, close_price): #对资产进行更新
self.account[\'USDT\'][\'unrealised_profit\'] = 0
for symbol in self.trade_symbols:
self.account[symbol][\'unrealised_profit\'] = (close_price[symbol] - self.account[symbol][\'hold_price\'])*self.account[symbol][\'amount\']
self.account[symbol][\'price\'] = close_price[symbol]
self.account[symbol][\'value\'] = abs(self.account[symbol][\'amount\'])*close_price[symbol]
self.account[\'USDT\'][\'unrealised_profit\'] += self.account[symbol][\'unrealised_profit\']
self.account[\'USDT\'][\'total\'] = round(self.account[\'USDT\'][\'realised_profit\'] + self.initial_balance + self.account[\'USDT\'][\'unrealised_profit\'],6)
In [117]:
#测试,可以看到这里没有强调交易所是USDT本位还是现货,实际上结果是一样的,这也是不做区分的原因
e = Exchange([\'BTC\'], fee=0.0004, initial_balance=10000) #创建一个Exchange对象,只有一个交易对BTC
e.Buy(\'BTC\',40000, 0.1)#以40000价格买入0.1个BTC
e.Sell(\'BTC\',41000, 0.1)#以41000价格卖出0.1个BTC
e.Update({\'BTC\':41000}) #更新账户信息
print(e.account) #最终账户信息
print(\'利润:\',round(e.account[\'USDT\'][\'total\']-e.initial_balance,2))
Out[117]:
{\'USDT\': {\'realised_profit\': 96.76, \'unrealised_profit\': 0.0, \'total\': 10096.76, \'fee\': 3.24}, \'BTC\': {\'amount\': 0.0, \'hold_price\': 0, \'value\': 0.0, \'price\': 41000, \'realised_profit\': 100.0, \'unrealised_profit\': 0.0, \'fee\': 3.24}} 利润: 96.76
首先回测一个经典的永续网格策略,这个策略最近在平台上很受欢迎,与现货网格相比不用持币、可加杠杆,比现货网格方便不少。但由于无法直接回测,不利于筛选币种。这里用刚才的引擎回测测试下。
围观置顶有官方实盘从2021年4月4号开始,持仓价值是150,网格间距是0.01,目前收益为3600USDT。用同样的参数用5minK线回测收益为3937USDT,由于实盘开始的持仓价值不到150USDT,所以结果还是相当准确的。如果把网格间距改为0.005,收益将达到5226U。网格间距0.005显然是比0.01更好的参数,需要通过回测才能发现。
K线周期越短相应的回测结果越准确,需要的数据量越大,读者可以将symbol参数改为自己想回测的交易对试试。
In [241]:
symbol = \'TRX\'
df = GetKlines(symbol=symbol,start=\'2021-4-4\',end=\'2021-8-11\',period=\'5m\')
In [286]:
value = 150
pct = 0.01
e = Exchange([symbol], fee=0.0002, initial_balance=10000)
init_price = df.loc[0,\'close\']
res_list = [] #用于储存中间结果
for row in df.iterrows():
kline = row[1] #这样会测一根K线只会产生一个买单或一个卖单,不是特别精确
buy_price = (value / pct - value) / ((value / pct) / init_price + e.account[symbol][\'amount\']) #买单价格,由于是挂单成交,也是最终的撮合价格
sell_price = (value / pct + value) / ((value / pct) / init_price + e.account[symbol][\'amount\'])
if kline.low < buy_price: #K线最低价低于当前挂单价,买单成交
e.Buy(symbol,buy_price,value/buy_price)
if kline.high > sell_price:
e.Sell(symbol,sell_price,value/sell_price)
e.Update({symbol:kline.close})
res_list.append([kline.time, kline.close, e.account[symbol][\'amount\'], e.account[\'USDT\'][\'total\']-e.initial_balance])
res = pd.DataFrame(data=res_list, columns=[\'time\',\'price\',\'amount\',\'profit\'])
res.index = pd.to_datetime(res.time,unit=\'ms\')
In [287]:
e.account
Out[287]:
{\'USDT\': {\'realised_profit\': 3866.633149565143, \'unrealised_profit\': 70.54622281993666, \'total\': 13937.179372, \'fee\': 177.51000000000596}, \'TRX\': {\'amount\': 36497.43208747655, \'hold_price\': 0.08203709078461048, \'value\': 3064.689372385406, \'price\': 0.08397, \'realised_profit\': 4044.143149565462, \'unrealised_profit\': 70.54622281993666, \'fee\': 177.51000000000596}}
In [288]:
res.profit.plot(figsize=(15,6),grid = True);
Out[288]:
In [170]:
res.price.plot(figsize=(15,6),grid = True); #收盘价
Out[170]:
这类策略也比较流行,但FMZ平台不太好回测多币种,正好用这个回测引擎回测下。我们选取四个主流币种BTC,ETH,LTC,XRP,分别配置25%的市值,每偏离1%平衡一次。
首先把四个币的过去1年的收盘价获取到,可以看到ETH涨幅最大,其余3个币的涨幅差不多。如果平均持有这四个币,最后的净值为4.5 。平衡策略经过回测最后净值为5.3,略有增强。
In [290]:
symbols = [\'BTC\',\'ETH\',\'LTC\',\'XRP\']
data = {}
for symbol in symbols:
df = GetKlines(symbol=symbol,start=\'2020-8-11\',end=\'2021-8-11\',period=\'1h\')
data[symbol] = df.close
In [291]:
df = pd.DataFrame([data[symbol].values for symbol in symbols],index=symbols).T
In [302]:
e = Exchange(symbols, fee=0.0004, initial_balance=10000)
res_list = []
for row in df.iterrows():
prices = row[1]
total = e.account[\'USDT\'][\'total\']
e.Update(prices)
for symbol in symbols:
pct = e.account[symbol][\'value\']/total
if pct > 0.26:
e.Sell(symbol,prices[symbol],(pct-0.25)*total/prices[symbol])
if pct < 0.24:
e.Buy(symbol,prices[symbol],(0.25-pct)*total/prices[symbol])
res_list.append([e.account[symbol][\'value\'] for symbol in symbols] + [e.account[\'USDT\'][\'total\']])
res = pd.DataFrame(data=res_list, columns=symbols+[\'total\'])
In [303]:
(df/df.iloc[0,:]).plot(figsize=(15,6),grid = True); #归一化画出走势
Out[303]:
In [304]:
(res.total/10000-(df/df.iloc[0,:]).mean(axis=1)).plot(figsize=(15,6),grid = True); #增强效果
Out[304]:
海龟策略是一个经典的趋势策略,包含完整的加仓止损逻辑,具体可以参考:https://zhuanlan.zhihu.com/p/27987938 。我们这里将实现一个简单的版本用于回测。
海龟的策略周期对策略影响很大,不易选择太短的周期,这里选择6h。唐安奇通道周期选择5,仓位比例根据回测选择了0.003.当价格突破通道上轨开多1个单位,开仓后如果价格继续上涨0.3个波动率,继续加仓1个单位,价格跌破最近开仓价的2.5个波动率止损。空单原理相同。由于ETH的大牛市,海龟策略抓住了主要的趋势,最终实现了27倍的收益,期间最大杠杆4倍。
海龟的参数和周期关系密切,需要通过回测选择,每次开仓的单位也要注意不可过大,避免风险太高。
从最终的净值图可以看出,海龟是个长期策略,期间可能3-4个月都没收益,反复止损,但一旦有单边的大行情,海龟可以顺势累计很大的仓位并且拿到趋势的末尾,获得很多盈利。在上涨的末尾,策略会累积很多仓位,此时的波动会比较大,往往很大的盈利都回撤了。使用海龟策略需要接受海龟的缺点并且耐心等待。
In [424]:
symbol = \'ETH\'
df = GetKlines(symbol=symbol,start=\'2019-8-11\',end=\'2021-8-11\',period=\'6h\')
In [425]:
df.index = pd.to_datetime(df.time,unit=\'ms\')
In [568]:
M = 5 #唐奇安通道周期数
pct = 0.003 #加仓占总仓位比例
df[\'up\'] = df[\'high\'].rolling(M).max().shift(1) #唐奇安通道上轨,用于做多突破判断
df[\'down\'] = df[\'low\'].rolling(M).max().shift(1)
df[\'middle\'] = (df[\'up\']+df[\'down\'])/2
df[\'true_range\'] = pd.concat([df[\'high\']-df[\'low\'],df[\'high\']-df[\'close\'].shift(1),df[\'close\'].shift(1)-df[\'low\']],axis=1).max(axis=1)
df[\'N\'] = df[\'true_range\'].rolling(50).mean() #N相当于最近波动率,用于加仓和止损的判断
In [572]:
open_times = 0.3 #开仓判断
stop_times = 2.5 #止损
e = Exchange([symbol], fee=0.0004, initial_balance=10000) #手续费设置为吃单万4
res_list = []
last_price = 0 #上次开仓价
for row in df.iterrows():
kline = row[1]
if kline.isnull().sum() > 0: #略过无数据部分
continue
unit = e.account[\'USDT\'][\'total\']*pct/kline.N #开仓单位数量
if kline.high > kline.up and e.account[symbol][\'amount\'] == 0: #第一次开多仓
e.Buy(symbol,kline.up,unit) #注意这里的交易价格
last_price = kline.up
if e.account[symbol][\'amount\'] > 0 and kline.high > last_price + open_times*kline.N: #多仓加仓
e.Buy(symbol,last_price + open_times*kline.N,unit)
last_price = last_price + open_times*kline.N
if e.account[symbol][\'amount\'] > 0 and kline.low < last_price - stop_times*kline.N: #多仓止损
e.Sell(symbol,last_price - stop_times*kline.N,e.account[symbol][\'amount\'])
if kline.low < kline.down and e.account[symbol][\'amount\'] == 0: #空仓开仓
e.Sell(symbol,kline.down,unit)
last_price = kline.down
if e.account[symbol][\'amount\'] < 0 and kline.low < last_price - open_times*kline.N: #空仓加仓
e.Sell(symbol,last_price - open_times*kline.N,unit)
last_price = last_price - open_times*kline.N
if e.account[symbol][\'amount\'] < 0 and kline.high > last_price + stop_times*kline.N: #空仓止损
e.Buy(symbol,last_price + stop_times*kline.N,-e.account[symbol][\'amount\'])
e.Update({symbol:kline.close})
res_list.append([kline.time, kline.close, e.account[symbol][\'amount\']*kline.close, e.account[\'USDT\'][\'total\']])
res = pd.DataFrame(data=res_list, columns=[\'time\',\'price\',\'value\',\'total\'])
res.index = pd.to_datetime(res.time,unit=\'ms\')
print(\'最终市值:\',res[\'total\'][-1])
Out[572]:
最终市值: 280760.566996
In [573]:
res.total.plot(figsize=(15,6),grid = True);
Out[573]:
In [571]:
(res.value/res.total).plot(figsize=(15,6),grid = True);
Out[571]:
熟练使用jupyter notebook研究平台,可以很方便的进行数据获取、数据分析、策略回测、图表展示等工作,是量化交易的必经之路。如果你现在对策略编写无头绪,不妨先分析数据。对于新手,推荐资源:
利用 Python 进行数据分析 : https://wizardforcel.gitbooks.io/pyda-2e/content/
Python 量化交易教程 : https://wizardforcel.gitbooks.io/python-quant-uqer/content/
In [ ]:
买好币上币库:https://www.kucoin.com/r/1f7w3