quant-research-env / server /backtester.py
yobro4619's picture
Upload folder using huggingface_hub
c30c8bd verified
"""
Backtesting engine for the Quant Research Environment.
Ported from the RAETH Trading Eval verifier. Replays trade logs
against price series with transaction costs, computes Sharpe,
drawdown, and exposure metrics.
"""
import numpy as np
import pandas as pd
TRANSACTION_COST = 0.00009 # 0.9 bps
BARS_PER_DAY = 375
ANNUALIZATION_FACTOR = np.sqrt(252 * BARS_PER_DAY)
MAX_NET_EXPOSURE_RATIO = 0.80
def compute_exposure_ratio(
nifty_pos: float, bn_pos: float, nifty_close: float, bn_close: float
) -> float:
"""Compute net exposure ratio at a given bar."""
nifty_notional = nifty_pos * nifty_close
bn_notional = bn_pos * bn_close
gross = abs(nifty_notional) + abs(bn_notional)
if gross == 0:
return 0.0
return abs(nifty_notional + bn_notional) / gross
def replay_trades_single(
trades_df: pd.DataFrame, close_series: pd.Series
) -> dict:
"""
Replay a single-instrument trade log against close prices.
Parameters
----------
trades_df : DataFrame with columns [bar, position].
close_series : Series of close prices indexed 0..N-1.
Returns
-------
dict with total_trades, total_pnl, max_drawdown, annualized_sharpe, bar_pnls.
"""
n_bars = len(close_series)
trade_map = {}
for _, row in trades_df.iterrows():
trade_map[int(row["bar"])] = row["position"]
position = 0.0
running_pnl = 0.0
peak_pnl = 0.0
max_drawdown = 0.0
total_trades = 0
bar_pnls = []
for i in range(n_bars):
bar_pnl = 0.0
if i > 0:
bar_pnl = position * (close_series.iloc[i] - close_series.iloc[i - 1])
if i in trade_map:
new_position = trade_map[i]
if new_position != position:
total_trades += 1
bar_pnl -= TRANSACTION_COST * close_series.iloc[i] * abs(
new_position - position
)
position = new_position
running_pnl += bar_pnl
bar_pnls.append(bar_pnl)
if running_pnl > peak_pnl:
peak_pnl = running_pnl
dd = peak_pnl - running_pnl
if dd > max_drawdown:
max_drawdown = dd
bar_pnls_arr = np.array(bar_pnls)
if len(bar_pnls_arr) > 1 and np.std(bar_pnls_arr) > 0:
annualized_sharpe = (
np.mean(bar_pnls_arr) / np.std(bar_pnls_arr) * ANNUALIZATION_FACTOR
)
elif len(bar_pnls_arr) > 1 and np.mean(bar_pnls_arr) > 0:
annualized_sharpe = 99.0 # Zero variance with positive mean → very high Sharpe
elif len(bar_pnls_arr) > 1 and np.mean(bar_pnls_arr) < 0:
annualized_sharpe = -99.0 # Zero variance with negative mean → very low Sharpe
else:
annualized_sharpe = 0.0
return {
"total_trades": total_trades,
"total_pnl": round(running_pnl, 2),
"max_drawdown": round(max_drawdown, 2),
"annualized_sharpe": round(annualized_sharpe, 4),
"bar_pnls": bar_pnls,
}
def replay_trades_multi(
trades_df: pd.DataFrame,
nifty_close: pd.Series,
banknifty_close: pd.Series,
) -> dict:
"""
Replay a multi-instrument (spread) trade log.
Parameters
----------
trades_df : DataFrame with columns [bar, nifty_position, banknifty_position].
nifty_close, banknifty_close : Series of close prices indexed 0..N-1.
Returns
-------
dict with total_trades_per_leg, total_spread_entries, total_pnl,
max_drawdown, annualized_sharpe, bar_pnls, max_net_exposure_ratio,
exposure_violations.
"""
n_bars = min(len(nifty_close), len(banknifty_close))
trade_map = {}
for _, row in trades_df.iterrows():
trade_map[int(row["bar"])] = (
float(row["nifty_position"]),
float(row["banknifty_position"]),
)
nifty_pos = 0.0
bn_pos = 0.0
running_pnl = 0.0
peak_pnl = 0.0
max_drawdown = 0.0
total_trades_nifty = 0
total_trades_bn = 0
spread_entries = 0
bar_pnls = []
max_net_exposure = 0.0
exposure_violations = []
for i in range(n_bars):
bar_pnl = 0.0
if i > 0:
bar_pnl = nifty_pos * (
nifty_close.iloc[i] - nifty_close.iloc[i - 1]
) + bn_pos * (banknifty_close.iloc[i] - banknifty_close.iloc[i - 1])
if i in trade_map:
new_nifty_pos, new_bn_pos = trade_map[i]
old_nifty_pos, old_bn_pos = nifty_pos, bn_pos
if new_nifty_pos != nifty_pos:
total_trades_nifty += 1
bar_pnl -= TRANSACTION_COST * nifty_close.iloc[i] * abs(
new_nifty_pos - nifty_pos
)
nifty_pos = new_nifty_pos
if new_bn_pos != bn_pos:
total_trades_bn += 1
bar_pnl -= TRANSACTION_COST * banknifty_close.iloc[i] * abs(
new_bn_pos - bn_pos
)
bn_pos = new_bn_pos
both_were_flat = old_nifty_pos == 0 and old_bn_pos == 0
any_now_active = nifty_pos != 0 or bn_pos != 0
if both_were_flat and any_now_active:
spread_entries += 1
ratio = compute_exposure_ratio(
nifty_pos, bn_pos, nifty_close.iloc[i], banknifty_close.iloc[i]
)
if ratio > max_net_exposure:
max_net_exposure = ratio
if ratio > MAX_NET_EXPOSURE_RATIO:
exposure_violations.append({"bar": i, "ratio": round(ratio, 4)})
running_pnl += bar_pnl
bar_pnls.append(bar_pnl)
if running_pnl > peak_pnl:
peak_pnl = running_pnl
dd = peak_pnl - running_pnl
if dd > max_drawdown:
max_drawdown = dd
total_trades_per_leg = max(total_trades_nifty, total_trades_bn)
bar_pnls_arr = np.array(bar_pnls)
if len(bar_pnls_arr) > 1 and np.std(bar_pnls_arr) > 0:
annualized_sharpe = (
np.mean(bar_pnls_arr) / np.std(bar_pnls_arr) * ANNUALIZATION_FACTOR
)
elif len(bar_pnls_arr) > 1 and np.mean(bar_pnls_arr) > 0:
annualized_sharpe = 99.0
elif len(bar_pnls_arr) > 1 and np.mean(bar_pnls_arr) < 0:
annualized_sharpe = -99.0
else:
annualized_sharpe = 0.0
return {
"total_trades_per_leg": total_trades_per_leg,
"total_spread_entries": spread_entries,
"total_pnl": round(running_pnl, 2),
"max_drawdown": round(max_drawdown, 2),
"annualized_sharpe": round(annualized_sharpe, 4),
"bar_pnls": bar_pnls,
"max_net_exposure_ratio": round(max_net_exposure, 4),
"exposure_violations": exposure_violations,
}