Python數字貨幣量化交易開發——構建回測功能

心虛的時候要微笑發表於2020-12-15

前言

上次建立了一個簡易的csv資料庫,在執行了大概一週左右的時間裡積累了一定量的資料已經可以用於除錯回測系統了,所以這篇部落格主要記錄一下回測系統的建立,再次強調並非程式設計師,所以很多東西都是想當然的,不吝指正。
_
_

Episode 2. 構建回測功能

構建一個回測邏輯,主要分為以下四塊:
1、初始化部分,讀取之前一直在更新的csv價格庫並轉換為所需要的資料格式,系統的需求主要是知道目前的回測位置、時間戳和價格,所以初步構想的資料結構如下:

'''
step_value:回測進度的位置,直接用1到資料總長的順序數字排序來定義
對應的.value就是交易所的ohlcv資料加上.key方便之後呼叫
'''
{step_value:{'time':value1,'open':value2,'high':value3,'low':value4,'close':value5,'volume':value6}}

2、封裝功能部分,在做回測時需要呼叫的各種用於資料重新整理的class rerun(),包括更新賬戶餘額、更新每一步回測最新價格、處理買賣請求、處理在某個時間點交易邏輯發來的過往資料請求等等。
3、交易邏輯,呼叫已封裝好的功能做出交易判斷的部分。
4、回測邏輯,在每一個step_value上重複一次交易邏輯。

話不多說開始碼python:

'''
初始化呼叫csv部分
'''
symbol='ETH/USDT'
ini_account_dict={'init_balance':300,'init_stocks':0.5}#初始資金
fee = 0.002

nl='\n'
account_balance={'balance':0,'stocks':0}#用於交易變動的賬戶
ohlcv_dict={}
b=pd.read_csv(str(symbol[0:3])+'_30d.csv')
ohlcv_dict=b.to_dict ('list')
step_dict={}
step = 1
for key,value in ohlcv_dict.items():#將csv轉為我們需要的資料結構
	step_dict[step] ={'time':key,'open':value[0],'high':value[1],'low':value[2],'close':value[3],'volume':round(value[4],4)}
	step += 1
max_step=len(step_dict)
ini_market_value=step_dict[1]['high']*ini_account_dict['init_stocks']+ini_account_dict['init_balance']
print('讀取資料成功:'+nl+'初始賬戶餘額:',ini_account_dict,nl+'總市值:',ini_market_value,'USDT'+nl+'開始回測...')

def cutpoint(num,c):
	num_x , num_y = str(num).split('.')
	num = float(num_x+'.'+num_y[0:c])
	return num

這部分還不算太複雜,主要是設定一些引數比如初始的賬戶情況和資料的呼叫作為整個回測系統的基礎資料,cutpoiont用於切個小數點,比round各種bug輸出更美觀易用。主要的函式還是下面封裝的功能類class rerun():

class rerun():
	def __init__(self,progress):
		self.step = progress
		if self.step == 1:
			self.update_balance(ini_account_dict['init_balance'],ini_account_dict['init_stocks'])


	def update_balance(self,Balance,Amount):
		account_balance['balance'] = Balance
		account_balance['stocks'] = Amount
		print('Balance updated...|'+str(symbol[0:3])+':',account_balance['stocks'],'USDT:',account_balance['balance'])


	def get_account(self):
		self.balance = account_balance['balance']
		self.amount = account_balance['stocks']


	def create_order(self, order_type, price, amount):
		if order_type == 'buy':
			account_balance['balance']=cutpoint(account_balance['balance']-amount,4)
			account_balance['stocks']=cutpoint(account_balance['stocks']+amount/price*(1-fee),4)
			log = step_dict[self.step]['time']+'   價格'+str(price)+'usdt  買入 '+str(cutpoint(amount/price,4))+symbol[0:3]
		elif order_type == 'sell':
			account_balance['balance']=cutpoint(account_balance['balance']+amount*price*(1-fee),4)
			account_balance['stocks']=cutpoint(account_balance['stocks']-amount,4)
			log = step_dict[self.step]['time']+'   價格'+str(price)+'usdt  賣出 '+str(cutpoint(amount,4))+symbol[0:3]
		print(log + '  賬戶餘額:',account_balance)
		self.get_account()


	def last_price(self):
		self.open = step_dict[self.step]['open']
		self.high = step_dict[self.step]['high']
		self.low = step_dict[self.step]['low']
		self.close = step_dict[self.step]['close']
		self.volume = step_dict[self.step]['volume']
		return self.open


	def last_period(self,period):#使用者想要調取在當前step_value前多少的價格資料
		self.period_list = []
		for k,v in step_dict.items():
			if k <= self.step-1:
				self.period_list.append(v['close'])
			else:
				break
		self.period_list = self.period_list[-period:]
		return self.period_list


	def sleep(self,period):#用於設定最短交易間隔
		self.next_step = self.step + period+1
		return self.next_step


	def refresh_data(self):
		self.price = self.last_price()
		self.get_account()

功能和輸出的內容基本都是字面意思,而我思考了很久還是把def sleep()加進了這個class裡,用返回下一個step_value的方法告訴主函式要跳過多久,如果想到更好的方法之後再優化吧。
下面就是做一個主函式來執行想要進行的交易邏輯,交易邏輯暫定問系統要一個歷史k線,取平均值,低於平均值就買、高於就賣的簡單邏輯用於除錯功能:

def main():
	next_progress = 0
	for progress in range(1,max_step):
		if progress<=next_progress:#跳過一部分sleep掉的step_value
			continue
		next_progress = trade_logic(progress)

	close_value = step_dict[max_step]['close']*account_balance['stocks']
	close_balance = account_balance['balance']
	close_profit_rate = cutpoint(((close_value+close_balance)/ini_market_value-1)*100,2)
	year_index = cutpoint(365*24*60/progress,0)
	year_rate = cutpoint(((1+close_profit_rate/100)**(year_index)-1)*100,2)
	print('回測結果:'+nl+'賬戶餘額:',account_balance,nl+'回測週期:',cutpoint(progress/1440,2),'天  回測週期內收益率:',close_profit_rate,'%  年化收益率:',year_rate,'%')
	return
	
def trade_logic(progress):
	period = 60
	test = rerun(progress)
	test.refresh_data()#重新整理一下資料讓我們可以拿到最新價格和餘額資料
	price_list = test.last_period(60)
	if len(price_list)<period:
		return progress#資料不符合預期數量
	last_price = test.price
	kline_avg = np.mean(price_list)
	if last_price>kline_avg*1.01 and test.amount>0.1:
		test.create_order('sell',test.low,test.amount/5)
	elif last_price<kline_avg*0.99 and test.balance>100:
		test.create_order('buy',test.high,test.balance/5)
	return test.sleep(10)

當中碰到了一個問題是如何用當前回測出的收益率來轉換成年化,最後還是忽略了誤差使用了按回測週期分割一年的時間做次方的複利思想,這部分也待優化吧。

然後看一下最後的回測效果:

讀取資料成功:
初始賬戶餘額: {'init_balance': 300, 'init_stocks': 0.5} 
總市值: 595.14 USDT
開始回測...
Balance updated...|ETH: 0.5 USDT: 300
20201208_17:47:00   價格576.04usdt  買入 0.1041ETH  賬戶餘額: {'balance': 240.0, 'stocks': 0.6039}
20201208_17:59:00   價格573.76usdt  買入 0.0836ETH  賬戶餘額: {'balance': 192.0, 'stocks': 0.6873}
20201209_07:11:00   價格552.83usdt  買入 0.0694ETH  賬戶餘額: {'balance': 153.6, 'stocks': 0.7566}
20201209_15:59:00   價格539.64usdt  買入 0.0569ETH  賬戶餘額: {'balance': 122.88, 'stocks': 0.8134}
20201209_16:35:00   價格532.23usdt  買入 0.0461ETH  賬戶餘額: {'balance': 98.304, 'stocks': 0.8594}
20201209_17:11:00   價格543.37usdt  賣出 0.1718ETH  賬戶餘額: {'balance': 191.5116, 'stocks': 0.6875}
20201209_18:23:00   價格552.13usdt  賣出 0.1375ETH  賬戶餘額: {'balance': 267.2776, 'stocks': 0.55}
20201209_18:35:00   價格558.0usdt  賣出 0.11ETH  賬戶餘額: {'balance': 328.5348, 'stocks': 0.44}
20201209_18:47:00   價格558.96usdt  賣出 0.088ETH  賬戶餘額: {'balance': 377.6249, 'stocks': 0.352}
20201209_21:59:00   價格571.57usdt  賣出 0.0703ETH  賬戶餘額: {'balance': 417.7829, 'stocks': 0.2815}
20201209_22:11:00   價格572.75usdt  賣出 0.0562ETH  賬戶餘額: {'balance': 449.9642, 'stocks': 0.2251}
20201210_17:59:00   價格558.79usdt  買入 0.161ETH  賬戶餘額: {'balance': 359.9713, 'stocks': 0.3858}
20201210_18:11:00   價格558.91usdt  買入 0.1288ETH  賬戶餘額: {'balance': 287.977, 'stocks': 0.5143}
20201211_08:47:00   價格550.61usdt  買入 0.1046ETH  賬戶餘額: {'balance': 230.3816, 'stocks': 0.6186}
20201211_16:23:00   價格546.85usdt  賣出 0.1237ETH  賬戶餘額: {'balance': 297.9025, 'stocks': 0.4948}
20201212_08:23:00   價格552.12usdt  賣出 0.0989ETH  賬戶餘額: {'balance': 352.431, 'stocks': 0.3958}
20201214_00:11:00   價格589.08usdt  賣出 0.0791ETH  賬戶餘額: {'balance': 398.9693, 'stocks': 0.3166}
20201214_00:23:00   價格593.51usdt  賣出 0.0633ETH  賬戶餘額: {'balance': 436.4751, 'stocks': 0.2532}
20201214_19:11:00   價格577.44usdt  買入 0.1511ETH  賬戶餘額: {'balance': 349.18, 'stocks': 0.404}
回測結果:
賬戶餘額: {'balance': 349.18, 'stocks': 0.404} 
回測週期: 6.65 天  回測週期內收益率: -1.86 %  年化收益率: -63.71 %
[Finished in 2.4s]

剩下的就是根據實際使用的情況優化細節了,比如輸出的內容,回測的迴圈方式。
今天就記錄到這裡,看到寫了很久的程式碼沒什麼令人費解的報錯還是感到很滿足的。

相關文章