前面兩篇文章,分別講述了基于事件驅動(Event-driven)的量化回測系統的層次結構,以及事件類型。本文重點講述市場數據是如何在回測系統以及實盤中使用的。
我們很重要的目標是要實現最大化在回測和實盤間復用代碼,避免開發兩套系統,也有效避免回測過程與實盤不一樣。這樣DataHandler提供數據,Strategy產生信號以及Porfolio類處理訂單都應該使用一致的數據接口。
DataHandler是一個數據管理基類,所以子類比如基于CSV文件提供數據是CSVDataHandler或者MongodbDataHandler等,替換這個數據處理器,并不會影響Strategy或Porfolio,這樣就相對容易實現功能擴展。
下面我們開始實現DataHandler。
首先我們要導入相應的頭文件,包括datetime,pandas等,基類有兩個抽象接口,是子類必須實現的。
update_bars:在循環中每調用一次,時間會往前走一個周期,如果是日線則就是一天,相當于新的一天收市。
get_latest_bars:是在策略計算中,可以取最近的N個bars。用于各種指標計算,比如MA(5)等
# data.py
importdatetime
importos, os.path
importpandasaspd
importcopy
fromabcimportABCMeta, abstractmethod
fromaiquant.engine.eventimportBarEvent
'''
DataHandler是一個抽象數據處理類,所以實際數據處理類都繼承于此(包含歷史回測、實盤)
'''
classDataHandler(object):
__metaclass__= ABCMeta
@abstractmethod
defget_latest_bars(self, symbol, N=1):
"""
返回最近的N個Bar,如果當前Bar的數量不足N,則有多少就返回多少
"""
raiseNotImplementedError("沒有實現get_latest_bars()")
@abstractmethod
defupdate_bars(self):
"""
把symbol列表里所有symbol最近的Bar導入
"""
raiseNotImplementedError("沒有實現update_bars()")
為簡單起見,本文先實現一個數據保存的csv里的數據管理器。
classCSVDataHandler(DataHandler):
def__init__(self,events,csv_dir,symbol_list):
self.b_continue_backtest =True
self.events = events
#symbol_list:傳入要處理的symbol列表集合,list類型
self.symbol_list = symbol_list
self.symbol_list_with_benchmark = copy.deepcopy(self.symbol_list)
self.symbol_list_with_benchmark.append('000300')
self.csv_dir = csv_dir
self.symbol_data = {}#symbol_data,{symbol:DataFrame}
self.latest_symbol_data = {}#最新的bar:{symbol:[bar1,bar2,barNew]}
self.b_continue_backtest =True
self._open_convert_csv_files()
def_open_convert_csv_files(self):
comb_index =None
forsinself.symbol_list_with_benchmark:
#加載csv文件,date,OHLC,Volume
self.symbol_data[s] = pd.read_csv(
os.path.join(self.csv_dir,'%s.csv'% s),
header=0,index_col=0,parse_dates=False,
names=['date','open','high','low','close','volume']
).sort_index()
# Combine the index to pad forward values
ifcomb_indexis None:
comb_index =self.symbol_data[s].index
else:
#這里要賦值,否則comb_index還是原來的index
comb_index = comb_index.union(self.symbol_data[s].index)
#設置latest symbol_data為None
self.latest_symbol_data[s] = []
# Reindex the dataframes
forsinself.symbol_list_with_benchmark:
#這是一個發生器iterrows[index,series],用next(self.symbol_data[s])
#pad方式,就是用前一天的數據再填充這一天的丟失,對于資本市場這是合理的,比如這段時間停牌。那就是按停牌前一天的價格數據來計算。
self.symbol_data[s] =self.symbol_data[s].reindex(index=comb_index,method='pad').iterrows()
def_get_new_bar(self, symbol):
"""
row = (index,series),row[0]=index,row[1]=[OHLCV]
"""
row =next(self.symbol_data[symbol])
#return tuple(symbol,row[0],row[1][0],row[1][1],row[1][2],row[1][3],row[1][4]])
row_dict = {'symbol':symbol,'date':row[0],'open':row[1][0],'high':row[1][1],'low':row[1][2],'close':row[1][3]}
returnrow_dict
defupdate_bars(self):
"""
Pushes the latest bar to the latest_symbol_data structure
for all symbols in the symbol list.
"""
forsinself.symbol_list_with_benchmark:
try:
bar =self._get_new_bar(s)
print(bar)
exceptStopIteration:
self.b_continue_backtest =False
else:
ifbaris not None:
self.latest_symbol_data[s].append(bar)
self.events.put(BarEvent())
defget_latest_bars(self, symbol, N=1):
"""
Returns the last N bars from the latest_symbol list,
or N-k if less available.
"""
try:
bars_list =self.latest_symbol_data[symbol]
exceptKeyError:
print("That symbol is not available in the historical data set.")
else:
returnbars_list[-N:]
后續還需要擴展到真實環境,比如直接從線上的mongodb里訪問數據。
關于作者:魏佳斌,互聯網產品/技術總監,北京大學光華管理學院(MBA),特許金融分析師(CFA)。深度關注互聯網發展趨勢,AI金融量化。致力于使用最新的人工智能技術去理解經濟、金融,實現信息增值。
掃描下方二維碼,關注:AI量化實驗室(ailabx),了解AI量化最前沿技術、資訊。