Sí, eso puede ser realmente sofisticado incluso utilizando herramientas tan bonitas como pandas. Pero la idea básica es encontrar entradas y salidas de posiciones para derivar el flujo de caja.
Aquí está mi código para derivar todas esas cosas de las señales generadas (en mi backtester las señales son fracciones de 2 acciones en cartera para cada momento). Espero haber encontrado todos los errores aquí, pero no hay garantías. Total_invertido se utiliza para calcular el número total de acciones en la cartera utilizando sólo el capital inicial.
En realidad, este código es parte de mi motor de backtest que voy a abrir en unos meses. También se ocupa de los spreads y las comisiones fijas, por lo que puede omitir esta parte. Será necesario adaptarlo a tus condiciones, pero espero que te sirva de ayuda.
También puede consultar artículos de Mike Halls-Moore . Este sitio me ha ayudado mucho.
def get_portfolio(self, signals, prices):
time_index = prices.index
end = time_index[-1]
# Assets prices
x_prices = prices[self.sym1]
y_prices = prices[self.sym2]
# Assets weights in portfolio
x_weights = signals.map(lambda sig:sig[self.sym1])
y_weights = signals.map(lambda sig:sig[self.sym2])
# Total number of invested shares
total_invested = pd.Series(index = signals.index)
# Need trade enters to calculate portfolio size
enter_points = x_weights.diff().fillna(0)!=0
# Index magic here:
# We need to delete position exits, we suppose there are separate enters/exits
enter_points[enter_points[enter_points].index[1::2]] = False
# capital = w₁⋅total⋅p₁ + w₂⋅total⋅p₂ (cover position)
total_invested[enter_points] = (self.initial_capital
/(x_weights.abs()[enter_points]*x_prices[enter_points]
+ y_weights.abs()[enter_points]*y_prices[enter_points]))
# Zero for right initial open and final close
total_invested.iloc[0] = 0; total_invested.iloc[-1] = 0;
total_invested.fillna(method='ffill', inplace=True)
# Positions rounded to lot sizes
x_positions = (total_invested*x_weights//self.lot1*self.lot1).fillna(0)
y_positions = (total_invested*y_weights//self.lot2*self.lot2).fillna(0)
long_pos = x_positions.copy(); long_pos[long_pos<0] = 0
short_pos = x_positions.copy(); short_pos[short_pos>0] = 0
x_pos_diff = x_positions.diff().fillna(0)
y_pos_diff = y_positions.diff().fillna(0)
# Tribute to the market-makers: spread
# Binary divide because we count twice: enter & exit
tribute = (abs(x_pos_diff)*self.spread1 + abs(y_pos_diff)*self.spread2)/2
# Cashflow & Turnover: not the same!
cashflow = -x_pos_diff*x_prices - y_pos_diff*y_prices
turnover = abs(x_pos_diff)*x_prices + abs(y_pos_diff)*y_prices
# Commission from turnover
commission = turnover * self.commission
commission[cashflow!=0] += self.fixed_commission
portfolio = pd.DataFrame(index=time_index, columns=['EQ','holdings',
'cash','cashflow','long_trades','short_trades',
'x_positions','y_positions'])
# Value of shares
portfolio['holdings'] = x_positions*x_prices + y_positions*y_prices
# Value of cash, paid commission on each transaction
portfolio['cash'] = self.initial_capital + (cashflow
- commission - tribute).cumsum()
# Equity is the sum of both
portfolio['EQ'] = portfolio['cash'] + portfolio['holdings']
portfolio['turnover'] = turnover
# Time points of trades
portfolio['long_trades'] = long_pos.diff().fillna(0)
portfolio['short_trades'] = short_pos.diff().fillna(0)
portfolio['x_positions'] = x_positions
portfolio['y_positions'] = y_positions
return portfolio