The VolatilityFilteredIchimokuStrategy
is a trading
system that combines the comprehensive trend-following signals of the
Ichimoku Kinko Hyo indicator with a dynamic
volatility filter. This strategy aims to enter trades
only when a confirmed Ichimoku breakout occurs in a sufficiently
volatile market, thereby avoiding false signals during quiet periods.
All open positions are managed with a trailing stop-loss.
This strategy capitalizes on the robust trend signals provided by the Ichimoku Cloud but adds a layer of market condition filtering based on volatility.
An entry signal is generated when three key Ichimoku conditions align, along with a volatility confirmation:
Ichimoku Breakout and Alignment:
chikou
periods
ago’s high. For a short entry, Chikou Span is below the price
chikou
periods ago’s low.Volatility Filter:
All these conditions must be met simultaneously for a buy or sell order to be placed.
Upon successful entry, the position is managed solely by a
trailing stop-loss order. This means that once a
position is opened, a stop-loss order is automatically placed to trail
the market price by a fixed trail_percent
. As the market
moves favorably, the stop-loss adjusts upwards for long positions (or
downwards for short positions) to lock in profits, but it never moves
against the trade if the market reverses, thus protecting gains and
limiting losses.
The strategy is implemented in backtrader
as
follows:
import backtrader as bt
class VolatilityFilteredIchimokuStrategy(bt.Strategy):
"""
Trades on a confirmed breakout from the Ichimoku Cloud, but only when
market volatility (measured by ATR) is above a dynamic threshold.
1. Price breaks out of the Kumo, Tenkan/Kijun cross, Chikou Span confirmation.
2. Volatility Filter: Current ATR must be above its N-period average.
3. Exit is managed with a trailing stop-loss.
"""
= (
params 'tenkan', 7),
('kijun', 14),
('senkou', 30),
('senkou_lead', 14),
('chikou', 14),
('atr_period', 7),
('atr_filter_period', 30), # Period to calculate average ATR for threshold
('trail_percent', 0.01),
(
)
def __init__(self):
self.order = None
self.dataclose = self.datas[0].close
# Add the Ichimoku indicator
self.ichimoku = bt.indicators.Ichimoku(
self.datas[0],
=self.p.tenkan,
tenkan=self.p.kijun,
kijun=self.p.senkou,
senkou=self.p.senkou_lead,
senkou_lead=self.p.chikou
chikou
)
# Add ATR and its average for volatility filtering
self.atr = bt.indicators.ATR(self.datas[0], period=self.p.atr_period)
self.avg_atr = bt.indicators.SMA(self.atr, period=self.p.atr_filter_period)
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.sell(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
elif order.issell():
self.buy(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
self.order = None
def next(self):
if self.order:
return
# Ensure enough data for all indicators
if len(self) < max(self.p.senkou_lead, self.p.chikou, self.p.atr_filter_period):
return
# Volatility filter condition: Current ATR must be above its average
= self.atr[0] > self.avg_atr[0]
is_volatile_enough
if not self.position:
# Bullish Entry Conditions
= (self.dataclose[0] > self.ichimoku.senkou_span_a[0] and
is_above_cloud self.dataclose[0] > self.ichimoku.senkou_span_b[0])
= self.ichimoku.tenkan_sen[0] > self.ichimoku.kijun_sen[0]
is_tk_cross_bullish # Chikou Span must be above price 'chikou' periods ago's high for confirmation
= self.ichimoku.chikou_span[0] > self.datas[0].high[-self.p.chikou]
is_chikou_bullish
if is_above_cloud and is_tk_cross_bullish and is_chikou_bullish and is_volatile_enough:
self.order = self.buy()
# Bearish Entry Conditions
= (self.dataclose[0] < self.ichimoku.senkou_span_a[0] and
is_below_cloud self.dataclose[0] < self.ichimoku.senkou_span_b[0])
= self.ichimoku.tenkan_sen[0] < self.ichimoku.kijun_sen[0]
is_tk_cross_bearish # Chikou Span must be below price 'chikou' periods ago's low for confirmation
= self.ichimoku.chikou_span[0] < self.datas[0].low[-self.p.chikou]
is_chikou_bearish
if is_below_cloud and is_tk_cross_bearish and is_chikou_bearish and is_volatile_enough:
self.order = self.sell()
params
)The strategy’s behavior is configured through its parameters:
tenkan
: Period for the Tenkan-Sen (Conversion Line) of
Ichimoku.kijun
: Period for the Kijun-Sen (Base Line) of
Ichimoku.senkou
: Period for calculating Senkou Span A and Senkou
Span B (leading spans of the Kumo/Cloud).senkou_lead
: How many periods Senkou Span A and B are
projected forward.chikou
: Period for the Chikou Span (Lagging Span).atr_period
: Period for the Average True Range (ATR)
calculation.atr_filter_period
: Period for the Simple Moving Average
(SMA) of ATR, used as the dynamic volatility threshold.trail_percent
: The percentage at which the trailing
stop-loss trails the market price.__init__
)In the __init__
method, all necessary indicators are set
up:
self.order
: This variable keeps track of any pending
orders to prevent multiple orders being placed on the same bar.self.dataclose
: A convenient reference to the close
price line of the data feed.self.ichimoku
: The Ichimoku Kinko Hyo
indicator is instantiated with its various components.self.atr
: The Average True Range (ATR)
indicator is instantiated to measure current volatility.self.avg_atr
: A Simple Moving Average (SMA) of the
self.atr
is created. This avg_atr
serves as
the dynamic threshold for the volatility filter.notify_order
)This method is automatically called by backtrader
whenever an order’s status changes. It is crucial for managing the
trailing stop-loss:
buy
order is completed (meaning
a long position has just been opened), a corresponding sell
trailing stop order is immediately placed using
self.sell(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
.sell
order is completed
(meaning a short position has just been opened), a corresponding
buy trailing stop order is placed using
self.buy(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
.self.order = None
ensures that the order reference is
cleared once the order is no longer in a submitted or accepted state,
allowing for new orders on subsequent bars if conditions are met.next
)The next
method contains the core trading logic and is
executed on each new bar of data:
if self.order: return
ensures that no new actions are taken
if a previous order is still pending.if len(self) < max(self.p.senkou_lead, self.p.chikou, self.p.atr_filter_period): return
ensures that all indicators have enough historical data to provide valid
readings before the strategy attempts any trading decisions.is_volatile_enough = self.atr[0] > self.avg_atr[0]
checks if the current market volatility (measured by
atr[0]
) is above its recent average
(avg_atr[0]
). Entries are only considered if this condition
is true.if not self.position
): The strategy only looks
for entries if there is no position currently open.
is_above_cloud
: Current dataclose[0]
is
above both senkou_span_a[0]
and
senkou_span_b[0]
(price is above the Kumo).is_tk_cross_bullish
: tenkan_sen[0]
is
above kijun_sen[0]
(Tenkan-Sen has crossed above
Kijun-Sen).is_chikou_bullish
: chikou_span[0]
is above
the high
of chikou
periods ago. This confirms
that price action is strong and has remained above prior
resistance.is_volatile_enough
is True
, a buy
order is placed.is_below_cloud
: Current dataclose[0]
is
below both senkou_span_a[0]
and
senkou_span_b[0]
(price is below the Kumo).is_tk_cross_bearish
: tenkan_sen[0]
is
below kijun_sen[0]
(Tenkan-Sen has crossed below
Kijun-Sen).is_chikou_bearish
: chikou_span[0]
is below
the low
of chikou
periods ago. This confirms
strong bearish price action.is_volatile_enough
is True
, a
sell
order is placed.To comprehensively evaluate the strategy’s performance, a rolling backtest is used. This method assesses the strategy over multiple, successive time windows, providing a more robust view of its consistency compared to a single, fixed backtest.
from collections import deque
import backtrader as bt
import numpy as np
import pandas as pd
import yfinance as yf
import dateutil.relativedelta as rd
# Assuming VolatilityFilteredIchimokuStrategy class is defined above this section.
def run_rolling_backtest(
ticker,
start,
end,
window_months,
strategy_class,=None
strategy_params
):= strategy_params or {}
strategy_params = []
all_results = pd.to_datetime(start)
start_dt = pd.to_datetime(end)
end_dt = start_dt
current_start
while True:
= current_start + rd.relativedelta(months=window_months)
current_end if current_end > end_dt:
= end_dt
current_end if current_start >= current_end:
break
print(f"\nROLLING BACKTEST: {current_start.date()} to {current_end.date()}")
= yf.download(ticker, start=current_start, end=current_end, auto_adjust=False, progress=False)
data
if data.empty or len(data) < 90:
print("Not enough data for this period. Skipping.")
+= rd.relativedelta(months=window_months)
current_start continue
if isinstance(data.columns, pd.MultiIndex):
= data.droplevel(1, 1)
data
= data['Close'].iloc[0]
start_price = data['Close'].iloc[-1]
end_price = (end_price - start_price) / start_price * 100
benchmark_ret
= bt.feeds.PandasData(dataname=data)
feed = bt.Cerebro()
cerebro
**strategy_params)
cerebro.addstrategy(strategy_class,
cerebro.adddata(feed)100000)
cerebro.broker.setcash(=0.001)
cerebro.broker.setcommission(commission=95)
cerebro.addsizer(bt.sizers.PercentSizer, percents
= cerebro.broker.getvalue()
start_val try:
cerebro.run()except Exception as e:
print(f"Error running backtest for {current_start.date()} to {current_end.date()}: {e}")
+= rd.relativedelta(months=window_months)
current_start continue
= cerebro.broker.getvalue()
final_val = (final_val - start_val) / start_val * 100
strategy_ret
all_results.append({'start': current_start.date(),
'end': current_end.date(),
'strategy_return_pct': strategy_ret,
'benchmark_return_pct': benchmark_ret,
'final_value': final_val,
})
print(f"Strategy Return: {strategy_ret:.2f}% | Buy & Hold Return: {benchmark_ret:.2f}%")
+= rd.relativedelta(months=window_months)
current_start
if current_start > end_dt:
break
return pd.DataFrame(all_results)
ticker
, start
, end
: The asset
symbol and the overall historical period for the backtest.window_months
: The duration of each individual
backtesting window in months.strategy_class
: The backtrader.Strategy
class to be tested (e.g.,
VolatilityFilteredIchimokuStrategy
).strategy_params
: A dictionary to pass specific
parameters to the chosen strategy for each run.yfinance
with
auto_adjust=False
and droplevel
applied.backtrader.Cerebro
instance.strategy_class
and its
parameters.Pandas DataFrame
containing the
comprehensive results for each rolling window.
The VolatilityFilteredIchimokuStrategy
offers a
sophisticated approach to trend trading by leveraging the comprehensive
nature of the Ichimoku Kinko Hyo and enhancing it with
a dynamic volatility filter. By ensuring trades are
only entered when volatility is conducive to strong directional moves,
it aims to reduce false signals and improve overall strategy robustness.
The implementation of a trailing stop-loss provides
essential risk management. The use of a rolling
backtest is vital for thoroughly evaluating the strategy’s
consistency and adaptability across various market conditions, providing
a more reliable assessment of its performance.