矢量化方法回测相对于事件驱动回测优点在于直观与计算效率高, 但是对于复杂策略或者模拟真实交易情形时, 矢量方法复原能力较弱. 引入适当的回测框架可以实现代码在不同策略下的重复利用, 这也体现出了模块化编程的思想. 这里将回测框架大致区分为: 数据处理, 头寸管理, 交易指令, 策略四个部分, 用事件驱动产生交易信息.
1. 结构化数据
class TickData:
def __init__(self, date, close):
self.date = date
self.close = close
TickData对象, 用于记录日期(date), 收盘价(close). 这里将使用均值回归策略(MeanRevertingStrategy)进行回测, 仅需要记录日期和收盘价信息. 在使用更复杂策略回测时, 可以相应地改变数据结构.
2. 数据获取, 清洗与模拟
class MarketDataSource:
def __init__(self, symbol, start, end):
self.symbol = symbol
self.start = start
self.end = end
self.data = None
def download_data(self):
self.data = ts.get_k_data(self.symbol, self.start, self.end)
def clean_data(self):
self.data.index = pd.to_datetime(self.data.date)
self.data = self.data.sort_index()
def start_simulation(self, strategy, position):
for date, row in self.data.iterrows():
tick_data = TickData(date, row['close'])
strategy.event_tick(tick_data)
strategy.event_position(position)
strategy.event_order()
if strategy.order is not None:
position.operate(strategy.order.date, strategy.order.is_buy, strategy.order.price, strategy.order.qty)
MarketDataSource对象记录了代码(symbol), 开始与结束时间(start, end). 在MarketDataSource中, 定义download_data和clean_data方法用于下载和清洗数据, 定义start_simulation方法对数据(DataFrame对象)进行逐行迭代, 产生价格信息(tick_data). 将价格信息传递给策略(strategy), 判断是否产生交易信号以及相应地改变头寸.
3. 头寸管理
class Position:
def __init__(self):
self.buys = 0
self.sells = 0
self.net = 0
self.cash = 0
self.net_value = 0
def operate(self, date, is_buy, price, qty):
if is_buy:
self.buys += qty
print('Date: {}, Trade: Buy, Price: {}, Qty: {}'.format(date.strftime('%Y-%m-%d'), price, qty))
else:
self.sells += qty
print('Date: {}, Trade: Sell, Price: {}, Qty: {}'.format(date.strftime('%Y-%m-%d'), price, qty))
self.net = self.buys - self.sells
self.cash += price * qty * (-1 if is_buy else 1)
self.net_value = self.cash + self.net * price
def display(self):
print('Position Info: Cash: {}, Net: {}, Net Value: {}'. format(self.cash, self.net, self.net_value))
Position对象记录头寸相关信息. operate方法用于计算每次交易时现金(cash), 证券资产(net), 策略净值(net_value)变化情况, display方法用于打印出头寸中现金, 证券资产和净值信息.
4. 交易指令
class Order:
def __init__(self, date, is_buy, price, qty):
self.date = date
self.is_buy = is_buy
self.price = price
self.qty = qty
5. 均值回归策略
class MeanRevertingStrategy:
def __init__(self, cycle=20, buy_threshold=-1.5, sell_threshold=1.3):
self.cycle = cycle
self.buy_threshold = buy_threshold
self.sell_threshold = sell_threshold
self.prices = pd.DataFrame()
self.is_long = False
self.is_short = False
self.order = None
def event_tick(self, tick_data):
self.prices.loc[tick_data.date, 'close'] = tick_data.close
def event_position(self, position):
if position.net > 0:
self.is_long, self.is_short = True, False
elif position.net < 0:
self.is_long, self.is_short = False, True
else:
self.is_long = self.is_short = False
def event_order(self):
if len(self.prices) < self.cycle:
self.order = None
else:
z = self.calculate_z_score()
if self.is_long is False and z < self.buy_threshold:
self.order = Order(self.prices.index[-1], is_buy=True, price=self.prices.close[-1], qty=100)
elif self.is_short is False and z > self.sell_threshold:
self.order = Order(self.prices.index[-1], is_buy=False, price=self.prices.close[-1], qty=100)
else:
self.order = None
def calculate_z_score(self):
window = self.prices[-self.cycle:]
ret = window['close'].pct_change().dropna()
z = ((ret[-1]) - ret.mean()) / ret.std()
return z
初始化MeanRevertingStrategy需要给定计算均值周期(cycle), 买卖信号产生的阈值(buy_threshold, sell_threshold). event_tick, event_position方法分别用于接收MarketDataSource.start_simulation迭代产生的价格数据和头寸信息, event_order方法判断是否产生交易指令(order), 并将order记录为策略的属性值.
6. 回测对象
class BackTester:
def __init__(self, symbol, start, end):
self.symbol = symbol
self.start = start
self.end = end
self.position = None
self.strategy = None
def check_position(self):
if self.position is None:
self.position = Position()
else:
return None
def start_back_test(self):
market_data_source = MarketDataSource(self.symbol, self.start, self.end)
market_data_source.download_data()
market_data_source.clean_data()
print('Start Simulation: ')
market_data_source.start_simulation(self.strategy, self.position)
self.position.display()
print('Completed. ')
建立回测对象的实例需要初始化代码, 起止时间. check_position方法用于检测当前回测对象的实例是否已经存在头寸. start_back_test方法整合了MarketDataSource中的方法, 产生回测信息.
7. 回测
back_tester = BackTester('600000', '2019-01-01', '2020-01-01')
back_tester.check_position()
back_tester.strategy = MeanRevertingStrategy()
back_tester.start_back_test()
下面是回测结果. 打印出了交易时间, 买卖方向, 价格和数量等信息.
尾记: 面对对象的编程相对于矢量化编程更加抽象, 因为在实例化之前要先赋予类属性和方法, 要求对需要实现的功能, 特征具有全面的理解. 在参照了他人写的非常优秀, 功能强大(花里胡哨)的回测框架后, 拆拆补补写出了一个简单的回测, 这有种拆了法拉利的车轮安在了夏利车上的感觉. 当然好的回测框架应该具有强大的可再用性. 从最基础的框架出发, 下面的任务就是怎么一步步去实现更多的功能. 最后不管怎么样, 小破车也要上路了.
拓展阅读: