
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 startnext()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
