Timeframe
15m
Direction
Long & Short
Stoploss
-2.0%
Trailing Stop
Yes
ROI
0m: 6.0%, 480m: 4.0%, 1440m: 2.0%, 4320m: 0.0%
Interface Version
3
Startup Candles
300
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
ICT/SMC Strategy — Smart Money Concepts for Freqtrade
Efficient implementation using vectorized operations
15m timeframe, Binance Futures
Core ICT Concepts Implemented:
1. Market Structure (HH/HL/BOS/CHoCH)
2. Liquidity Sweeps
3. Order Blocks (simplified)
4. Fair Value Gaps (simplified)
5. Displacement / Momentum
6. Session Killzones
"""
from pandas import DataFrame
import pandas as pd
import talib.abstract as ta
import numpy as np
from freqtrade.strategy import IStrategy
class ICT_SMC_Strategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.02
trailing_stop = True
trailing_stop_positive = 0.005
trailing_stop_positive_offset = 0.012
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.06, "480": 0.04, "1440": 0.02, "4320": 0}
max_open_trades = 6
startup_candle_count = 300
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d: DataFrame, m: dict) -> DataFrame:
# ============================================================
# 1. SWING HIGHS/LOWS (vectorized)
# ============================================================
# Swing high: candle high > 3 before and 3 after
d['swing_high'] = (
(d['high'] > d['high'].shift(1)) &
(d['high'] > d['high'].shift(2)) &
(d['high'] > d['high'].shift(3)) &
(d['high'] >= d['high'].shift(-1)) &
(d['high'] >= d['high'].shift(-2)) &
(d['high'] >= d['high'].shift(-3))
)
# Swing low: candle low < 3 before and 3 after
d['swing_low'] = (
(d['low'] < d['low'].shift(1)) &
(d['low'] < d['low'].shift(2)) &
(d['low'] < d['low'].shift(3)) &
(d['low'] <= d['low'].shift(-1)) &
(d['low'] <= d['low'].shift(-2)) &
(d['low'] <= d['low'].shift(-3))
)
# Forward-fill last swing high/low values
d['last_sh'] = d['high'].where(d['swing_high']).ffill()
d['last_sl'] = d['low'].where(d['swing_low']).ffill()
# ============================================================
# 2. BREAK OF STRUCTURE (BOS)
# ============================================================
# Bull BOS: close breaks above last swing high
d['bos_bull'] = d['close'] > d['last_sh'].shift(1)
# Bear BOS: close breaks below last swing low
d['bos_bear'] = d['close'] < d['last_sl'].shift(1)
# CHoCH: BOS after opposite trend (simplified: 2+ BOS in one direction, then opposite BOS)
d['bull_count'] = d['bos_bull'].rolling(24).sum() # ~6 hours
d['bear_count'] = d['bos_bear'].rolling(24).sum()
d['choch_bear'] = d['bos_bear'] & (d['bull_count'].shift(1) >= 2) # bear BOS after bull run
d['choch_bull'] = d['bos_bull'] & (d['bear_count'].shift(1) >= 2) # bull BOS after bear run
# ============================================================
# 3. LIQUIDITY SWEEPS
# ============================================================
# Sweep of 20-bar highs/lows
d['hh_20'] = d['high'].rolling(20).max()
d['ll_20'] = d['low'].rolling(20).min()
# High sweep: price hits 20-bar high then closes back below
d['sweep_high'] = (d['high'] >= d['hh_20'].shift(1) * 0.998) & (d['close'] < d['hh_20'].shift(1) * 0.997)
# Low sweep: price hits 20-bar low then closes back above
d['sweep_low'] = (d['low'] <= d['ll_20'].shift(1) * 1.002) & (d['close'] > d['ll_20'].shift(1) * 1.003)
# Sweep of previous candle extremes
d['sweep_prev_high'] = (d['high'] > d['high'].shift(1)) & (d['close'] < d['high'].shift(1))
d['sweep_prev_low'] = (d['low'] < d['low'].shift(1)) & (d['close'] > d['low'].shift(1))
# ============================================================
# 4. ORDER BLOCKS (simplified: last opposite candle before displacement)
# ============================================================
# Displacement: candle body > 2x 20-period average body
d['body'] = (d['close'] - d['open']).abs()
d['avg_body'] = d['body'].rolling(20).mean()
d['displacement_up'] = (d['close'] > d['open']) & (d['body'] > d['avg_body'] * 1.8)
d['displacement_down'] = (d['close'] < d['open']) & (d['body'] > d['avg_body'] * 1.8)
d['displacement'] = d['displacement_up'] | d['displacement_down']
# Bull OB: bear candle before displacement up = reversal zone
d['ob_bull'] = d['displacement_up'] & (d['close'].shift(1) < d['open'].shift(1))
# Bear OB: bull candle before displacement down
d['ob_bear'] = d['displacement_down'] & (d['close'].shift(1) > d['open'].shift(1))
# OB candle range becomes potential entry zone
d['ob_bull_high'] = np.where(d['ob_bull'], d['high'].shift(1), np.nan)
d['ob_bull_low'] = np.where(d['ob_bull'], d['low'].shift(1), np.nan)
d['ob_bear_high'] = np.where(d['ob_bear'], d['high'].shift(1), np.nan)
d['ob_bear_low'] = np.where(d['ob_bear'], d['low'].shift(1), np.nan)
# OB retest: price returns to OB zone within 12 candles of OB formation
d['ob_bull_retest'] = False
d['ob_bear_retest'] = False
for lookback in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]:
d['ob_bull_retest'] = d['ob_bull_retest'] | (
d['ob_bull'].shift(lookback) &
(d['low'] <= d['ob_bull_high'].shift(lookback)) &
(d['close'] >= d['ob_bull_low'].shift(lookback))
)
d['ob_bear_retest'] = d['ob_bear_retest'] | (
d['ob_bear'].shift(lookback) &
(d['high'] >= d['ob_bear_low'].shift(lookback)) &
(d['close'] <= d['ob_bear_high'].shift(lookback))
)
# ============================================================
# 5. FAIR VALUE GAP (FVG) — simplified vectorized
# ============================================================
# Bull FVG: C[2] low > C[0] high → unfilled gap
d['fvg_bull'] = d['low'].shift(2) > d['high'].shift(0) * 1.002
d['fvg_bear'] = d['high'].shift(2) < d['low'].shift(0) * 0.998
# FVG retest within 12 candles
d['fvg_bull_retest'] = False
d['fvg_bear_retest'] = False
for lb in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]:
d['fvg_bull_retest'] = d['fvg_bull_retest'] | (
d['fvg_bull'].shift(lb) & (d['low'] <= d['high'].shift(lb - 2))
)
d['fvg_bear_retest'] = d['fvg_bear_retest'] | (
d['fvg_bear'].shift(lb) & (d['high'] >= d['low'].shift(lb - 2))
)
# ============================================================
# 6. FIBONACCI / OTE ZONE
# ============================================================
# Use rolling 96-period (1 day) swing high/low for fib levels
d['swing_high_96'] = d['high'].rolling(96).max()
d['swing_low_96'] = d['low'].rolling(96).min()
swing_range = d['swing_high_96'] - d['swing_low_96']
d['fib_062'] = d['swing_high_96'] - swing_range * 0.618 # OTE entry
d['fib_079'] = d['swing_high_96'] - swing_range * 0.786 # OTE boundary
d['fib_062_s'] = d['swing_low_96'] + swing_range * 0.618 # Short OTE
d['fib_079_s'] = d['swing_low_96'] + swing_range * 0.786
# In OTE zone for long: price between 0.62 and 0.79 retrace
d['in_ote_long'] = (d['close'] >= d['fib_079']) & (d['close'] <= d['fib_062'])
d['in_ote_short'] = (d['close'] >= d['fib_062_s']) & (d['close'] <= d['fib_079_s'])
# ============================================================
# 7. TRADITIONAL INDICATORS
# ============================================================
d['e20'] = ta.EMA(d, timeperiod=20)
d['e50'] = ta.EMA(d, timeperiod=50)
d['rsi'] = ta.RSI(d, timeperiod=14)
d['atr'] = ta.ATR(d, timeperiod=14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
# ============================================================
# 8. KILLZONE — London/NY session
# ============================================================
d['hour'] = pd.to_datetime(d['date']).dt.hour
d['in_killzone'] = d['hour'].between(7, 9) | d['hour'].between(12, 14)
return d
def populate_entry_trend(self, d: DataFrame, m: dict) -> DataFrame:
# ===== ICT LONG ENTRY =====
# Structure: channel_h/bos_bull/choch_bull confirming uptrend
structure_bull = d['bos_bull'] | d['close'] > d['e50']
# Setup: liquidity sweep below recent low (stop hunt) + OB/FVG entry
entry_setup_long = (
d['sweep_low'] | d['sweep_prev_low']
) & (
d['ob_bull_retest'] | d['fvg_bull_retest'] | d['in_ote_long']
)
# Confirmation: displacement + RSI
confirm_long = (
d['displacement_up'] &
d['rsi'].between(30, 70) &
d['vr'] > 0.6
)
d.loc[
structure_bull & entry_setup_long & confirm_long & (d['volume'] > 0),
['enter_long', 'enter_tag']
] = (1, 'ICT_L')
# ===== ICT SHORT ENTRY =====
structure_bear = d['bos_bear'] | d['close'] < d['e50']
entry_setup_short = (
d['sweep_high'] | d['sweep_prev_high']
) & (
d['ob_bear_retest'] | d['fvg_bear_retest'] | d['in_ote_short']
)
confirm_short = (
d['displacement_down'] &
d['rsi'].between(30, 70) &
d['vr'] > 0.6
)
d.loc[
structure_bear & entry_setup_short & confirm_short & (d['volume'] > 0),
['enter_short', 'enter_tag']
] = (1, 'ICT_S')
return d
def populate_exit_trend(self, d: DataFrame, m: dict) -> DataFrame:
return d