Freqtrade strategy implementing the VWAP + DMI algorithm described in the video: VWAP cross, DMI cross, and ADX filter.
Timeframe
1h
Direction
Long Only
Stoploss
-99.0%
Trailing Stop
No
ROI
0m: 99.0%
Interface Version
N/A
Startup Candles
200
Indicators
3
freqtrade/freqtrade-strategies
This strategy uses custom_stoploss() to enforce a fixed risk/reward ratio by first calculating a dynamic initial stoploss via ATR - last negative peak
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
# pylint: disable=C0301, E1101
# Import all necessary modules
from freqtrade.strategy import IStrategy, merge_informative_pair
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
from datetime import datetime
from freqtrade.persistence import Trade
class VwapDmiAlgo(IStrategy):
"""
Freqtrade strategy implementing the VWAP + DMI algorithm described
in the video: VWAP cross, DMI cross, and ADX filter.
"""
# Strategy settings
timeframe = '1h'
can_short: bool = True # Strategy has both long and short entry rules
# Setting a very high stoploss to allow custom_stoploss to override
stoploss = -0.99
startup_candle_count = 200
# We set minimal_roi to a high value so it doesn't trigger a default take-profit,
# as the TP is handled dynamically via custom_stoploss and custom_exit.
minimal_roi = {
"0": 0.99
}
# VWAP settings (window size mentioned in the video)
VWAP_WINDOW = 200
# DMI settings (default 14)
DMI_WINDOW = 14
# ADX filter threshold
ADX_THRESHOLD = 20
# ATR multiplier for stop-loss distance
ATR_MULTIPLIER = 2.5
# R:R ratio for Take-Profit
RR_RATIO = 3.0
# --- Indicator Population ---
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 1. DMI (Directional Movement Index) Indicators
# FIX: The original ta.ADX call was incorrect for returning all three indicators.
# We must call ADX, PLUS_DI, and MINUS_DI separately.
dataframe['adx'] = ta.ADX(dataframe, timeperiod=self.DMI_WINDOW)
dataframe['plus_di'] = ta.PLUS_DI(dataframe, timeperiod=self.DMI_WINDOW)
dataframe['minus_di'] = ta.MINUS_DI(dataframe, timeperiod=self.DMI_WINDOW)
# 2. ATR (Average True Range) for stop-loss
dataframe['atr'] = ta.ATR(dataframe, timeperiod=self.DMI_WINDOW)
# 3. Rolling VWAP
tp_vol = (dataframe['volume'] * (dataframe['high'] + dataframe['low'] + dataframe['close']) / 3).rolling(self.VWAP_WINDOW).sum()
total_vol = dataframe['volume'].rolling(self.VWAP_WINDOW).sum()
dataframe['vwap_200'] = tp_vol / total_vol
return dataframe
# --- Entry Logic ---
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Long Entry Conditions
long_conditions = [
# 1. Price crosses above VWAP
(dataframe['close'] > dataframe['vwap_200']),
(dataframe['close'].shift(1) <= dataframe['vwap_200'].shift(1)),
# 2. +DI is above -DI (Bullish bias)
(dataframe['plus_di'] > dataframe['minus_di']),
# 3. ADX is above 20 (Trend Confirmation)
(dataframe['adx'] > self.ADX_THRESHOLD),
]
dataframe.loc[
(
(dataframe['volume'] > 0) &
np.all(long_conditions, axis=0)
),
'enter_long'] = 1
# Short Entry Conditions
short_conditions = [
# 1. Price crosses below VWAP
(dataframe['close'] < dataframe['vwap_200']),
(dataframe['close'].shift(1) >= dataframe['vwap_200'].shift(1)),
# 2. -DI is above +DI (Bearish bias)
(dataframe['minus_di'] > dataframe['plus_di']),
# 3. ADX is above 20 (Trend Confirmation)
(dataframe['adx'] > self.ADX_THRESHOLD),
]
dataframe.loc[
(
(dataframe['volume'] > 0) &
np.all(short_conditions, axis=0)
),
'enter_short'] = 1
return dataframe
# --- Exit Logic (SL/TP) ---
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float,
initial: float, **kwargs) -> float:
# --- 1. Calculate and Store Risk/Reward on the first run ---
if 'initial_risk_distance' not in trade.data:
# Get the dataframe for the ATR value at the entry candle.
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
# Find the last complete candle *before* the trade was opened
entry_candle = dataframe.loc[dataframe.index < trade.open_date].iloc[-1]
entry_atr = entry_candle['atr']
# Calculate Risk (SL distance) and Reward (TP distance)
risk_distance = self.ATR_MULTIPLIER * entry_atr
reward_distance = risk_distance * self.RR_RATIO
# Store the distance for future use
trade.data['initial_risk_distance'] = risk_distance
# Calculate the Stop-Loss Price
stoploss_price = (
trade.open_rate - risk_distance if trade.is_open_long
else trade.open_rate + risk_distance
)
trade.data['stop_loss_price'] = stoploss_price
# Calculate the Take-Profit Price
take_profit_price = (
trade.open_rate + reward_distance if trade.is_open_long
else trade.open_rate - reward_distance
)
# Store the TP price on the trade object for later assignment
trade.data['take_profit_price'] = take_profit_price
# --- 2. Set the Take-Profit price (THE FIX) ---
# Freqtrade uses the 'limit' property to track the Take-Profit price.
trade.limit = trade.data['take_profit_price']
# --- 3. Calculate and Return the Stop-Loss Percentage ---
stoploss_price = trade.data['stop_loss_price']
# Convert the SL price back into a negative percentage from the open rate
sl_percentage = (stoploss_price / trade.open_rate) - 1.0
return sl_percentage
# --- Exit Logic (Must be kept empty to rely on trade.adjust_trade_entry for TP) ---
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit logic is handled dynamically via `custom_stoploss` which sets the TP price.
"""
return dataframe