Professional Smart Money Concepts Strategy with Volume Detection
Timeframe
15m
Direction
Long Only
Stoploss
-8.0%
Trailing Stop
No
ROI
0m: 20.0%, 15m: 10.0%, 30m: 5.0%, 60m: 2.0%
Interface Version
3
Startup Candles
N/A
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np
import pandas as pd
from pandas import DataFrame
from datetime import datetime, timedelta
from typing import Optional, Union
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IntParameter, IStrategy, merge_informative_pair)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class ClaudeSmartMoney(IStrategy):
"""
Professional Smart Money Concepts Strategy with Volume Detection
This strategy combines institutional smart money concepts with advanced volume analysis:
- Order blocks and fair value gaps identification
- Liquidity sweeps and stop hunts detection
- Volume profile analysis
- Market structure breaks
- Risk management with proper position sizing
Designed for hyperopt, backtesting, FreqAI and ML integration.
No future leak or data bias - all indicators use historical data only.
"""
INTERFACE_VERSION = 3
# Minimal ROI designed for the strategy
minimal_roi = {
"0": 0.20, # 20% at any time
"15": 0.10, # 10% after 15 minutes
"30": 0.05, # 5% after 30 minutes
"60": 0.02, # 2% after 1 hour
"120": 0.01 # 1% after 2 hours
}
# Optimal stoploss
stoploss = -0.08 # 8% stoploss
# Optimal timeframe for the strategy
timeframe = '15m'
# Informative timeframes for multi-timeframe analysis
inf_1h = '1h'
inf_4h = '4h'
inf_1d = '1d'
# Run "populate_indicators" only for new candle
process_only_new_candles = True
# These values can be overridden in the config
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 200
# Strategy parameters - Hyperopt enabled
# === Volume Analysis Parameters ===
volume_lookback = IntParameter(10, 30, default=20, space='buy', optimize=True)
volume_threshold = DecimalParameter(1.2, 3.0, default=2.0, space='buy', optimize=True)
volume_ma_period = IntParameter(10, 50, default=20, space='buy', optimize=True)
# === Smart Money Structure Parameters ===
structure_lookback = IntParameter(20, 50, default=30, space='buy', optimize=True)
liquidity_threshold = DecimalParameter(0.5, 2.0, default=1.0, space='buy', optimize=True)
order_block_strength = IntParameter(3, 10, default=5, space='buy', optimize=True)
# === Fair Value Gap Parameters ===
fvg_min_size = DecimalParameter(0.1, 1.0, default=0.3, space='buy', optimize=True)
fvg_lookback = IntParameter(5, 20, default=10, space='buy', optimize=True)
# === Trend Parameters ===
ema_fast = IntParameter(8, 21, default=12, space='buy', optimize=True)
ema_slow = IntParameter(21, 55, default=34, space='buy', optimize=True)
ema_trend = IntParameter(50, 200, default=100, space='buy', optimize=True)
# === RSI Parameters ===
rsi_period = IntParameter(10, 21, default=14, space='buy', optimize=True)
rsi_overbought = IntParameter(70, 85, default=75, space='buy', optimize=True)
rsi_oversold = IntParameter(15, 30, default=25, space='buy', optimize=True)
# === Exit Parameters ===
exit_rsi_high = IntParameter(75, 90, default=80, space='sell', optimize=True)
exit_rsi_low = IntParameter(10, 25, default=20, space='sell', optimize=True)
# === Risk Management ===
max_open_trades = 3
position_adjustment_enable = True
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pairs will automatically be available in strategy methods.
"""
pairs = self.dp.current_whitelist()
informative_pairs = [(pair, self.inf_1h) for pair in pairs]
informative_pairs += [(pair, self.inf_4h) for pair in pairs]
informative_pairs += [(pair, self.inf_1d) for pair in pairs]
return informative_pairs
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate all indicators used by the strategy
"""
# === Basic Price Action ===
dataframe['hl2'] = (dataframe['high'] + dataframe['low']) / 2
dataframe['hlc3'] = (dataframe['high'] + dataframe['low'] + dataframe['close']) / 3
dataframe['ohlc4'] = (dataframe['open'] + dataframe['high'] + dataframe['low'] + dataframe['close']) / 4
# === Moving Averages ===
dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=self.ema_fast.value)
dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=self.ema_slow.value)
dataframe['ema_trend'] = ta.EMA(dataframe, timeperiod=self.ema_trend.value)
# === RSI ===
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.rsi_period.value)
# === Volume Analysis ===
dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=self.volume_ma_period.value)
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_sma']
dataframe['high_volume'] = dataframe['volume_ratio'] > self.volume_threshold.value
# Volume-weighted indicators
dataframe['vwap'] = qtpylib.vwap(dataframe)
# === ATR for volatility ===
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['atr_percent'] = (dataframe['atr'] / dataframe['close']) * 100
# === Smart Money Concepts ===
dataframe = self.calculate_order_blocks(dataframe)
dataframe = self.calculate_fair_value_gaps(dataframe)
dataframe = self.calculate_market_structure(dataframe)
dataframe = self.calculate_liquidity_levels(dataframe)
# === Higher Timeframe Analysis ===
dataframe = self.populate_higher_timeframe_indicators(dataframe, metadata)
return dataframe
def calculate_order_blocks(self, dataframe: DataFrame) -> DataFrame:
"""
Identify order blocks - zones where institutional orders are likely placed
"""
# Bullish order blocks - last down candle before strong up move
dataframe['bull_ob_high'] = 0.0
dataframe['bull_ob_low'] = 0.0
dataframe['bull_ob_active'] = False
# Bearish order blocks - last up candle before strong down move
dataframe['bear_ob_high'] = 0.0
dataframe['bear_ob_low'] = 0.0
dataframe['bear_ob_active'] = False
for i in range(self.order_block_strength.value, len(dataframe)):
# Look for bullish order blocks
if (dataframe.iloc[i]['close'] > dataframe.iloc[i-1]['high'] and # Break of previous high
dataframe.iloc[i]['volume'] > dataframe.iloc[i-1]['volume'] * 1.5): # Strong volume
# Find the last bearish candle before the break
for j in range(i-1, max(0, i-self.order_block_strength.value), -1):
if dataframe.iloc[j]['close'] < dataframe.iloc[j]['open']: # Bearish candle
dataframe.iloc[i, dataframe.columns.get_loc('bull_ob_high')] = dataframe.iloc[j]['high']
dataframe.iloc[i, dataframe.columns.get_loc('bull_ob_low')] = dataframe.iloc[j]['low']
dataframe.iloc[i, dataframe.columns.get_loc('bull_ob_active')] = True
break
# Look for bearish order blocks
if (dataframe.iloc[i]['close'] < dataframe.iloc[i-1]['low'] and # Break of previous low
dataframe.iloc[i]['volume'] > dataframe.iloc[i-1]['volume'] * 1.5): # Strong volume
# Find the last bullish candle before the break
for j in range(i-1, max(0, i-self.order_block_strength.value), -1):
if dataframe.iloc[j]['close'] > dataframe.iloc[j]['open']: # Bullish candle
dataframe.iloc[i, dataframe.columns.get_loc('bear_ob_high')] = dataframe.iloc[j]['high']
dataframe.iloc[i, dataframe.columns.get_loc('bear_ob_low')] = dataframe.iloc[j]['low']
dataframe.iloc[i, dataframe.columns.get_loc('bear_ob_active')] = True
break
return dataframe
def calculate_fair_value_gaps(self, dataframe: DataFrame) -> DataFrame:
"""
Identify Fair Value Gaps (FVG) - price gaps that indicate inefficiency
"""
dataframe['fvg_bullish'] = False
dataframe['fvg_bearish'] = False
dataframe['fvg_bull_high'] = 0.0
dataframe['fvg_bull_low'] = 0.0
dataframe['fvg_bear_high'] = 0.0
dataframe['fvg_bear_low'] = 0.0
for i in range(2, len(dataframe)):
# Bullish FVG: gap between candle i-2 high and candle i low
if (dataframe.iloc[i-2]['high'] < dataframe.iloc[i]['low'] and
dataframe.iloc[i-1]['high'] < dataframe.iloc[i]['low'] and
(dataframe.iloc[i]['low'] - dataframe.iloc[i-2]['high']) / dataframe.iloc[i]['close'] > self.fvg_min_size.value / 100):
dataframe.iloc[i, dataframe.columns.get_loc('fvg_bullish')] = True
dataframe.iloc[i, dataframe.columns.get_loc('fvg_bull_low')] = dataframe.iloc[i-2]['high']
dataframe.iloc[i, dataframe.columns.get_loc('fvg_bull_high')] = dataframe.iloc[i]['low']
# Bearish FVG: gap between candle i-2 low and candle i high
if (dataframe.iloc[i-2]['low'] > dataframe.iloc[i]['high'] and
dataframe.iloc[i-1]['low'] > dataframe.iloc[i]['high'] and
(dataframe.iloc[i-2]['low'] - dataframe.iloc[i]['high']) / dataframe.iloc[i]['close'] > self.fvg_min_size.value / 100):
dataframe.iloc[i, dataframe.columns.get_loc('fvg_bearish')] = True
dataframe.iloc[i, dataframe.columns.get_loc('fvg_bear_high')] = dataframe.iloc[i-2]['low']
dataframe.iloc[i, dataframe.columns.get_loc('fvg_bear_low')] = dataframe.iloc[i]['high']
return dataframe
def calculate_market_structure(self, dataframe: DataFrame) -> DataFrame:
"""
Identify market structure breaks and trend changes
"""
# Calculate swing highs and lows
dataframe['swing_high'] = (
(dataframe['high'] > dataframe['high'].shift(1)) &
(dataframe['high'] > dataframe['high'].shift(-1)) &
(dataframe['high'] > dataframe['high'].shift(2)) &
(dataframe['high'] > dataframe['high'].shift(-2))
)
dataframe['swing_low'] = (
(dataframe['low'] < dataframe['low'].shift(1)) &
(dataframe['low'] < dataframe['low'].shift(-1)) &
(dataframe['low'] < dataframe['low'].shift(2)) &
(dataframe['low'] < dataframe['low'].shift(-2))
)
# Market structure breaks
dataframe['bullish_bos'] = False
dataframe['bearish_bos'] = False
# Simple structure break logic
for i in range(self.structure_lookback.value, len(dataframe)):
# Recent swing high broken to upside
recent_highs = dataframe.iloc[i-self.structure_lookback.value:i][dataframe.iloc[i-self.structure_lookback.value:i]['swing_high']]['high']
if len(recent_highs) > 0 and dataframe.iloc[i]['close'] > recent_highs.max():
dataframe.iloc[i, dataframe.columns.get_loc('bullish_bos')] = True
# Recent swing low broken to downside
recent_lows = dataframe.iloc[i-self.structure_lookback.value:i][dataframe.iloc[i-self.structure_lookback.value:i]['swing_low']]['low']
if len(recent_lows) > 0 and dataframe.iloc[i]['close'] < recent_lows.min():
dataframe.iloc[i, dataframe.columns.get_loc('bearish_bos')] = True
return dataframe
def calculate_liquidity_levels(self, dataframe: DataFrame) -> DataFrame:
"""
Identify liquidity levels where stops are likely placed
"""
# Equal highs/lows where liquidity sits
dataframe['liquidity_high'] = 0.0
dataframe['liquidity_low'] = 0.0
dataframe['sweep_high'] = False
dataframe['sweep_low'] = False
for i in range(20, len(dataframe)):
# Look for equal highs (liquidity levels)
recent_highs = dataframe.iloc[i-20:i]['high']
high_clusters = recent_highs[abs(recent_highs - recent_highs.max()) < (recent_highs.max() * 0.002)]
if len(high_clusters) >= 2:
dataframe.iloc[i, dataframe.columns.get_loc('liquidity_high')] = recent_highs.max()
# Check for liquidity sweep
if dataframe.iloc[i]['high'] > recent_highs.max() and dataframe.iloc[i]['close'] < recent_highs.max():
dataframe.iloc[i, dataframe.columns.get_loc('sweep_high')] = True
# Look for equal lows (liquidity levels)
recent_lows = dataframe.iloc[i-20:i]['low']
low_clusters = recent_lows[abs(recent_lows - recent_lows.min()) < (recent_lows.min() * 0.002)]
if len(low_clusters) >= 2:
dataframe.iloc[i, dataframe.columns.get_loc('liquidity_low')] = recent_lows.min()
# Check for liquidity sweep
if dataframe.iloc[i]['low'] < recent_lows.min() and dataframe.iloc[i]['close'] > recent_lows.min():
dataframe.iloc[i, dataframe.columns.get_loc('sweep_low')] = True
return dataframe
def populate_higher_timeframe_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Add higher timeframe context for better decision making
"""
# 1H timeframe
inf_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_1h)
inf_1h['ema_trend_1h'] = ta.EMA(inf_1h, timeperiod=50)
inf_1h['rsi_1h'] = ta.RSI(inf_1h, timeperiod=14)
inf_1h['trend_1h'] = (inf_1h['close'] > inf_1h['ema_trend_1h']).astype(int)
dataframe = merge_informative_pair(dataframe, inf_1h, self.timeframe, self.inf_1h, ffill=True)
# 4H timeframe
inf_4h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_4h)
inf_4h['ema_trend_4h'] = ta.EMA(inf_4h, timeperiod=21)
inf_4h['trend_4h'] = (inf_4h['close'] > inf_4h['ema_trend_4h']).astype(int)
dataframe = merge_informative_pair(dataframe, inf_4h, self.timeframe, self.inf_4h, ffill=True)
# Daily timeframe
inf_1d = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_1d)
inf_1d['ema_trend_1d'] = ta.EMA(inf_1d, timeperiod=12)
inf_1d['trend_1d'] = (inf_1d['close'] > inf_1d['ema_trend_1d']).astype(int)
dataframe = merge_informative_pair(dataframe, inf_1d, self.timeframe, self.inf_1d, ffill=True)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe
"""
conditions = []
# === BULLISH CONDITIONS ===
# 1. Higher timeframe trend alignment
higher_tf_bullish = (
(dataframe['trend_1h'] == 1) &
(dataframe['trend_4h'] == 1) &
(dataframe['trend_1d'] == 1)
)
# 2. Price action conditions
price_bullish = (
(dataframe['close'] > dataframe['ema_fast']) &
(dataframe['ema_fast'] > dataframe['ema_slow']) &
(dataframe['close'] > dataframe['vwap'])
)
# 3. Smart money structure
structure_bullish = (
(dataframe['bullish_bos']) |
(dataframe['bull_ob_active']) |
(dataframe['fvg_bullish']) |
(dataframe['sweep_low'])
)
# 4. Volume confirmation
volume_bullish = (
(dataframe['high_volume']) &
(dataframe['volume'] > dataframe['volume'].shift(1))
)
# 5. RSI conditions
rsi_bullish = (
(dataframe['rsi'] > self.rsi_oversold.value) &
(dataframe['rsi'] < self.rsi_overbought.value) &
(dataframe['rsi'] > dataframe['rsi'].shift(1))
)
# 6. Volatility filter
volatility_ok = dataframe['atr_percent'] < 5.0 # Not too volatile
# Combine all bullish conditions
dataframe.loc[
(higher_tf_bullish) &
(price_bullish) &
(structure_bullish) &
(volume_bullish) &
(rsi_bullish) &
(volatility_ok),
'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
"""
# === EXIT LONG CONDITIONS ===
# 1. RSI overbought
rsi_exit = (
(dataframe['rsi'] > self.exit_rsi_high.value)
)
# 2. Price action reversal
reversal_signals = (
(dataframe['bearish_bos']) |
(dataframe['bear_ob_active']) |
(dataframe['fvg_bearish']) |
(dataframe['sweep_high'])
)
# 3. Moving average cross down
ma_cross_down = (
(dataframe['ema_fast'] < dataframe['ema_slow']) &
(dataframe['close'] < dataframe['ema_fast'])
)
# 4. Volume declining
volume_declining = (
(dataframe['volume'] < dataframe['volume_sma']) &
(dataframe['volume'] < dataframe['volume'].shift(1))
)
# 5. Higher timeframe turning bearish
htf_bearish = (
(dataframe['trend_1h'] == 0) |
(dataframe['rsi_1h'] > 75)
)
dataframe.loc[
(rsi_exit) |
(reversal_signals) |
(ma_cross_down & volume_declining) |
(htf_bearish),
'exit_long'] = 1
return dataframe
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
side: str, **kwargs) -> float:
"""
Customize leverage for each new trade. This method is only called in futures mode.
"""
# Conservative leverage for smart money strategy
return min(proposed_leverage, 3.0)
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic - trailing stop based on ATR
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# ATR-based trailing stop
atr_multiplier = 2.0
atr_stop = atr_multiplier * last_candle['atr'] / current_rate
# Don't allow stop to be closer than original stoploss
return max(self.stoploss, -atr_stop)
def confirm_trade_entry(self, pair: str, order_type: str, amount: float,
rate: float, time_in_force: str, current_time: datetime,
entry_tag: Optional[str], side: str, **kwargs) -> bool:
"""
Called right before placing a entry order.
Timing of this method is as close as possible to the actual order placement.
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# Additional safety checks
# 1. Ensure we're not buying at resistance levels
if side == "long":
# Don't buy if price is at a bearish order block
if last_candle['bear_ob_active'] and last_candle['bear_ob_low'] <= rate <= last_candle['bear_ob_high']:
return False
# Don't buy if RSI too high
if last_candle['rsi'] > 70:
return False
return True
def custom_exit(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
"""
Custom exit logic beyond the exit signals
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# Take profit at major resistance levels
if trade.is_open and current_profit > 0.05: # 5% profit
if last_candle['bear_ob_active']:
return "resistance_exit"
# Emergency exit on major bearish structure break
if last_candle['bearish_bos'] and current_profit < -0.03: # -3% loss with structure break
return "emergency_exit"
return None
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
"""
pass
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool:
"""
Check entry timeout function callback.
This method can be used to override the entry-timeout.
"""
return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool:
"""
Check exit timeout function callback.
This method can be used to override the exit-timeout.
"""
return False