# 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