nse-backtest-app / engine.py
analavishal's picture
Upload engine.py
b7e9b28 verified
Raw
History Blame Contribute Delete
3.73 kB
"""Backtest engine"""
import pandas as pd, numpy as np
from core import Trade, CAPITAL, BROKER, STT, SLIP, MAXPOS, DELAY, MAXTRADES
from collections import Counter
def backtest(signals):
trades=[]; pos=None; daily_pnl={}; day_trades={}; equity=[CAPITAL]
for idx,row in signals.iterrows():
dt=row.get('datetime',idx); date=row.get('date',dt.date() if hasattr(dt,'date') else dt)
tm=row.get('time',dt.time() if hasattr(dt,'time') else None)
close=float(row['close']); sig=int(row.get('signal',0))
sl=row.get('sl_price',None); tp=row.get('tp_price',None)
lo=float(row['low']); hi=float(row['high'])
if date not in day_trades: day_trades[date]=0
if pos is not None:
ex=False; ep=close; er=""; d=pos['direction']
if d==1 and lo<=pos['sl']: ep=pos['sl']; ex=True; er="stop_loss"
elif d==-1 and hi>=pos['sl']: ep=pos['sl']; ex=True; er="stop_loss"
elif d==1 and hi>=pos['tp']: ep=pos['tp']; ex=True; er="take_profit"
elif d==-1 and lo<=pos['tp']: ep=pos['tp']; ex=True; er="take_profit"
elif tm and hasattr(tm,'hour') and tm.hour==15 and tm.minute>=15: ep=close; ex=True; er="eod_exit"
elif sig!=0 and sig!=d: ep=close; ex=True; er="signal_exit"
if ex:
ep=ep*(1-SLIP) if d==1 else ep*(1+SLIP)
sh=pos['shares']; gross=(ep-pos['entry_price'])*d*sh
costs=BROKER+pos['entry_price']*sh*STT+BROKER+ep*sh*STT; net=gross-costs
trades.append(Trade(pos['ticker'],pos['entry_time'],dt,d,pos['entry_price'],ep,sh,net,
net/(pos['entry_price']*sh) if pos['entry_price']*sh!=0 else 0,0,er))
equity.append(equity[-1]+net)
daily_pnl[date]=daily_pnl.get(date,0)+net; day_trades[date]+=1; pos=None
if pos is None and sig!=0:
if day_trades.get(date,0)<MAXTRADES:
if row.get('bar_num',999)>=DELAY:
ep=close*(1+SLIP) if sig==1 else close*(1-SLIP)
sh=int(CAPITAL*MAXPOS/ep)
if sh>0:
dsl=ep*(1-0.008*sig); dtp=ep*(1+0.012*sig)
psl=float(sl) if sl is not None and not pd.isna(sl) else dsl
ptp=float(tp) if tp is not None and not pd.isna(tp) else dtp
pos={'ticker':row.get('ticker',''),'entry_time':dt,'entry_price':ep,
'direction':sig,'shares':sh,'sl':psl,'tp':ptp}
return trades, daily_pnl, equity
def metrics(name, all_trades, all_daily):
if not all_trades: return None
pnls=[t.pnl for t in all_trades]
w=[t for t in all_trades if t.pnl>0]; l=[t for t in all_trades if t.pnl<=0]
tr=sum(pnls)/CAPITAL
ds=pd.Series(all_daily).sort_index(); dr=ds/CAPITAL
sh=(dr.mean()/dr.std()*np.sqrt(252)) if len(dr)>1 and dr.std()>0 else 0
dd=dr[dr<0]; so=(dr.mean()/dd.std()*np.sqrt(252)) if len(dd)>0 and dd.std()>0 else 0
eq=CAPITAL+ds.cumsum(); mdd=abs(((eq-eq.cummax())/eq.cummax()).min()) if len(eq)>0 else 0
gp=sum(t.pnl for t in w) if w else 0; gl=abs(sum(t.pnl for t in l)) if l else 1
pf=gp/gl if gl>0 else 0
ex=Counter(t.exit_reason for t in all_trades); sp={}
for t in all_trades: sp[t.ticker]=sp.get(t.ticker,0)+t.pnl
return dict(strategy=name, total_trades=len(all_trades), win_rate=len(w)/len(all_trades)*100,
total_return_pct=tr*100, sharpe_ratio=sh, sortino_ratio=so, max_drawdown_pct=mdd*100,
profit_factor=pf, avg_pnl=np.mean(pnls), avg_win=np.mean([t.pnl for t in w]) if w else 0,
avg_loss=np.mean([t.pnl for t in l]) if l else 0, trades=all_trades, exits=ex, stock_pnl=sp)