Pandas向量化计算交易回测:份额计算未同步更新资本的问题
交易回测系统向量化计算的资本更新问题
问题背景
正在开发一套交易回测系统,输入初始资金、历史价格DataFrame,以及随机生成的买卖信号DataFrame。示例数据如下:
| price | signal | entry | exit | shares | profit | capital 0 22 0 nan nan 0 0 1000 1 24 1 24 nan 41,66 0 1000 2 22 1 nan nan 0 0 1000 3 22 0 nan 22 0 -83,33 916,67 4 24 1 24 nan 41,66 0 916,67
计算规则
- 当
['signal']从0变为1时:['entry']存储入场价格['shares']计算公式:['capital']/['entry price']
- 当
['signal']从1变为0时:['exit']存储出场价格['profit']计算公式:(['exit']-['entry']) * ['shares']['capital']为累计利润加之前的资本值,即['capital'] + ['profit'].cumsum()
示例计算:
entry = 24, shares = 1000/24 = 41.., exit = 22 profit = (22-24)*41 = -83 capital = 1000+(-83)
核心要求
所有操作必须采用Pandas向量化实现,因相关文章指出向量化比循环快71-803倍(尤其针对大型DataFrame),需完全避免循环。
当前问题
使用Pandasloc向量化方法按顺序计算时,份额计算先于资本更新,导致份额仅使用初始资本值,未考虑更新后的资本。例如示例DataFrame最后一行的份额应为916/24=38,实际却是1000/24=41,66。
现有核心代码
# ENTRY/EXIT CONDITIONS buy = ((data['signal'].shift(1) != 1) & (data['signal'] == 1)) buyclose = ((data['signal'].shift(1) == 1) & (data['signal'] != 1)) # ENTRY PRICE data['entry'].loc[buy] = data['price'].shift(-1) # SHARES data['shares'].loc[buy] = (data['capital'].loc[buy].values / data['entry'].loc[buy].values) # EXIT PRICE data['exit'].loc[buyclose] = data['price'].shift(-1) # PROFIT data['profit'].loc[buyclose] = (data['exit'].loc[buyclose].values - data['entry'].loc[buy].values) * data['shares'].loc[buy].values # CAPITAL data['capital'] = data['capital'].values + data['profit'].cumsum().values
完整代码
from tvDatafeed import TvDatafeed, Interval import numpy as np import pandas as pd capital = 1000 leverage = 1 spread = 0.03 ### DATA ### data = TvDatafeed().get_hist(symbol='OIL_CRUDE',exchange='CAPITALCOM',interval=Interval.in_30_minute,n_bars=5000) data = data.drop(['symbol','close','volume'], axis=1) data = data.rename_axis('date').reset_index() data['date'] = pd.to_datetime(data['date'], unit='D')#.dt.date data = data.rename_axis('index').reset_index() data['position'] = np.random.randint(3, size=len(data)) - 1 data.loc[0, 'position'] = 0 ### PROVVISORIO new_entry = pd.DataFrame() data['entry'] = np.zeros((len(data)), int) data['entry date'] = np.zeros((len(data)), int) data['exit'] = np.zeros((len(data)), int) data['exit date'] = np.zeros((len(data)), int) data['shares'] = np.zeros((len(data)), int) data['profit'] = np.zeros((len(data)), int) data['capital'] = np.full((len(data)), capital) ### FIX LAST BUY/SELL ### index = 1 if data['position'].iloc[-1] != 0: fix = data['position'] != data['position'].iloc[-1] index = len(data.loc[fix[fix].index[-1]:])-1 ### CONDITIONS ### nlast = (data['index'] < data['index'].iloc[-index]) #EXCLUDE LAST ENTRY buy = ((data['position'].shift(1) != 1) & (data['position'] == 1) & (nlast)) sell = ((data['position'].shift(1) != -1) & (data['position'] == -1) & (nlast)) buyclose = ((data['position'].shift(1) == 1) & (data['position'] != 1)) sellclose = ((data['position'].shift(1) == -1) & (data['position'] != -1)) ##### ENTRIES ##### # ENTRY PRICE data['entry'].loc[buy] = data['open'].shift(-1) + spread data['entry'].loc[sell] = data['open'].shift(-1) - spread ##### EXITS ##### # EXIT PRICE data['exit'].loc[buyclose] = data['open'].shift(-1) data['exit'].loc[sellclose] = data['open'].shift(-1) data.loc[0, 'exit'] = 0 ##### CALCULATIONS ##### # SHARES data['shares'].loc[buy] = (data['capital'].loc[buy].values / data['entry'].loc[buy].values) * leverage data['shares'].loc[sell] = (data['capital'].loc[sell].values / data['entry'].loc[sell].values) * leverage # PROFIT data['profit'].loc[buyclose] = (data['exit'].loc[buyclose].values - data['entry'].loc[buy].values) * data['shares'].loc[buy].values data['profit'].loc[sellclose] = (data['entry'].loc[sell].values - data['exit'].loc[sellclose].values) * data['shares'].loc[sell].values # CAPITAL data['capital'] = data['capital'].values + data['profit'].cumsum().values print(data)
解决方案
问题核心是资本更新存在状态依赖,后续交易的资本取决于前序交易的盈亏结果。要实现向量化计算,需先提取完整交易周期,计算每个周期的资本变化,再映射回原DataFrame。
步骤说明
- 提取并配对交易事件:分离所有入场(buy/sell)和出场(buyclose/sellclose)的时间点,配对每个入场对应的出场。
- 向量化计算交易序列:从初始资本开始,依次计算每笔交易的可用资本、份额、利润及交易后的资本,用累计计算替代循环状态更新。
- 映射结果回原DataFrame:将计算得到的份额、利润、资本值对应到原DataFrame的对应行。
代码实现
# 1. 提取并配对买入/卖出交易周期 # 处理买入交易 buy_entries = data.loc[buy, ['index', 'entry', 'capital']].reset_index(drop=True) buy_exits = data.loc[buyclose, ['index', 'exit']].reset_index(drop=True) buy_trades = pd.concat([buy_entries, buy_exits], axis=1) buy_trades.columns = ['entry_idx', 'entry_price', 'initial_cap', 'exit_idx', 'exit_price'] # 处理卖出交易 sell_entries = data.loc[sell, ['index', 'entry', 'capital']].reset_index(drop=True) sell_exits = data.loc[sellclose, ['index', 'exit']].reset_index(drop=True) sell_trades = pd.concat([sell_entries, sell_exits], axis=1) sell_trades.columns = ['entry_idx', 'entry_price', 'initial_cap', 'exit_idx', 'exit_price'] # 合并所有交易并按入场时间排序 all_trades = pd.concat([buy_trades, sell_trades]).sort_values('entry_idx').reset_index(drop=True) # 2. 向量化计算每笔交易的资本与盈亏 # 初始化当前可用资本 all_trades['current_cap'] = capital # 计算交易份额 all_trades['shares'] = (all_trades['current_cap'] / all_trades['entry_price']) * leverage # 区分买入/卖出交易计算利润 all_trades['profit'] = np.where( all_trades['entry_price'] < data.loc[all_trades['entry_idx'], 'open'].shift(-1).values, (all_trades['exit_price'] - all_trades['entry_price']) * all_trades['shares'], (all_trades['entry_price'] - all_trades['exit_price']) * all_trades['shares'] ) # 计算交易后的资本,更新后续交易的初始资本 all_trades['post_cap'] = all_trades['current_cap'] + all_trades['profit'].cumsum() all_trades['current_cap'] = all_trades['post_cap'].shift(1).fillna(capital) # 3. 将结果映射回原DataFrame data.loc[all_trades['entry_idx'], 'shares'] = all_trades['shares'].values data.loc[all_trades['exit_idx'], 'profit'] = all_trades['profit'].values # 重新计算资本序列 data['capital'] = capital + data['profit'].cumsum().fillna(0)
关键说明
- 该方法通过提取独立交易周期,将状态依赖的计算转化为序列累计计算,完全避免循环,保持向量化高效性。
- 针对大型DataFrame,性能远优于循环,同时解决了资本更新滞后导致的份额计算错误问题。
内容的提问来源于stack exchange,提问作者Francesco Battisti
相关产品推荐
相关产品推荐

