Products
Platform
Research
Market
Learn
Partner
Support
IPO
Logo_light
Module 6
Optimizing and Sustaining Your Algo
Course Index

Chapter 1 | 2 min read

Tweak & Improve Your Strategy

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:

  • Is the entry condition too tight? (Very few trades)
  • Or too loose? (Too many random trades)

For example:

  • If you're buying at 5% dip, try 4% or 6% and see what changes.
  • Add filters like:
    • RSI < 30 (oversold)
    • Price below 20 EMA
    • Volume spike above average
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.

dip_pct (Dip Percentage)

  • This is how much the stock needs to fall from its recent high before the algo considers it a buying opportunity.
  • Example: If dip_pct = 0.05, the algo waits for a 5% drop from the 20-day high before triggering a signal.
  • Smaller dips (4%) mean more frequent trades; bigger dips (6%) mean fewer but possibly stronger signals.

RSI<30 (Oversold Filter)

  • RSI (Relative Strength Index) measures whether a stock is overbought or oversold.
  • If enabled, the algo only buys when RSI is below 30 — a sign that the stock might be oversold and due for a bounce.
  • This filter reduces false signals during strong downtrends.

Close<EMA20 (Trend Filter)

  • EMA20 is the 20-day Exponential Moving Average, a common trend indicator.
  • If enabled, the algo only buys if the price is below the EMA20, meaning it’s catching stocks trading at a discount relative to their recent trend.
  • Helps avoid buying too high during sideways markets.

VolSpike (Volume Spike Filter)

  • Compares today’s trading volume against the recent average.
  • If enabled, the algo requires volume to be at least 1.5× the average volume (adjustable by vol_mult).
  • Idea: big volume spikes = strong institutional interest, making signals more reliable.

hold_days (Holding Period)

  • The number of days the trade is held before exiting.
  • Here it’s fixed at 3 days, but you can tweak it to see if shorter or longer holds perform better.
  • Short holds mean quick trades; longer holds let you capture bigger swings.

Performance Metrics (What You’ll See in Results)

  • Trades: Number of trades taken for each parameter set.
  • HitRate_%: % of trades that were profitable.
  • AvgRet_%: Average return per trade.
  • TotalRet_%: Overall compounded return if you had followed that strategy

These filters help avoid buying into falling knives or noisy moves.

Profit and stop-loss levels are** super important.**

Try variations:

  • Profit Target: 2.5%, 3%, 3.5%
  • Stop-Loss: 1.5%, 2%, 2.5%

What you want is a good reward-to-risk ratio — ideally at least 1.5:1 or 2:1.

Also consider:

  • Trailing stop-loss: Lock profits as price moves up
  • Time-based exit: Exit trade after X minutes/hours if target is not hit
# 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"{pt*100:.1f}%\t     {sl*100:.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:

  • 5-min, 15-min, or 1-hour candles

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:

  • Price above 200 EMA = bullish
  • India VIX above 18 = volatile market (reduce trade size or stay out)
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:

  • Has win-rate improved?
  • Is drawdown reduced?
  • Are trades more consistent?

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.

Is this chapter helpful?
Previous
Algo Strategy: Sentiment Driven Trades
Next
Managing Risk Like a Pro

Discover our extensive knowledge center

Explore our comprehensive video library that blends expert market insights with Kotak's innovative financial solutions to support your goals.