So you’ve built your algo and backtested it. Now you might be thinking:
“It worked okay, but can I make it better?”
Absolutely. Algo trading is like cooking — you start with a recipe, but tweak it based on taste (aka data).
Let’s look at how to fine-tune your strategy using simple techniques.
Ask yourself:
import yfinance as yf import pandas as pd import numpy as np
#------------------------------- #Helpers #------------------------------- def ema(series, span): return series.ewm(span=span, adjust=False).mean()
def rsi(series, period=14): delta = series.diff() gain = (delta.clip(lower=0)).rolling(period).mean() loss = (-delta.clip(upper=0)).rolling(period).mean() rs = gain / (loss.replace(0, np.nan)) out = 100 - (100 / (1 + rs)) return out.fillna(50)
def run_backtest(df, dip_pct=0.05, use_rsi=False, use_ema=False, use_vol=False, hold_days=3, vol_mult=1.5, lookback_high=20, vol_window=20): d = df.copy()
#Indicators
d['HighN'] = d['High'].rolling(lookback_high).max()
d['EMA20'] = ema(d['Close'], 20)
d['RSI14'] = rsi(d['Close'], 14)
d['VolAvg'] = d['Volume'].rolling(vol_window).mean()
#Entry condition: dip X% below recent high
entry_base = d['Close'] <= (1 - dip_pct) * d['HighN']
#Filters
cond = entry_base.copy()
if use_rsi:
cond &= d['RSI14'] < 30
if use_ema:
cond &= d['Close'] < d['EMA20']
if use_vol:
cond &= d['Volume'] > (vol_mult * d['VolAvg'])
#First true in any streak (avoid multiple entries on consecutive days)
entry_signal = cond & cond.shift(1).fillna(False).eq(False)
entries = d.index[entry_signal]
#---- FIXED: prevent overlapping trades with an index gate ----
trades = []
next_allowed_idx = -1 # block new entries until after current trade exits
for ts in entries:
idx = d.index.get_loc(ts)
#skip if a prior trade hasn't finished yet
if idx <= next_allowed_idx:
continue
#enter at the next bar's open
if idx + 1 >= len(d):
break
entry_time = d.index[idx + 1]
entry_px = d.loc[entry_time, 'Open']
#exit after hold_days at the next bar's open (fixed horizon)
exit_bar_idx = min(idx + 1 + hold_days, len(d) - 1)
if exit_bar_idx + 1 < len(d):
exit_time = d.index[exit_bar_idx + 1]
exit_px = d.loc[exit_time, 'Open']
next_allowed_idx = exit_bar_idx + 1
else:
exit_time = d.index[exit_bar_idx]
exit_px = d.loc[exit_time, 'Close']
next_allowed_idx = exit_bar_idx
ret = (exit_px - entry_px) / entry_px
trades.append({
'EntryTime': entry_time, 'EntryPrice': round(entry_px, 2),
'ExitTime': exit_time, 'ExitPrice': round(exit_px, 2),
'Return_%': round(ret * 100, 2)
})
trades_df = pd.DataFrame(trades)
if trades_df.empty:
summary = {
'dip_pct': dip_pct, 'RSI<30': use_rsi, 'Close<EMA20': use_ema,
'VolSpike': use_vol, 'Trades': 0, 'HitRate_%': 0.0,
'AvgRet_%': 0.0, 'TotalRet_%': 0.0
}
return trades_df, summary
hitrate = (trades_df['Return_%'] > 0).mean() * 100
avgret = trades_df['Return_%'].mean()
total_ret = (np.prod(1 + trades_df['Return_%']/100) - 1) * 100 # naive compounding
summary = {
'dip_pct': dip_pct, 'RSI<30': use_rsi, 'Close<EMA20': use_ema,
'VolSpike': use_vol, 'Trades': len(trades_df),
'HitRate_%': round(hitrate, 2),
'AvgRet_%': round(avgret, 2),
'TotalRet_%': round(total_ret, 2)
}
return trades_df, summary
#------------------------------- #Data #------------------------------- symbol = "AAPL" #replace with your ticker, e.g., "RELIANCE.NS" data = yf.download(symbol, period="2y", interval="1d").dropna()
#------------------------------- #Parameter grid (4%, 5%, 6% dips + filters on/off) #------------------------------- dip_list = [0.04, 0.05, 0.06] bools = [False, True] HOLD_DAYS = 3 summaries = [] example_trades = {}
for dip in dip_list: for rsi_on in bools: for ema_on in bools: for vol_on in bools: trades_df, summary = run_backtest( data, dip_pct=dip, use_rsi=rsi_on, use_ema=ema_on, use_vol=vol_on, hold_days=HOLD_DAYS ) summaries.append(summary) example_trades[(dip, rsi_on, ema_on, vol_on)] = trades_df
results = pd.DataFrame(summaries).sort_values('TotalRet_%', ascending=False) print("\nTop parameter sets (sorted by TotalRet_%):\n") print(results.head(10).to_string(index=False))
#Inspect trade log for the best combo best = results.iloc[0] best_key = (best['dip_pct'], best['RSI<30'], best['Close<EMA20'], best['VolSpike']) print("\nSample trades for best combo:\n", example_trades[best_key].head(10))
When you look at the backtest results, you’ll see different settings being tested. Each parameter here controls how strict or loose your algo is when entering trades. Think of them as dials you can adjust to find the sweet spot.
These filters help avoid buying into falling knives or noisy moves.
Profit and stop-loss levels are** super important.**
Try variations:
What you want is a good reward-to-risk ratio — ideally at least 1.5:1 or 2:1.
Also consider:
#Example: Optimize exit conditions profit_targets = [0.025, 0.03, 0.035] #2.5%, 3%, 3.5% stop_losses = [0.015, 0.02, 0.025] #1.5%, 2%, 2.5%
print("Profit Target | Stop Loss | R:R Ratio") for pt in profit_targets: for sl in stop_losses: rr = round(pt / sl, 2) print(f"{pt100:.1f}%\t {sl100:.1f}%\t {rr}:1")
#Example trailing stop-loss function def apply_trailing_stop(entry_price, current_price, trail_pct=0.01): """ Locks in profit as price moves up. trail_pct = 1% means stop is 1% below highest seen price. """ highest_price = max(entry_price, current_price) stop_price = highest_price * (1 - trail_pct) return stop_price
#Example time-based exit (pseudo logic) import time entry_time = time.time() max_hold_minutes = 30 while True: if time.time() - entry_time > max_hold_minutes * 60: print("Time-based exit triggered.") break #Check price updates here...
Backtest the same strategy on different timeframes:
Some strategies work best intraday, others do better swing-style. If your algo is whipsawed in 5-min, try 15-min or higher for cleaner trends.
Avoid going against the trend. You can filter trades based on:
import pandas as pd import yfinance as yf
#Download example stock data symbol = "RELIANCE.NS" df = yf.download(symbol, period="6mo", interval="1d")
#Calculate 200 EMA df['EMA200'] = df['Close'].ewm(span=200, adjust=False).mean()
#Mock India VIX value (in real use, fetch from NSE API or data provider) india_vix = 19 #Example
#Define filters df['Trend_Filter'] = df['Close'] > df['EMA200'] #True = bullish volatility_filter = india_vix <= 18 #True = normal volatility
#Apply combined filter df['Trade_OK'] = df['Trend_Filter'] & volatility_filter
print(df[['Close', 'EMA200', 'Trend_Filter', 'Trade_OK']].tail())
Adding simple context filters can drastically improve accuracy.
Every time you tweak something, re-run your backtest and check:
Keep comparing versions to see if you’re really improving or just overfitting.
You’re not looking for a 100% win-rate robot. You're building a system that works most of the time, keeps losses small, and keeps you consistent.
Remember: Even a 55–60% win rate with a 2:1 reward-risk ratio is a decent outcome.
Disclaimer: This article is for informational purposes only and does not constitute financial advice. It is not produced by the desk of the Kotak Securities Research Team, nor is it a report published by the Kotak Securities Research Team. The information presented is compiled from several secondary sources available on the internet and may change over time. Investors should conduct their own research and consult with financial professionals before making any investment decisions. Read the full disclaimer here.
Investments in securities market are subject to market risks, read all the related documents carefully before investing. Brokerage will not exceed SEBI prescribed limit. The securities are quoted as an example and not as a recommendation. SEBI Registration No-INZ000200137 Member Id NSE-08081; BSE-673; MSE-1024, MCX-56285, NCDEX-1262.
Disclaimer: This article is for informational purposes only and does not constitute financial advice. It is not produced by the desk of the Kotak Securities Research Team, nor is it a report published by the Kotak Securities Research Team. The information presented is compiled from several secondary sources available on the internet and may change over time. Investors should conduct their own research and consult with financial professionals before making any investment decisions. Read the full disclaimer here.
Investments in securities market are subject to market risks, read all the related documents carefully before investing. Brokerage will not exceed SEBI prescribed limit. The securities are quoted as an example and not as a recommendation. SEBI Registration No-INZ000200137 Member Id NSE-08081; BSE-673; MSE-1024, MCX-56285, NCDEX-1262.
Explore our comprehensive video library that blends expert market insights with Kotak's innovative financial solutions to support your goals.