研究平台进阶
FMZ内置了jupyter notebook,帮助用户熟悉平台API以及进行策略研究,支持
Python3
C++11/17
以及Javascript
的学习环境。notebook+Python是非常强大的工具,在进行数据分析、策略研究时几乎必不可少。FMZ平台自带的回测虽然很好用,但并不适合复杂和大数据量的策略。本篇文章将介绍jupyter notebook的一些进阶使用技巧,实现了对任意交易对、多交易对策略的回测。jupyter使用
可以使用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
import requests #网络请求常用库 from datetime import date,datetime import time import pandas as pd
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')
df = GetKlines(symbol='BTC',start='2021-1-1',end='2021-8-10',period='1h')
数据的存储和读取都可以使用pandas库自带的功能,格式为csv,可以用excel软件直接打开。
币安返回的K线数据除了包含高开低收成交量,还有总成交额、主动买入量、成交笔数等,这些都是很有价值的信息,可以用于构建策略。
df.to_csv('btc_klines.csv') df = pd.read_csv('btc_klines.csv',index_col=0)
df
time open high low close amount \ 0 1596988800000 11575.08 11642.00 11566.07 11591.37 6541.466 1 1596992400000 11591.39 11610.23 11526.90 11534.39 6969.252 2 1596996000000 11534.39 11656.69 11527.93 11641.07 6439.365 3 1596999600000 11641.06 11665.90 11624.20 11635.30 3911.582 4 1597003200000 11635.29 11684.00 11635.29 11673.81 3461.004 ... ... ... ... ... ... ... 8805 1628658000000 45627.72 45894.53 45540.00 45801.45 10296.202 8806 1628661600000 45801.46 46270.00 45800.01 46087.86 26409.962 8807 1628665200000 46087.87 46450.00 46087.87 46367.38 23969.309 8808 1628668800000 46367.37 46643.13 46002.01 46217.01 23472.769 8809 1628672400000 46217.01 46329.69 46046.54 46297.16 6579.477 end_time volume count buy_amount buy_volume null 0 1596992399999 7.592336e+07 25724 3127.898 3.630633e+07 0 1 1596995999999 8.057780e+07 27403 3390.424 3.920162e+07 0 2 1596999599999 7.469135e+07 25403 3446.186 3.997906e+07 0 3 1597003199999 4.555459e+07 17820 1842.413 2.145768e+07 0 4 1597006799999 4.036804e+07 15513 1660.575 1.936981e+07 0 ... ... ... ... ... ... ... 8805 1628661599999 4.710187e+08 112187 4988.565 2.282399e+08 0 8806 1628665199999 1.215164e+09 247170 13696.301 6.302708e+08 0 8807 1628668799999 1.110210e+09 232348 11990.951 5.554267e+08 0 8808 1628672399999 1.086549e+09 229533 12334.292 5.711837e+08 0 8809 1628675999999 3.039580e+08 78812 3313.055 1.530718e+08 0 [8810 rows x 12 columns]
df.index = pd.to_datetime(df.time,unit='ms') # 把索引转化为时间,方便画图参考
df.close.plot(figsize=(15,6),grid = True); #收盘价
<Figure size 1080x432 with 1 Axes>
(df.buy_amount.rolling(150).mean()/df.amount.rolling(150).mean()).plot(figsize=(15,6),grid = True); #平滑后的主动买入量比例 #主动买入比例见底回升往往对应着价格的回升,但主动买入比例长期均值在49%
<Figure size 1080x432 with 1 Axes>
(df['count'].rolling(100).mean()).plot(figsize=(15,6),grid = True); #平滑后成交笔数,处于低位可能酝酿着行情
<Figure size 1080x432 with 1 Axes>
回测引擎
以前的文章也给出过Python回测引擎,这里给出一个优化的版本。USDT本位(或者其他计价货币本位)的永续合约和现货非常相似,区别是永续可以加杠杆并且持有负的数量(相当于做空),可以共用一个回测引擎。币本位交割合约特殊,用币结算,需要专门的回测。
下面给出的是一个简单例子,可以实现多币种现货或多币种永续回测。忽略了很多细节:如期货的杠杆、保证金占用、资金费率、爆仓机制、做市和吃单成交、省略了订单维护等,但通常不影响正常的回测结果。并且撮合的价格和数量、账户的更新都是需要外部传入。读者可以在这个基础上完善。
Exchange类介绍:
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)
#测试,可以看到这里没有强调交易所是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))
{'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参数改为自己想回测的交易对试试。
symbol = 'TRX' df = GetKlines(symbol=symbol,start='2021-4-4',end='2021-8-11',period='5m')
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')
e.account
{'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}}
res.profit.plot(figsize=(15,6),grid = True);
<Figure size 1080x432 with 1 Axes>
res.price.plot(figsize=(15,6),grid = True); #收盘价
<Figure size 1080x432 with 1 Axes>
现货平衡策略回测
这类策略也比较流行,但FMZ平台不太好回测多币种,正好用这个回测引擎回测下。我们选取四个主流币种BTC,ETH,LTC,XRP,分别配置25%的市值,每偏离1%平衡一次。
首先把四个币的过去1年的收盘价获取到,可以看到ETH涨幅最大,其余3个币的涨幅差不多。如果平均持有这四个币,最后的净值为4.5 。平衡策略经过回测最后净值为5.3,略有增强。
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
df = pd.DataFrame([data[symbol].values for symbol in symbols],index=symbols).T
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'])
(df/df.iloc[0,:]).plot(figsize=(15,6),grid = True); #归一化画出走势
<Figure size 1080x432 with 1 Axes>
(res.total/10000-(df/df.iloc[0,:]).mean(axis=1)).plot(figsize=(15,6),grid = True); #增强效果
<Figure size 1080x432 with 1 Axes>
海龟策略
海龟策略是一个经典的趋势策略,包含完整的加仓止损逻辑,具体可以参考:https://zhuanlan.zhihu.com/p/27987938 。我们这里将实现一个简单的版本用于回测。
海龟的策略周期对策略影响很大,不易选择太短的周期,这里选择6h。唐安奇通道周期选择5,仓位比例根据回测选择了0.003.当价格突破通道上轨开多1个单位,开仓后如果价格继续上涨0.3个波动率,继续加仓1个单位,价格跌破最近开仓价的2.5个波动率止损。空单原理相同。由于ETH的大牛市,海龟策略抓住了主要的趋势,最终实现了27倍的收益,期间最大杠杆4倍。
海龟的参数和周期关系密切,需要通过回测选择,每次开仓的单位也要注意不可过大,避免风险太高。
从最终的净值图可以看出,海龟是个长期策略,期间可能3-4个月都没收益,反复止损,但一旦有单边的大行情,海龟可以顺势累计很大的仓位并且拿到趋势的末尾,获得很多盈利。在上涨的末尾,策略会累积很多仓位,此时的波动会比较大,往往很大的盈利都回撤了。使用海龟策略需要接受海龟的缺点并且耐心等待。
symbol = 'ETH' df = GetKlines(symbol=symbol,start='2019-8-11',end='2021-8-11',period='6h')
df.index = pd.to_datetime(df.time,unit='ms')
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相当于最近波动率,用于加仓和止损的判断
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])
最终市值: 280760.566996
res.total.plot(figsize=(15,6),grid = True);
<Figure size 1080x432 with 1 Axes>
(res.value/res.total).plot(figsize=(15,6),grid = True);
<Figure size 1080x432 with 1 Axes>
总结
熟练使用jupyter notebook研究平台,可以很方便的进行数据获取、数据分析、策略回测、图表展示等工作,是量化交易的必经之路。如果你现在对策略编写无头绪,不妨先分析数据。对于新手,推荐资源:
利用 Python 进行数据分析 : https://wizardforcel.gitbooks.io/pyda-2e/content/
Python 量化交易教程 : https://wizardforcel.gitbooks.io/python-quant-uqer/content/
Bố ơi.start_time = res_list[-1][0] dẫn đến sự lặp lại cuối
Dao xươngXin hỏi, ở đâu có 3 mã chiến lược trong bài viết?
Cỏ nhỏBa chính sách trong văn bản có thể thay đổi cặp giao dịch, thay đổi tham số để kiểm tra lại
flywuCó ba vấn đề trong mã chiến lược của biển. 1. kênh xuống sử dụng max Theo logic của việc kiểm tra lại, hai lần tiếp tục tăng giá, lần đầu tiên là trong dòng k trước và lần thứ hai là trong dòng k sau. Ví dụ, giá đầu vào trong dòng k trước là 4000 và giá tăng giá tiếp theo nên ở dòng k tiếp theo, giả sử là 4400, nhưng giá tối thiểu của dòng k tiếp theo là 4500, vì vậy việc tăng giá 4400 trong kiểm tra lại là không thể thực hiện được, do đó tính toán lợi nhuận là quá lớn. 3. Số lượng đặt hàng có vấn đề, e.buy nên là số lượng, đơn vị được tính là đơn vị lưu trữ, và cần phải chuyển đổi thành số lượng