• 【量化】一个简版单档tick数据回测框架


    这是一个简易的模拟实际交易流程的回测框架,所使用的行情数据是单档的tick成交数据。为了实现调用者可以实现自己的交易逻辑,本框架预留了几个函数予以调用者能够继承类后在子类中重写以实现买入卖出信号的生成(check_sell()和check_buy())。

    因为最近比较忙,忙着实习、放假忙着骑车(找骑友,在上海),所以文档上的内容就没有写得很详细啦,如果想要进一步交流的欢迎私信我或者留言评论,如果需要数据来复线本文的话请私信。

    这是一个简易的模拟实际交易流程的回测框架,所使用的行情数据是单档的tick成交数据

    直接上图:

    为了模拟实际交易流程,在属性中定义了账户信息、下单记录、成交记录和持仓信息这四个表来控制交易流程,交易流程如下所示:

    为了实现调用者可以实现自己的交易逻辑,本框架预留了几个函数予以调用者能够继承类后在子类中重写以实现买入卖出信号的生成(check_sell()和check_buy())。

    以下是Tickbacktest.py文件中的代码:

    1. # -*- coding=utf-8 -*-
    2. # --------------------------------
    3. # @Time : 2023年11月6日19:24:47
    4. # @Author : Noah Zhan
    5. # @File : tickbacktest.py
    6. # @Project : tickbacktest
    7. # @Function :tickbacktest类
    8. # --------------------------------
    9. from datetime import datetime
    10. import datetime as dt
    11. import logging
    12. #from typing_extensions import Self
    13. import numpy as np
    14. import pandas as pd
    15. import os
    16. from tqdm import tqdm
    17. from empyrical import max_drawdown,sharpe_ratio
    18. def timeformat(date)->datetime:
    19. """
    20. 将各种字符串格式的日期数据转化为datetime数据类型
    21. """
    22. if isinstance(date,datetime):
    23. return date
    24. elif isinstance(date,str):
    25. if("/" in date):
    26. return pd.to_datetime(datetime.strptime(date,'%Y/%m/%d %H:%M:%S'),format = '%Y-%m-%d %H:%M:%S')
    27. elif("-"in date):
    28. return pd.to_datetime(datetime.strptime(date,'%Y-%m-%d %H:%M:%S'),format = '%Y-%m-%d %H:%M:%S')
    29. else:
    30. return pd.to_datetime(datetime.strptime(date,'%Y%m%d %H:%M:%S'),format = '%Y-%m-%d %H:%M:%S')
    31. class tickbacktest(object):
    32. def __init__(self,name,start_dt,end_dt,now,total_assets = 1000*10000,commissions = 2,slip = 0.2,tick_data = dict(),context = dict()) -> None:
    33. '''
    34. 初始化tickbacktest类。
    35. '''
    36. self.start_dt = timeformat(start_dt)
    37. self.end_dt = timeformat(end_dt)
    38. self.now = timeformat(now)
    39. self.commissions = commissions
    40. if(tick_data):
    41. self.tick_data = tick_data
    42. tick_data['tick_all'] = tick_data['tick_all'][(tick_data['tick_all']['time_stamp']>=self.start_dt)&(tick_data['tick_all']['time_stamp']<=self.end_dt) ]
    43. #初始化accoun表
    44. self.account = pd.DataFrame(index = [name],columns = ['name','initial_cash','total_assets_lastday','total_assets','cash_useable','profit','total_mkt_cap'])
    45. self.account.loc[name,'name'] = name
    46. self.account.loc[name,'total_assets_lastday'] = total_assets
    47. self.account.loc[name,'total_assets'] = total_assets
    48. self.account.loc[name,'cash_useable'] = total_assets
    49. self.account.loc[name,'initial_cash'] = total_assets
    50. self.account.loc[name,'profit'] = 0
    51. self.account.loc[name,'total_mkt_cap'] = 0
    52. #初始化order表
    53. self.order = pd.DataFrame(columns=['order_id','stk_id','order_price','order_num','state','order_dt','direction','order_method','num_left'])
    54. #初始化deal表
    55. self.deal = pd.DataFrame(columns=['deal_id','stk_id','deal_price','deal_num','deal_dt','commission','direction','deal_amount'])
    56. #初始化portfolio表
    57. self.portfolio = pd.DataFrame(columns=['stk_id','port_num','intraday_buy_num','intraday_sell_num','port_amount','commission','hold_cost','price_now','dynamic_equity','hold_profit','realized_profit','state'])
    58. #初始化contex(用于存储可能用到的全局变量参数或资料)
    59. self.context = context
    60. context['name'] = name
    61. context['slip'] = 0.01*slip
    62. #创建文件夹保存相关文件
    63. self.mkdir(name)
    64. #配置日志输出
    65. logging.basicConfig(filename=name+'/log.txt',
    66. format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s-%(funcName)s',
    67. level=logging.INFO)
    68. self.firstday = True
    69. @staticmethod
    70. def mkdir(path) -> None:
    71. '''
    72. 创建文件夹,用于保存回测相关的数据和日志。
    73. '''
    74. folder = os.path.exists(path)
    75. if not folder: #判断是否存在文件夹如果不存在则创建为文件夹
    76. os.makedirs(path) #makedirs 创建文件时如果路径不存在会创建这个路径
    77. else:
    78. print("--- There is this folder! ---")
    79. @staticmethod
    80. def weighted_price(value,volume) -> float:
    81. '''
    82. 给定价格和数量,计算标的的平均价格。
    83. '''
    84. return np.average(value, weights=volume)
    85. def make_order(self,stk_id,order_price,order_num,direction,order_method) -> None:
    86. '''
    87. function:委托下单,将委托下单信息更新到order表中
    88. params:
    89. - stk_id: str,需要下单的标的id;
    90. - order_price: float,下单限价金额;
    91. - order_num: int,委托下单数量;
    92. - direction: str,候选值有'buy'和'sell',下单的方向;
    93. - order_method: 下单的类型,目前只支持'限价'单。
    94. return:
    95. -
    96. '''
    97. index = len(self.order)+1
    98. order_toadd = pd.DataFrame(index=[index],columns=['order_id','stk_id','order_price','order_num','state','order_dt','direction','order_method','num_left'])
    99. order_toadd.loc[index,'order_id'] = index
    100. order_toadd.loc[index,'stk_id'] = stk_id
    101. order_toadd['order_price'] = order_price
    102. order_toadd.loc[index,'order_num'] = order_num
    103. order_toadd.loc[index,'state'] = '未成'
    104. order_toadd.loc[index,'order_dt'] = self.now
    105. order_toadd.loc[index,'direction'] = direction
    106. order_toadd.loc[index,'order_method'] = order_method
    107. order_toadd.loc[index,'num_left'] = order_num
    108. self.order = pd.concat([self.order,order_toadd],axis=0)
    109. def clear_order(self,order_id) -> None:
    110. '''
    111. function:将未成的id为order_id单子撤掉,修改相应的单子的stata为'撤单'。
    112. params:
    113. - order_id: 要撤掉的单子的id。
    114. return:
    115. -
    116. '''
    117. self.order.loc[order_id,'state'] = '已撤'
    118. logging.info("info,撤单成功-id-"+str(order_id)+'-stkid-'+str(self.order.loc[order_id,'stk_id'])+"-time-"+str(self.order.loc[order_id,'order_dt'])+str(self.order.loc[order_id,'order_price'])+"-price-")
    119. def clear_allorder(self) -> None:
    120. '''
    121. function:将未成的单子全部撤掉,修改相应的单子的stata为'撤单'。
    122. params:
    123. - order_id: 要撤掉的单子的id。
    124. return:
    125. -
    126. '''
    127. order_ids = list(self.order[self.order['state']=='未成']['order_id'])
    128. for order_id in order_ids:
    129. self.clear_order(order_id)
    130. def excecute_deal(self,direction) -> None:
    131. '''
    132. function:将order表中未成的单子与当前tick的价格和数量进行比对,如果满足成交条件则成交,并修改account、portfolio、order、deal表的相关数据
    133. params:
    134. - direction:要执行的order的方向。
    135. return:
    136. -
    137. '''
    138. order_todo = self.order[(self.order['state']=='未成')&(self.order['direction']==direction)]
    139. for ind,data in order_todo.iterrows():
    140. dealable = self.tick_data['tick_now'][data['stk_id']]
    141. if(direction=='buy'):
    142. dealable = dealable[dealable['value']<=data['order_price']]
    143. elif(direction=='sell'):
    144. dealable = dealable[dealable['value']>=data['order_price']]
    145. # print('sell',dealable)
    146. if(not dealable.empty):
    147. dealable = pd.DataFrame(dealable)
    148. for inde,deal in dealable.iterrows():
    149. if(isinstance(self.order.loc[ind,'num_left'],pd.Series)):
    150. self.order = self.order.reset_index().drop_duplicates(subset=['index'], keep='last').set_index('index')
    151. if(((self.order.loc[ind,'num_left']>0) & (self.order.loc[ind,'state']=='未成'))):
    152. #判断可用金额是否足够,若不够,则自动调整下单量,以保证交易可执行,并输出日志
    153. if((direction=='buy')and(data['num_left'] * (self.weighted_price(list(self.tick_data['tick_now'][data['stk_id']]['value']),list(self.tick_data['tick_now'][data['stk_id']]['volume'])) + self.commissions) > self.account.loc[self.context['name'],'cash_useable'])):
    154. data['num_left'] = int(self.account.loc[self.context['name'],'cash_useable']/(self.weighted_price(list(self.tick_data['tick_now'][data['stk_id']]['value']),list(self.tick_data['tick_now'][data['stk_id']]['volume'])) + self.commissions))
    155. self.order.loc[inde,'num_left'] = data['num_left']
    156. logging.warning('warning,剩余现金不足,将order-'+str(ind)+"下单量自动调整为"+str(data['num_left']))
    157. #修改deal表
    158. index = len(self.deal)+1
    159. deal_toadd = pd.DataFrame(index=[index],columns=['deal_id','stk_id','deal_price','deal_num','deal_dt','commission','direction','deal_amount'])
    160. deal_toadd.loc[index,'deal_id'] = index
    161. deal_toadd.loc[index,'stk_id'] = data['stk_id']
    162. deal_toadd.loc[index,'deal_price'] = deal['value']
    163. deal_toadd.loc[index,'deal_num'] = min(deal['volume'],data['num_left'])
    164. deal_toadd.loc[index,'deal_dt'] = data['order_dt']
    165. deal_toadd.loc[index,'commission'] = deal_toadd.loc[index,'deal_num'] * self.commissions
    166. deal_toadd.loc[index,'direction'] = data['direction']
    167. deal_toadd.loc[index,'deal_amount'] = deal_toadd.loc[index,'deal_num'] * deal_toadd.loc[index,'deal_price']
    168. self.deal = pd.concat([self.deal,deal_toadd],axis=0)
    169. #修改order表
    170. self.order.loc[ind,'num_left'] = self.order.loc[ind,'num_left'] - deal_toadd.loc[index,'deal_num']
    171. if(self.order.loc[ind,'num_left']<=0): self.order.loc[ind,'state'] = '已成'
    172. #修改portfolio和account表
    173. if(data['direction']=='buy'):
    174. if (data['stk_id'] in list(self.portfolio['stk_id'])):
    175. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'intraday_buy_num'] += deal_toadd.loc[index,'deal_num']
    176. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num']=self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'intraday_buy_num']-self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'intraday_sell_num']
    177. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_amount'] += deal_toadd.loc[index,'deal_amount']
    178. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'commission'] += deal_toadd.loc[index,'commission']
    179. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'hold_cost'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_amount'] + self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'commission']
    180. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'price_now'] = self.weighted_price(list(self.tick_data['tick_now'][data['stk_id']]['value']),list(self.tick_data['tick_now'][data['stk_id']]['volume']))
    181. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'dynamic_equity'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num'] * self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'price_now']
    182. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'hold_profit'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'dynamic_equity']-self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'hold_cost']
    183. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'state'] = '持仓'
    184. self.account.loc[self.context['name'],'cash_useable'] -= (deal_toadd.loc[index,'deal_amount']+deal_toadd.loc[index,'commission'])
    185. self.account.loc[self.context['name'],'total_mkt_cap'] = sum(self.portfolio['dynamic_equity'])
    186. self.account.loc[self.context['name'],'total_assets'] =self.account.loc[self.context['name'],'cash_useable'] +self.account.loc[self.context['name'],'total_mkt_cap']
    187. self.account.loc[self.context['name'],'profit'] = self.account.loc[self.context['name'],'total_assets'] - self.account.loc[self.context['name'],'initial_cash']
    188. else:
    189. portfolio_toadd = pd.DataFrame(index=[len(self.portfolio)+1],columns=['stk_id','port_num','intraday_buy_num','intraday_sell_num','port_amount','commission','hold_cost','price_now','dynamic_equity','hold_profit','realized_profit','state'])
    190. portfolio_toadd.loc[len(self.portfolio)+1,'intraday_buy_num'] = deal_toadd.loc[index,'deal_num']
    191. portfolio_toadd.loc[len(self.portfolio)+1,'intraday_sell_num'] = 0
    192. portfolio_toadd.loc[len(self.portfolio)+1,'stk_id'] = data['stk_id']
    193. portfolio_toadd.loc[len(self.portfolio)+1,'port_num'] = deal_toadd.loc[index,'deal_num']
    194. portfolio_toadd.loc[len(self.portfolio)+1,'port_amount'] = deal_toadd.loc[index,'deal_amount']
    195. portfolio_toadd.loc[len(self.portfolio)+1,'commission'] = deal_toadd.loc[index,'commission']
    196. portfolio_toadd.loc[len(self.portfolio)+1,'hold_cost'] = deal_toadd.loc[index,'deal_amount'] + deal_toadd.loc[index,'commission']
    197. portfolio_toadd.loc[len(self.portfolio)+1,'price_now'] = self.weighted_price(list(self.tick_data['tick_now'][data['stk_id']]['value']),list(self.tick_data['tick_now'][data['stk_id']]['volume']))
    198. portfolio_toadd.loc[len(self.portfolio)+1,'dynamic_equity'] = deal_toadd.loc[index,'deal_num'] * self.weighted_price(list(self.tick_data['tick_now'][data['stk_id']]['value']),list(self.tick_data['tick_now'][data['stk_id']]['volume']))
    199. portfolio_toadd.loc[len(self.portfolio)+1,'hold_profit'] = portfolio_toadd.loc[len(self.portfolio)+1,'dynamic_equity'] - portfolio_toadd.loc[len(self.portfolio)+1,'hold_cost']
    200. portfolio_toadd.loc[len(self.portfolio)+1,'realized_profit'] = 0
    201. portfolio_toadd.loc[len(self.portfolio)+1,'state'] = '持仓'
    202. self.portfolio = pd.concat([self.portfolio,portfolio_toadd],axis=0)#合并
    203. self.account.loc[self.context['name'],'cash_useable'] -=(deal_toadd.loc[index,'deal_amount']+deal_toadd.loc[index,'commission'])
    204. self.account.loc[self.context['name'],'total_mkt_cap'] = sum(self.portfolio['dynamic_equity'])
    205. self.account.loc[self.context['name'],'total_assets'] = self.account.loc[self.context['name'],'cash_useable'] +self.account.loc[self.context['name'],'total_mkt_cap']
    206. self.account.loc[self.context['name'],'profit'] = self.account.loc[self.context['name'],'total_assets'] - self.account.loc[self.context['name'],'initial_cash']
    207. elif(data['direction']=='sell'):#卖出则持仓中必须有该标的资产
    208. if(self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num']<=0):
    209. self.clear_order(order_id=data['order_id'])#持仓不足,撤卖单
    210. continue
    211. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'intraday_sell_num'] +=deal_toadd.loc[index,'deal_num']
    212. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'intraday_buy_num'] - self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'intraday_sell_num']
    213. amount_temp = float(self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_amount'])
    214. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_amount'] *=(self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num']/(deal_toadd.loc[index,'deal_num'] +self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num']))
    215. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'commission'] -=deal_toadd.loc[index,'commission']
    216. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'hold_cost'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_amount'] + self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'commission']
    217. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'price_now'] = self.weighted_price(list(self.tick_data['tick_now'][data['stk_id']]['value']),list(self.tick_data['tick_now'][data['stk_id']]['volume']))
    218. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'dynamic_equity'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num'] * self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'price_now']
    219. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'hold_profit'] = self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'dynamic_equity']-self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'hold_cost']
    220. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'realized_profit'] += (deal_toadd.loc[index,'deal_num']*(self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'price_now']-self.commissions))-(deal_toadd.loc[index,'deal_num']/(deal_toadd.loc[index,'deal_num'] +self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num']))*amount_temp
    221. if(self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'port_num']==0):
    222. self.portfolio.loc[self.portfolio[self.portfolio['stk_id'] == data['stk_id']].index[0],'state'] = '已卖'
    223. self.account.loc[self.context['name'],'cash_useable'] +=(deal_toadd.loc[index,'deal_amount']-deal_toadd.loc[index,'commission'])
    224. self.account.loc[self.context['name'],'total_mkt_cap'] = sum(self.portfolio['dynamic_equity'])
    225. self.account.loc[self.context['name'],'total_assets'] = self.account.loc[self.context['name'],'cash_useable'] +self.account.loc[self.context['name'],'total_mkt_cap']
    226. self.account.loc[self.context['name'],'profit'] = self.account.loc[self.context['name'],'total_assets'] - self.account.loc[self.context['name'],'initial_cash']
    227. logging.info('info,'+str(data['stk_id'])+"-"+str(deal_toadd.loc[index,'deal_num'])+"-"+str(data['direction'])+"-deal id:"+str(deal_toadd.loc[index,'deal_id']))
    228. # print('info,'+str(data['stk_id'])+"-"+str(deal_toadd.loc[index,'deal_num'])+"-"+str(data['direction'])+"-deal id:"+str(deal_toadd.loc[index,'deal_id']))
    229. return
    230. def every_tick(self) -> None:
    231. '''
    232. function:定义每一个新tick应该做一些什么,包括1)更新self.now,self.tick_data['tick_now'];2)更新portfolio表相关信息;3)更新account表相关信息;4)本tick要卖出的stk_id和数量list并下单、执行委托;5)本tick要买入的stk_id和数量list并下单、执行委托.
    233. params:
    234. -
    235. return:
    236. -
    237. '''
    238. #1)更新self.now,self.tick_data['tick_now']
    239. self.now = self.now + dt.timedelta(seconds=1)
    240. self.tick_data['tick_now'] = dict()
    241. for stk_id in set(self.tick_data['tick_all']['security_id']):
    242. self.tick_data['tick_now'][stk_id] = self.tick_data['tick_all'][(self.tick_data['tick_all']['time_stamp']==self.now)&(self.tick_data['tick_all']['security_id']==stk_id)]
    243. i=1
    244. while(self.tick_data['tick_now'][stk_id].empty):
    245. self.tick_data['tick_now'][stk_id] = self.tick_data['tick_all'][(self.tick_data['tick_all']['time_stamp']==(self.now+dt.timedelta(seconds=-1*i)))&(self.tick_data['tick_all']['security_id']==stk_id)]
    246. i += 1
    247. # print(self.tick_data['tick_now'])
    248. #2)更新portfolio表相关信息
    249. for ind,row in self.portfolio.iterrows():
    250. if(not self.tick_data['tick_now'][row['stk_id']].empty):
    251. self.portfolio.loc[ind,'price_now'] = self.weighted_price(list(self.tick_data['tick_now'][row['stk_id']]['value']),list(self.tick_data['tick_now'][row['stk_id']]['volume']))
    252. self.portfolio.loc[ind,'dynamic_equity'] = self.portfolio.loc[ind,'price_now'] * self.portfolio.loc[ind,'port_num']
    253. self.portfolio.loc[ind,'hold_profit'] = self.portfolio.loc[ind,'dynamic_equity'] - self.portfolio.loc[ind,'hold_cost']
    254. #3)更新account表相关信息
    255. self.account.loc[self.context['name'],'total_mkt_cap'] = sum(self.portfolio['dynamic_equity'])
    256. self.account.loc[self.context['name'],'total_assets'] = self.account.loc[self.context['name'],'total_mkt_cap'] + self.account.loc[self.context['name'],'cash_useable']
    257. self.account.loc[self.context['name'],'profit'] = self.account.loc[self.context['name'],'total_assets'] - self.account.loc[self.context['name'],'initial_cash']
    258. #4)本tick要卖出的stk_id和数量并下单、执行委托;
    259. sell_id_list,sell_num_list = self.check_sell()
    260. if(not(len(sell_id_list)==0 or len(sell_num_list)==0)):
    261. for sell_id,sell_num in zip(sell_id_list,sell_num_list):
    262. self.make_order(stk_id=sell_id, order_price = self.weighted_price(self.tick_data['tick_now'][sell_id]['value'],self.tick_data['tick_now'][sell_id]['volume'])*(1-self.context['slip']), order_num=sell_num, direction='sell', order_method='限价')
    263. self.excecute_deal(direction='sell')
    264. # 5)本tick要买入的stk_id和数量list并下单、执行委托;
    265. buy_id_list,buy_num_list = self.check_buy()
    266. if(not(len(buy_id_list)==0 or len(buy_num_list)==0)):
    267. for buy_id,buy_num in zip(buy_id_list,buy_num_list):
    268. self.make_order(stk_id=buy_id, order_price = self.weighted_price(self.tick_data['tick_now'][buy_id]['value'],self.tick_data['tick_now'][buy_id]['volume'])*(1+self.context['slip']), order_num=buy_num, direction='buy', order_method='限价')
    269. self.excecute_deal(direction='buy')
    270. #6)每一分钟,输出账户信息到文件中。
    271. if(self.now.second==0):
    272. account_toadd = self.account.copy()
    273. account_toadd['datetime'] = self.now
    274. account_toadd.set_index(['datetime'])
    275. if os.path.exists(self.context['name'] + '/account_tick.csv'):
    276. account_toadd.to_csv(self.context['name'] + '/account_tick.csv', mode='a', header=False)
    277. else:
    278. account_toadd.to_csv(self.context['name'] + '/account_tick.csv')
    279. return
    280. def check_sell(self) -> tuple:
    281. '''
    282. function: 用于生成当tick需要卖出的股票的列表和数量,同时这一方法是开放给使用时进行重写的,以实现调用者自己的策略逻辑;
    283. params:
    284. -
    285. return:
    286. - sell_id_list: 需要卖出的股票的列表;
    287. - sell_num_list: 需要卖出的股票对应的数量列表。
    288. '''
    289. sell_id_list,sell_num_list = [],[]
    290. return sell_id_list,sell_num_list
    291. def check_buy(self)-> tuple:
    292. '''
    293. function: 用于生成当tick需要买入的股票的列表和数量,同时这一方法是开放给使用时进行重写的,以实现调用者自己的策略逻辑;
    294. params:
    295. -
    296. return:
    297. - buy_id_list: 需要买入的股票的列表;
    298. - buy_num_list: 需要买入的股票对应的数量列表。
    299. '''
    300. buy_id_list,buy_num_list = [],[]
    301. return buy_id_list,buy_num_list
    302. def every_morning(self)-> None:
    303. '''
    304. function:定义每一个新的交易日应该做一些什么,包括1)更新self.now;2)保存上一日的持仓信息到表portfolio.csv中,并做相应更新;3)保存上一日的account信息到account.csv中,并做相应更新;4)进行使用者自行定义的早上要进行的操作。
    305. *注意
    306. params:
    307. -
    308. return:
    309. -
    310. '''
    311. #1)更新self.now
    312. self.context['last_dt'] = datetime(self.now.year,self.now.month,self.now.day,14,30,0)
    313. self.now = datetime(self.now.year,self.now.month,self.now.day+1,8,59,59)
    314. while(not datetime(self.now.year,self.now.month,self.now.day) in list(self.tick_data['tick_all']['time_stamp'].apply(lambda x:datetime(x.year,x.month,x.day)))):#要确保是交易日
    315. self.now = datetime(self.now.year,self.now.month,self.now.day+1,8,59,59)
    316. #2)保存上一日的持仓信息到表portfolio.csv中,并做相应更新;
    317. if(not self.portfolio.empty):
    318. portfolio_toadd = self.portfolio.copy()
    319. portfolio_toadd['date'] = self.context['last_dt']
    320. portfolio_toadd.set_index(['date','stk_id'])
    321. if os.path.exists(self.context['name'] + '/portfolio.csv'):
    322. portfolio_toadd.to_csv(self.context['name'] + '/portfolio.csv', mode='a', header=False)
    323. else:
    324. portfolio_toadd.to_csv(self.context['name'] + '/portfolio.csv')
    325. self.portfolio = self.portfolio[~(self.portfolio['state']=='已卖')]
    326. #3)保存上一日的account信息到account.csv中,并做相应更新
    327. if(not self.account.empty):
    328. account_toadd = self.account.copy()
    329. account_toadd['date'] = self.context['last_dt']
    330. account_toadd['return'] = account_toadd['total_assets'] / account_toadd['total_assets_lastday'] - 1
    331. account_toadd.set_index(['date'])
    332. if os.path.exists(self.context['name'] + '/account.csv'):
    333. account_toadd.to_csv(self.context['name'] + '/account.csv', mode='a', header=False)
    334. else:
    335. account_toadd.to_csv(self.context['name'] + '/account.csv')
    336. #更新self.account['total_assets_lastday']
    337. self.account['total_assets_lastday'] = account_toadd['total_assets']
    338. #4)进行使用者自行定义的早上要进行的操作
    339. self.dosomethin_in_morning()
    340. return
    341. def before_close(self)-> None:
    342. '''
    343. function:定义收盘前(收盘前一分钟)应该做一些什么,留给用户来重写,如果持仓不过夜,请在这里实现清仓,会在每日收盘前一分钟调用调用.
    344. params:
    345. -
    346. return:
    347. -
    348. '''
    349. self.clear_allorder()
    350. def dosomethin_in_morning(self)-> None:
    351. '''
    352. function:定义早上要做些什么,除了已经定义的every_morning内的其他操作之外,用户可以重写此方法在早上马上开盘时进行操作。
    353. params:
    354. -
    355. return:
    356. -
    357. '''
    358. pass
    359. def pipline(self)-> None:
    360. '''
    361. function: 在这个函数中进行回测流程的控制,调用这个函数以进行回测;
    362. params:
    363. -
    364. return:
    365. -
    366. '''
    367. logging.info('info,'+str(self.context['name'])+'回测开始')
    368. pbar = tqdm(len(list(set(self.tick_data['tick_all']['time_stamp']))))
    369. while(self.now<=self.end_dt):
    370. if(self.firstday):
    371. self.firstday = False
    372. else:
    373. self.every_morning()
    374. if(self.now>self.end_dt):break
    375. while(not((self.now.hour==14) and (self.now.minute==29) and (self.now.second==0))):
    376. self.every_tick()
    377. pbar.update(1)
    378. if(self.now>self.end_dt):break
    379. if(self.now>self.end_dt):break
    380. self.before_close()
    381. logging.info('info,'+str(self.context['name'])+'回测结束')
    382. return
    383. def summary(self)-> None:
    384. '''
    385. function: gross and net of commissions return, P&L, annualized volatility of the strategy, Sharpe, Maximum Drawdown and P-value.
    386. params:
    387. -
    388. return:
    389. -
    390. '''
    391. data = pd.read_csv(self.context['name'] + '/account.csv')
    392. if data.empty:
    393. print('请先进行回测。')
    394. else:
    395. data = data.reset_index()
    396. data = data.sort_values('date',ascending=True)
    397. #return
    398. return_ = data.iloc[-1,:]['total_assets'] / data.iloc[-1,:]['initial_cash'] - 1
    399. #P&L
    400. PandL = data.iloc[-1,:]['total_assets'] - data.iloc[-1,:]['initial_cash']
    401. #volatility
    402. volatility = data['return'].std()
    403. #Sharpe
    404. Sharpe = sharpe_ratio(data['return'], risk_free=0, period='daily')
    405. #Maximum Drawdown
    406. max_drawdown_ = max_drawdown(data['return'])
    407. from prettytable import PrettyTable
    408. x = PrettyTable()
    409. x.padding_width = 2
    410. x.add_column("回测", [self.context['name']])
    411. x.add_column("回测收益", [str(round(float(return_ * 100), 2)) + "%"])
    412. x.add_column("P&L", [str(round(float(PandL), 2))])
    413. x.add_column("年化波动率", [str(round(float(volatility), 2))])
    414. x.add_column("夏普", [str(round(Sharpe, 2))])
    415. x.add_column("最大回撤", [str(round(max_drawdown_*100, 2))+ "%"])
    416. print(x)

    对上述代码进行实际应用,继承backtest方法并重写相关的方法,实现调用者自己的逻辑(check_sell,check_buy,before_close):

    这里使用了简单的均线策略进行测试。

    1. import pandas as pd
    2. import numpy as np
    3. from tickbacktest import *
    4. import datetime as dt
    5. #读取数据
    6. data = pd.read_csv('Sample Tick Data.csv')
    7. data = data.groupby(['time_stamp','security_id','value'])['volume'].apply(lambda x:sum(x)).reset_index()
    8. data['time_stamp'] = pd.to_datetime(data['time_stamp'])
    9. #继承backtest方法并重写相关的方法,实现调用者自己的逻辑(check_sell,check_buy,before_close)
    10. class my_tickbacktest(tickbacktest):
    11. def __init__(self,stk_id,average_short_min,average_long_min,trade_period_restriction,
    12. name,start_dt,end_dt,now,total_assets = 1000*10000,commissions = 2,slip = 0.2,tick_data = dict(),context = dict()):
    13. super().__init__(name,start_dt,end_dt,now,total_assets,commissions,slip,tick_data,context)
    14. self.stk_id = stk_id
    15. self.context['signal_record'] = pd.DataFrame(columns=['time_stamp', 'contract', 'moving_average_1', 'moving_average_2', 'signal', 'return'])
    16. self.average_short_min = average_short_min
    17. self.average_long_min = average_long_min
    18. self.average_short_line = []
    19. self.average_long_line = []
    20. self.signal = 0
    21. self.trade_time = 0
    22. self.trade_period_restriction = trade_period_restriction
    23. self.istrading_buy = 0
    24. self.istrading_sell = 0
    25. def moving_average(self,stk_id):
    26. short_panel = self.tick_data['tick_all'][(self.tick_data['tick_all']['time_stamp']>=self.now + dt.timedelta(seconds=-1*self.average_short_min*60))&(self.tick_data['tick_all']['time_stamp']<=self.now)]
    27. short_panel = short_panel[short_panel['security_id'] == stk_id]
    28. short_price = self.weighted_price(short_panel['value'],short_panel['volume'])
    29. long_panel = self.tick_data['tick_all'][(self.tick_data['tick_all']['time_stamp']>=self.now + dt.timedelta(seconds=-1*self.average_long_min*60))&(self.tick_data['tick_all']['time_stamp']<=self.now)]
    30. long_panel = long_panel[long_panel['security_id'] == stk_id]
    31. long_price = self.weighted_price(long_panel['value'],long_panel['volume'])
    32. return short_price,long_price
    33. def check_sell(self):
    34. '''
    35. function:重写check_sell的功能,实现卖出选股
    36. params:
    37. -
    38. return:
    39. - sell_id_list: 需要卖出的股票的列表;
    40. - sell_num_list: 需要卖出的股票对应的数量列表。
    41. '''
    42. short_price,long_price = self.moving_average(self.stk_id)
    43. self.average_short_line.append(short_price)
    44. self.average_long_line.append(long_price)
    45. if(not (self.stk_id in list(self.portfolio['stk_id']))):
    46. return [],[]
    47. sell_id_list,sell_num_list = [],[]
    48. #生成信号
    49. if((len(self.average_long_line)>=2 and len(self.average_short_line)>=2)and(not self.istrading_sell)):
    50. if(self.average_long_line[-2]>self.average_short_line[-2] and self.average_long_line[-1]1]):
    51. #输出日志
    52. logging.info('info,卖出信号-'+str(self.stk_id)+'-short Average:'+str(self.average_long_line[-1])+'-short Average:'+str(self.average_long_line[-1]))
    53. #更新信号记录表
    54. temp_signal_record = pd.DataFrame(index=[self.now],columns=['time_stamp', 'contract', 'moving_average_1', 'moving_average_2', 'signal', 'return'])
    55. temp_signal_record.loc[self.now,'time_stamp'] = self.now
    56. temp_signal_record.loc[self.now,'contract'] = self.stk_id
    57. temp_signal_record.loc[self.now,'moving_average_1'] = self.average_short_line[-1]
    58. temp_signal_record.loc[self.now,'moving_average_2'] = self.average_long_line[-1]
    59. temp_signal_record.loc[self.now,'signal'] = -1
    60. temp_signal_record.loc[self.now,'return'] = self.account.loc[self.context['name'],'profit']/self.account.loc[self.context['name'],'initial_cash']
    61. self.context['signal_record'] = pd.concat([self.context['signal_record'],temp_signal_record],axis=0)
    62. if os.path.exists(self.context['name'] + '/signal_record.csv'):
    63. temp_signal_record.to_csv(self.context['name'] + '/signal_record.csv', mode='a', header=False)
    64. else:
    65. temp_signal_record.to_csv(self.context['name'] + '/signal_record.csv')
    66. #其他操作
    67. self.clear_allorder()
    68. self.signal = -1
    69. self.trade_time = 0
    70. self.istrading_buy = 0
    71. self.istrading_sell = 1
    72. elif((self.istrading_sell) and (self.trade_time < self.trade_period_restriction)):
    73. self.signal = -1
    74. else:
    75. self.signal = 0
    76. #根据信号生成sell_id_list,sell_num_list
    77. if(self.signal==-1 and self.istrading_sell==1):
    78. sell_num = int(self.portfolio[self.portfolio['stk_id']==self.stk_id]['port_num'] / (self.trade_period_restriction-self.trade_time))
    79. sell_id_list.append(self.stk_id)
    80. sell_num_list.append(sell_num)
    81. self.trade_time = self.trade_time + 1
    82. return sell_id_list,sell_num_list
    83. def check_buy(self):
    84. '''
    85. function:重写check_buy的功能,实现买入选股
    86. params:
    87. -
    88. return:
    89. - buy_id_list: 需要买入的股票的列表;
    90. - buy_num_list: 需要买入的股票对应的数量列表。
    91. '''
    92. buy_id_list,buy_num_list = [],[]
    93. #生成信号
    94. if((len(self.average_long_line)>=2 and len(self.average_short_line)>=2 and (not self.istrading_buy))):
    95. if(self.average_long_line[-2]2] and self.average_long_line[-1]>self.average_short_line[-1]):
    96. #更新日志
    97. logging.info('info,买入信号-'+str(self.stk_id)+'-short Average:'+str(self.average_long_line[-1])+'-short Average:'+str(self.average_long_line[-1]))
    98. #更新信号记录表
    99. temp_signal_record = pd.DataFrame(index=[self.now],columns=['time_stamp', 'contract', 'moving_average_1', 'moving_average_2', 'signal', 'return'])
    100. temp_signal_record.loc[self.now,'time_stamp'] = self.now
    101. temp_signal_record.loc[self.now,'contract'] = self.stk_id
    102. temp_signal_record.loc[self.now,'moving_average_1'] = self.average_short_line[-1]
    103. temp_signal_record.loc[self.now,'moving_average_2'] = self.average_long_line[-1]
    104. temp_signal_record.loc[self.now,'signal'] = 1
    105. temp_signal_record.loc[self.now,'return'] = self.account.loc[self.context['name'],'profit']/self.account.loc[self.context['name'],'initial_cash']
    106. self.context['signal_record'] = pd.concat([self.context['signal_record'],temp_signal_record],axis=0)
    107. if os.path.exists(self.context['name'] + '/signal_record.csv'):
    108. temp_signal_record.to_csv(self.context['name'] + '/signal_record.csv', mode='a', header=False)
    109. else:
    110. temp_signal_record.to_csv(self.context['name'] + '/signal_record.csv')
    111. #其他操作
    112. self.clear_allorder()
    113. self.signal = 1
    114. self.trade_time = 0
    115. self.istrading_buy = 1
    116. self.istrading_sell = 0
    117. elif((self.istrading_buy) and (self.trade_time < self.trade_period_restriction)):
    118. self.signal = 1
    119. else:
    120. self.signal = 0
    121. #根据信号生成buy_id_list,buy_num_list
    122. if(self.signal==1 and self.istrading_buy==1):
    123. try:
    124. buy_num = int(self.account.cash_useable / self.weighted_price(self.tick_data['tick_now'][self.stk_id]['value'],self.tick_data['tick_now'][self.stk_id]['volume']) / (self.trade_period_restriction-self.trade_time))
    125. except KeyError:
    126. temp_tick=pd.DataFrame()
    127. i = 1
    128. while(temp_tick.empty):
    129. temp_tick = self.tick_data['tick_all'][(self.tick_data['tick_all']['time_stamp']==(self.now+dt.timedelta(seconds=-1*i)))&(self.tick_data['tick_all']['security_id']==self.stk_id)]
    130. i += 1
    131. buy_num = int(self.account.cash_useable / self.weighted_price(temp_tick['value'],temp_tick['volume']) / (self.trade_period_restriction-self.trade_time))
    132. if(not buy_num==0):
    133. buy_id_list.append(self.stk_id)
    134. buy_num_list.append(buy_num)
    135. self.trade_time = self.trade_time + 1
    136. return buy_id_list,buy_num_list
    137. def before_close(self)-> None:
    138. '''
    139. function:定义收盘前(收盘前一分钟)应该做一些什么,1)撤掉所有未成订单;2)以1分钟内的平均价格清仓所有持仓
    140. params:
    141. -
    142. return:
    143. -
    144. '''
    145. #1) 撤掉所有未成订单;
    146. self.clear_allorder()
    147. #2)以1分钟内的平均价格清仓所有持仓;(假设全成)
    148. #如果没有持仓则直接pass
    149. if(len(self.portfolio)<1):return
    150. else:
    151. #更新self.portfolio
    152. for ind,row in self.portfolio.iterrows():
    153. self.portfolio.loc[ind,'intraday_sell_num'] +=self.portfolio.loc[ind,'port_num']
    154. port_num_temp = self.portfolio.loc[ind,'port_num']
    155. self.portfolio.loc[ind,'port_num'] = 0
    156. self.portfolio.loc[ind,'port_amount'] = 0
    157. self.portfolio.loc[ind,'commission'] = 0
    158. hold_cost_temp = float(self.portfolio.loc[ind,'hold_cost'])
    159. self.portfolio.loc[ind,'hold_cost'] = 0
    160. self.portfolio.loc[ind,'price_now'] = '-'
    161. self.portfolio.loc[ind,'dynamic_equity'] = 0
    162. self.portfolio.loc[ind,'hold_profit'] = 0
    163. #计算一分钟内平均价
    164. temp_tick = self.tick_data['tick_all'][(self.tick_data['tick_all']['time_stamp']>=self.now)&(self.tick_data['tick_all']['time_stamp']<(self.now+dt.timedelta(minutes=1)))]
    165. temp_tick = temp_tick[temp_tick['security_id']==self.stk_id]
    166. value = temp_tick['value']
    167. volume = temp_tick['volume']
    168. self.portfolio.loc[ind,'realized_profit'] += (port_num_temp*(self.weighted_price(value,volume)-self.commissions) - hold_cost_temp)
    169. self.portfolio.loc[ind,'state'] = '已卖'
    170. #更新account
    171. self.account.loc[self.context['name'],'cash_useable'] += (port_num_temp*(self.weighted_price(value,volume)-self.commissions))
    172. self.account.loc[self.context['name'],'total_mkt_cap'] = sum(self.portfolio['dynamic_equity'])
    173. self.account.loc[self.context['name'],'total_assets'] =self.account.loc[self.context['name'],'cash_useable']+self.account.loc[self.context['name'],'total_mkt_cap']
    174. self.account.loc[self.context['name'],'profit'] = self.account.loc[self.context['name'],'total_assets'] - self.account.loc[self.context['name'],'initial_cash']
    175. #更新日志
    176. #进行回测
    177. tick_data = {'tick_all':data,
    178. 'tick_now':dict()}
    179. test = my_tickbacktest(stk_id='CLZ4',average_short_min=3,average_long_min=6,trade_period_restriction=60,
    180. name='CLZ4-3-6',start_dt='2014-11-03 9:00:00',end_dt='2014-11-03 11:00:00',now='2014-11-03 08:59:59',total_assets = 10*10000,commissions = 2,slip = 0.2,tick_data = tick_data,context = dict())
    181. test.pipline()

    买入卖出信号预览:

    pd.DataFrame([test.average_long_line,test.average_short_line]).T.plot()

    回测结果:

    回测结果保存到本地:

  • 相关阅读:
    并查集总结
    面向对象编程原则(07)——接口隔离原则
    小黑子—spring:第二章 注解开发
    【网站架构】一招搞定90%的分布式事务,实打实介绍数据库事务、分布式事务的工作原理应用场景
    高手必备 | Revit插件到底哪个好?区别是什么?
    【vue3】实现数据响应式(ref、shallowRef、trigger、reactive、shallowReactive、toRef、toRefs)
    交换机堆叠 配置(H3C)堆叠中一台故障如何替换
    k8s集群中部署prometheus server
    ruby基础-安装和命令行
    团队难带测试管理太难做?十多位名企测试专家带你成为优秀管理
  • 原文地址:https://blog.csdn.net/standingflower/article/details/134545885