You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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。

步骤说明

  1. 提取并配对交易事件:分离所有入场(buy/sell)和出场(buyclose/sellclose)的时间点,配对每个入场对应的出场。
  2. 向量化计算交易序列:从初始资本开始,依次计算每笔交易的可用资本、份额、利润及交易后的资本,用累计计算替代循环状态更新。
  3. 映射结果回原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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 06:25:22