Timeframe
1h
Direction
Long Only
Stoploss
-25.0%
Trailing Stop
No
ROI
N/A
Interface Version
3
Startup Candles
N/A
Indicators
3
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
this is an example class, implementing a PSAR based trailing stop loss you are supposed to take the `custom_stoploss()` and `populate_indicators()` parts and adapt it to your own strategy
freqtrade/freqtrade-strategies
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
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake13: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple
import logging
logger = logging.getLogger(__name__)
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import pandas_ta as pta
from technical import qtpylib
# ==========================================
# 13-21-34 WMA & MACD Strategy made 293% Profit! (Full Tutorial)
# YouTube Link: https://youtu.be/Rm8bcBPKoJA
# ==========================================
# ================================
# Freqtrade Version
# ================================
"""
freqtrade -V
Operating System: Linux-6.10.14-linuxkit-aarch64-with-glibc2.36
Python Version: Python 3.13.5
CCXT Version: 4.4.96
Freqtrade Version: freqtrade 2025.7
"""
# ================================
# Download Historical Data
# ================================
"""
freqtrade download-data \
-c user_data/binance_futures_MultiWmaMacd.json \
--timerange 20230101- \
-t 1m 5m 15m 30m 1h 4h 1d
"""
# ================================
# Hyperopt Optimization
# ================================
"""
freqtrade hyperopt \
--strategy MultiWmaMacd \
--config user_data/binance_futures_MultiWmaMacd.json \
--timeframe 1h \
--timerange 20240801-20250401 \
--hyperopt-loss MultiMetricHyperOptLoss \
--spaces buy\
-e 50 \
--j -2 \
--random-state 9319 \
--min-trades 30 \
--max-open-trades 1 \
-p ICP/USDT:USDT
"""
# ================================
# Backtesting
# ================================
"""
freqtrade backtesting \
--strategy MultiWmaMacd \
--timeframe 1h \
--timerange 20240801-20250801 \
--breakdown month \
-c user_data/binance_futures_MultiWmaMacd.json \
--max-open-trades 1 \
--cache none \
--timeframe-detail 5m \
-p ICP/USDT:USDT
"""
# ================================
# Start FreqUI Web Interface
# ================================
"""
freqtrade webserver \
--config user_data/binance_futures_MultiWmaMacd.json
"""
class MultiWmaMacd(IStrategy):
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 3
# Optimal timeframe for the strategy.
timeframe = "1h"
# Can this strategy go short?
can_short: bool = True
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {}
# Optimal stoploss designed for the strategy.
# This attribute will be overridden if the config file contains "stoploss".
stoploss = -0.25
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# 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
wma_rolling_window = CategoricalParameter([4, 6], default=4, space="buy")
macd_rolling_window = CategoricalParameter([13, 10], default=13, space="buy")
risk_ratio = CategoricalParameter([2, 2.5, 3], default=2, space="buy")
atr_mult = CategoricalParameter([2, 2.5, 3], default=2.5, space="buy")
leverage_level = IntParameter(1, 10, default=1, space="buy", optimize=False, load=False)
@property
def plot_config(self):
return {
"main_plot": {
"wma13": {"color": "red"},
"wma21": {"color": "blue"},
"wma34": {"color": "green"},
},
"subplots": {
"MACD": {
"macd": {"color": "#2962ff", "fill_to": "macdhist"},
"macdsignal": {"color": "#ff6d00"},
"macdhist": {"type": "bar", "plotly": {"opacity": 0.9}}
}
}
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
Sample: return [("ETH/USDT", "5m"),
("BTC/USDT", "15m"),
]
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["wma13"] = ta.WMA(dataframe, timeperiod=13)
dataframe["wma21"] = ta.WMA(dataframe, timeperiod=21)
dataframe["wma34"] = ta.WMA(dataframe, timeperiod=34)
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["wma13"] > dataframe["wma21"]) &
(dataframe["wma21"] > dataframe["wma34"]) &
# Check if WMA(13) crossed above WMA(34) in the rolling window
(dataframe["wma13"].rolling(window=self.wma_rolling_window.value).apply(
lambda x: any(qtpylib.crossed_above(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &
# Check if WMA(21) crossed above WMA(34) in the rolling window
(dataframe["wma21"].rolling(window=self.wma_rolling_window.value).apply(
lambda x: any(qtpylib.crossed_above(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &
(dataframe["macd"] > dataframe["macdsignal"]) &
# Check if MACD crossed above MACD signal in the rolling window
(dataframe["macd"].rolling(window=self.macd_rolling_window.value).apply(
lambda x: any(qtpylib.crossed_above(x, dataframe["macdsignal"].iloc[x.index[0]:x.index[-1]+1]))
)) &
(dataframe["volume"] > 0)
),
"enter_long"] = 1
dataframe.loc[
(
(dataframe["wma13"] < dataframe["wma21"]) &
(dataframe["wma21"] < dataframe["wma34"]) &
# Check if WMA(13) crossed below WMA(21) in the rolling window
(dataframe["wma13"].rolling(window=self.wma_rolling_window.value).apply(
lambda x: any(qtpylib.crossed_below(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &
# Check if WMA(21) crossed below WMA(34) in the rolling window
(dataframe["wma21"].rolling(window=self.wma_rolling_window.value).apply(
lambda x: any(qtpylib.crossed_below(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &
(dataframe['macd'] < dataframe['macdsignal']) &
# Check if MACD crossed below MACD signal in the rolling window
(dataframe['macd'].rolling(window=self.macd_rolling_window.value).apply(
lambda x: any(qtpylib.crossed_below(x, dataframe['macdsignal'].iloc[x.index[0]:x.index[-1]+1]))
)) &
(dataframe["volume"] > 0)
),
"enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, "exit_long"] = 0
dataframe.loc[:, "exit_short"] = 0
return dataframe
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs):
side = -1 if trade.is_short else 1
# Retrieve TP and SL from custom data
take_profit = trade.get_custom_data('take_profit')
stop_loss = trade.get_custom_data('stop_loss')
# If TP or SL is not set, initialize them
if take_profit is None or stop_loss is None:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
# Get the date just before trade opened
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
# Filter dataframe to candles before the trade opened
signal_data = dataframe.loc[dataframe["date"] < trade_date]
if signal_data.empty:
logger.warning(f"[{pair}] No signal candle found. Skip setting TP/SL.")
return None
signal_candle = signal_data.iloc[-1]
# Calculate TP and SL
atr = signal_candle["atr"]
close = signal_candle["close"]
take_profit = close + side * self.atr_mult.value * atr * self.risk_ratio.value
stop_loss = close - side * self.atr_mult.value * atr
# Save to trade's custom data
trade.set_custom_data('take_profit', take_profit)
trade.set_custom_data('stop_loss', stop_loss)
# logger.info(f"[{pair}] TP/SL set. TP: {take_profit:.2f}, SL: {stop_loss:.2f}")
# Check exit conditions
if (trade.is_short and current_rate <= take_profit) or \
(not trade.is_short and current_rate >= take_profit):
# logger.info(f"[{pair}] Take Profit hit! Close: {current_close:.2f}, TP: {take_profit:.2f}")
return "take_profit_achieved"
if (trade.is_short and current_rate >= stop_loss) or \
(not trade.is_short and current_rate <= stop_loss):
# logger.info(f"[{pair}] Stop Loss hit! Close: {current_close:.2f}, SL: {stop_loss:.2f}")
return "stop_loss_achieved"
return None
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
return self.leverage_level.value