Building the Strategy Class

Strategy Class Architecture
The Strategy class handles signal processing and position management

Ichimoku Trading Series: Part 8 of 10 | ← Previous | View Full Series

The backtesting.py Framework

The backtesting library uses a class-based approach where:

  • init() runs once at the start
  • next() runs on every candle

Complete Strategy Class

from backtesting import Strategy

class SignalStrategy(Strategy):
    """
    Ichimoku + EMA trend-following strategy.
    
    Entry: Pre-computed signal column (+1 long, -1 short)
    Exit: ATR-based SL and RR-based TP
    """
    
    # Class-level parameters (can be optimized)
    atr_mult_sl: float = 1.5   # SL distance = ATR x this
    rr_mult_tp:  float = 2.0   # TP distance = SL x this
    
    def init(self):
        """Initialize indicators (we pre-compute, so nothing needed here)."""
        pass
    
    def next(self):
        """Called on every bar. Check for signals and manage positions."""
        i = -1  # Current bar
        signal = int(self.data.signal[i])   # +1 long, -1 short, 0 none
        close  = float(self.data.Close[i])
        atr    = float(self.data.ATR[i])
        
        # Safety check
        if not (atr > 0):
            return
        
        # --- Manage open trades ---
        if self.position:
            # Let SL/TP handle exits automatically
            return
        
        # --- New entry logic ---
        sl_dist = atr * self.atr_mult_sl
        tp_dist = sl_dist * self.rr_mult_tp
        
        if signal == 1:  # LONG entry
            sl = close - sl_dist
            tp = close + tp_dist
            self.buy(size=0.99, sl=sl, tp=tp)
        
        elif signal == -1:  # SHORT entry
            sl = close + sl_dist
            tp = close - tp_dist
            self.sell(size=0.99, sl=sl, tp=tp)

Key Design Decisions

1. Pre-Computed Signals

We calculate signals BEFORE backtesting (in pandas), then the strategy just reads them. This is cleaner and faster.

2. Position Check

if self.position:
    return

We do not stack trades — one position at a time.

3. Size = 0.99

self.buy(size=0.99, sl=sl, tp=tp)

Using 99% of available equity leaves room for rounding.

Running the Backtest

def run_backtest(symbol, start, end, interval, cash, commission, show_plot=True):
    # Prepare data
    df = fetch_data(symbol, start, end, interval)
    df = add_ichimoku(df)
    df["EMA"] = ta.ema(df.Close, length=100)
    df = MovingAverageSignal(df, back_candles=7)
    df = createSignals(df, lookback_window=10, min_confirm=7)
    df = df.dropna()
    
    # Create backtest
    bt = Backtest(
        df,
        SignalStrategy,
        cash=cash,
        commission=commission,
        trade_on_close=True,
        exclusive_orders=True,
        margin=1/10,  # 10x leverage
    )
    
    # Run and display results
    stats = bt.run()
    print(f"\n=== {symbol} Signal Strategy ===")
    print(stats)
    
    if show_plot:
        bt.plot(open_browser=False)
    
    return stats, df, bt

# Execute
stats, df, bt = run_backtest(
    symbol="USDCHF=X",
    start="2023-10-01",
    end="2024-10-01", 
    interval="4h",
    cash=1_000_000,
    commission=0.0002
)

Example Output

=== USDCHF=X Signal Strategy ===
Return [%]                     28.5
Sharpe Ratio                    1.02
Max. Drawdown [%]              -6.3
Avg. Drawdown [%]              -3.7
Win Rate [%]                   53.8
# Trades                         13
Exposure Time [%]              42.1

Coming Up Next: Our strategy is coded — now let us optimize parameters using grid search and visualize results with heat maps. Continue to Part 9 →

CategoriesAI

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.