Timeframe
4h
Direction
Long & Short
Stoploss
-3.0%
Trailing Stop
Yes
ROI
0m: 12.0%, 1440m: 8.0%, 4320m: 4.0%, 8640m: 0.0%
Interface Version
3
Startup Candles
200
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
ICT 4H V2 — Tuned variants
V2a: CHoCH-only, 5 of 5 conditions required (strictest)
V2b: Remove OB, focus on sweep + fib + trend (3 factors)
V2c: Long only, fib discount entry + trend alignment
"""
from pandas import DataFrame
import pandas as pd
import talib.abstract as ta
import numpy as np
from freqtrade.strategy import IStrategy
def _build_base(d):
d['swing_high'] = (d['high'] > d['high'].shift(1)) & (d['high'] > d['high'].shift(2)) & (d['high'] >= d['high'].shift(-1)) & (d['high'] >= d['high'].shift(-2))
d['swing_low'] = (d['low'] < d['low'].shift(1)) & (d['low'] < d['low'].shift(2)) & (d['low'] <= d['low'].shift(-1)) & (d['low'] <= d['low'].shift(-2))
d['last_sh'] = d['high'].where(d['swing_high']).ffill()
d['last_sl'] = d['low'].where(d['swing_low']).ffill()
d['bos_bull'] = d['close'] > d['last_sh'].shift(1)
d['bos_bear'] = d['close'] < d['last_sl'].shift(1)
d['bull_bos_6'] = d['bos_bull'].rolling(6).sum()
d['bear_bos_6'] = d['bos_bear'].rolling(6).sum()
d['choch_bear'] = d['bos_bear'] & (d['bull_bos_6'].shift(1) >= 2)
d['choch_bull'] = d['bos_bull'] & (d['bear_bos_6'].shift(1) >= 2)
d['hh_20'] = d['high'].rolling(20).max()
d['ll_20'] = d['low'].rolling(20).min()
d['sweep_high'] = (d['high'] >= d['hh_20'].shift(1) * 0.997) & (d['close'] < d['hh_20'].shift(1) * 0.995)
d['sweep_low'] = (d['low'] <= d['ll_20'].shift(1) * 1.003) & (d['close'] > d['ll_20'].shift(1) * 1.005)
d['swing_high_30'] = d['high'].rolling(30).max()
d['swing_low_30'] = d['low'].rolling(30).min()
sr = d['swing_high_30'] - d['swing_low_30']
d['fib_062'] = d['swing_high_30'] - sr * 0.618
d['fib_079'] = d['swing_high_30'] - sr * 0.786
d['fib_050'] = d['swing_high_30'] - sr * 0.50
d['fib_062_s'] = d['swing_low_30'] + sr * 0.618
d['fib_079_s'] = d['swing_low_30'] + sr * 0.786
d['in_discount'] = d['close'] <= d['fib_050']
d['in_premium'] = d['close'] >= d['fib_050']
d['e50'] = ta.EMA(d, 50)
d['e200'] = ta.EMA(d, 200)
d['rsi'] = ta.RSI(d, 14)
d['adx'] = ta.ADX(d, 14)
d['di_plus'] = ta.PLUS_DI(d, 14)
d['di_minus'] = ta.MINUS_DI(d, 14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], 10)
d['atr'] = ta.ATR(d, 14)
body = (d['close'] - d['open']).abs()
d['displacement'] = body > body.rolling(20).mean() * 1.5
return d
# ============================================================
# V2a: CHoCH + sweep + fib ONLY (pure ICT structure)
# ============================================================
class ICT_CHoCH_Pure(IStrategy):
INTERFACE_VERSION = 3; timeframe = '4h'; can_short = True
stoploss = -0.03; trailing_stop = True
trailing_stop_positive = 0.01; trailing_stop_positive_offset = 0.025
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.12, "1440": 0.08, "4320": 0.04, "8640": 0}
max_open_trades = 4; startup_candle_count = 200
process_only_new_candles = True; use_exit_signal = False
def populate_indicators(self, d, m):
d = _build_base(d)
# Stronger displacement
d['strong_displace'] = d['displacement'] & (d['vr'] > 1.2)
return d
def populate_entry_trend(self, d, m):
# LONG: CHoCH bull after bear run + sweep low + in discount + strong ADX
long_cond = (
d['choch_bull'] &
(d['sweep_low'] | d['in_discount']) &
d['strong_displace'] &
(d['adx'] > 22) & (d['di_plus'] > d['di_minus']) &
d['rsi'].between(30, 60) &
(d['volume'] > 0)
)
d.loc[long_cond, ['enter_long', 'enter_tag']] = (1, 'CH_L')
# SHORT: CHoCH bear after bull run + sweep high + in premium + strong ADX
short_cond = (
d['choch_bear'] &
(d['sweep_high'] | d['in_premium']) &
d['strong_displace'] &
(d['adx'] > 22) & (d['di_minus'] > d['di_plus']) &
d['rsi'].between(40, 70) &
(d['volume'] > 0)
)
d.loc[short_cond, ['enter_short', 'enter_tag']] = (1, 'CH_S')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V2b: Sweep-only — liquidity grab reversal
# ============================================================
class ICT_SweepOnly(IStrategy):
INTERFACE_VERSION = 3; timeframe = '4h'; can_short = True
stoploss = -0.025; trailing_stop = True
trailing_stop_positive = 0.008; trailing_stop_positive_offset = 0.020
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.10, "1440": 0.06, "4320": 0.03, "8640": 0}
max_open_trades = 4; startup_candle_count = 200
process_only_new_candles = True; use_exit_signal = False
def populate_indicators(self, d, m):
d = _build_base(d)
return d
def populate_entry_trend(self, d, m):
# LONG: sweep below 20-low then reversal candle closes green
long_cond = (
d['sweep_low'] &
(d['close'] > d['open']) & # reversal candle
(d['close'] > d['e50']) |
d['bos_bull'] &
(d['in_discount']) &
(d['rsi'] > 35) &
(d['volume'] > 0)
)
# Properly group the OR conditions
long_final = (
(d['sweep_low'] & (d['close'] > d['open']) & (d['rsi'] > 35)) |
(d['bos_bull'] & d['in_discount'] & (d['rsi'] > 35))
) & (d['volume'] > 0)
d.loc[long_final, ['enter_long', 'enter_tag']] = (1, 'SW_L')
short_final = (
(d['sweep_high'] & (d['close'] < d['open']) & (d['rsi'] < 65)) |
(d['bos_bear'] & d['in_premium'] & (d['rsi'] < 65))
) & (d['volume'] > 0)
d.loc[short_final, ['enter_short', 'enter_tag']] = (1, 'SW_S')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V2c: Long-only, fib discount entry with trend
# ============================================================
class ICT_LongOnly(IStrategy):
INTERFACE_VERSION = 3; timeframe = '4h'; can_short = False
stoploss = -0.03; trailing_stop = True
trailing_stop_positive = 0.01; trailing_stop_positive_offset = 0.025
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.12, "1440": 0.08, "4320": 0.04, "8640": 0}
max_open_trades = 3; startup_candle_count = 200
process_only_new_candles = True; use_exit_signal = False
def populate_indicators(self, d, m):
d = _build_base(d)
d['ema_cross'] = ta.EMA(d, 20) > ta.EMA(d, 50)
return d
def populate_entry_trend(self, d, m):
cond = (
(d['close'] > d['e200']) & # macro trend up
d['ema_cross'] & # EMA aligned
d['in_discount'] & # buying at a discount
(d['rsi'] < 50) & # not overbought
(d['sweep_low'] | (d['close'] < d['e50'])) & # swept or pulled back
(d['adx'] > 18) & (d['di_plus'] > d['di_minus']) &
(d['volume'] > 0)
)
d.loc[cond, ['enter_long', 'enter_tag']] = (1, 'L_disc')
return d
def populate_exit_trend(self, d, m): return d