DEV Community

Henry Lin
Henry Lin

Posted on

Lesson 26: Freqtrade-Custom Strategy Development

Lesson 26: Custom Strategy Development

⏱ Duration: 2.5 hours
🎯 Learning Objectives: Learn to write your own trading strategies from scratch


Course Overview

So far, we've been using pre-made strategies. But true advancement is the ability to develop your own strategies based on your trading ideas.

This lesson will teach you:

  • Basic structure of Freqtrade strategies
  • How to implement buy and sell logic
  • How to add and use technical indicators
  • Complete strategy development workflow
  • Practical examples from simple to complex

Part 1: Strategy Basic Structure

1.1 Strategy Template

Create user_data/strategies/MyFirstStrategy.py:

from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta

class MyFirstStrategy(IStrategy):
    """
    My first custom strategy
    """

    # Strategy basic information
    INTERFACE_VERSION = 3

    # Minimum ROI (optional, can be set high if strategy has its own sell logic)
    minimal_roi = {
        "0": 0.10,   # 10% take profit
        "30": 0.05,  # 5% take profit after 30 minutes
        "60": 0.03,  # 3% take profit after 60 minutes
        "120": 0.01  # 1% take profit after 120 minutes
    }

    # Stop loss
    stoploss = -0.03  # -3%

    # Trailing stop loss (optional)
    trailing_stop = False
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02

    # Timeframe
    timeframe = '5m'

    # Length of historical data to download at startup
    startup_candle_count: int = 100

    # Order types
    order_types = {
        'entry': 'limit',
        'exit': 'limit',
        'stoploss': 'market',
        'stoploss_on_exchange': True
    }

    # Order timeout
    order_time_in_force = {
        'entry': 'GTC',
        'exit': 'GTC'
    }

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add technical indicators
        This function is called every time new data arrives
        """
        # Add indicators you need here
        # dataframe['indicator_name'] = ta.INDICATOR(dataframe)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Define buy signals
        """
        dataframe.loc[
            (
                # Add your buy conditions here
                # (dataframe['indicator'] > threshold) &
                (dataframe['volume'] > 0)  # Ensure there is volume
            ),
            'enter_long'] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Define sell signals
        """
        dataframe.loc[
            (
                # Add your sell conditions here
                # (dataframe['indicator'] < threshold) &
                (dataframe['volume'] > 0)
            ),
            'exit_long'] = 1

        return dataframe
Enter fullscreen mode Exit fullscreen mode

1.2 Key Components Explanation

Required Parameters

# 1. Interface version (required)
INTERFACE_VERSION = 3

# 2. Timeframe (required)
timeframe = '5m'  # Options: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 1d

# 3. Stop loss (strongly recommended)
stoploss = -0.03  # -3%
Enter fullscreen mode Exit fullscreen mode

Optional Parameters

# ROI (Return on Investment) configuration
minimal_roi = {
    "0": 0.10,    # Sell immediately at 10%
    "30": 0.05,   # After 30 minutes, sell at 5%
    "60": 0.03,   # After 60 minutes, sell at 3%
}

# Trailing stop loss
trailing_stop = True
trailing_stop_positive = 0.01       # Enable trailing stop after 1% profit
trailing_stop_positive_offset = 0.02  # Activate after 2% profit
trailing_only_offset_is_reached = True

# Historical data needed at startup
startup_candle_count = 100  # Load 100 historical candles
Enter fullscreen mode Exit fullscreen mode

Three Core Functions

# 1. populate_indicators()
#    Purpose: Calculate technical indicators
#    Called when: When new candle data is received

# 2. populate_entry_trend()
#    Purpose: Define buy conditions
#    Called when: After indicator calculation

# 3. populate_exit_trend()
#    Purpose: Define sell conditions
#    Called when: After indicator calculation
Enter fullscreen mode Exit fullscreen mode

Part 2: Practical Example - Simple Moving Average Strategy

2.1 Strategy Logic Design

Trading Idea:

  • Use fast EMA (9) and slow EMA (21)
  • Buy when fast EMA crosses above slow EMA (golden cross)
  • Sell when fast EMA crosses below slow EMA (death cross)
  • Use RSI to filter overbought/oversold zones

Specific Rules:

  • Buy conditions:

    1. EMA 9 crosses above EMA 21
    2. RSI > 30 (avoid oversold)
    3. Volume > average volume
  • Sell conditions:

    1. EMA 9 crosses below EMA 21
    2. Or RSI > 70 (overbought zone)

2.2 Complete Code Implementation

Create user_data/strategies/SimpleCrossStrategy.py:

from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib

class SimpleCrossStrategy(IStrategy):
    """
    Simple moving average crossover strategy
    EMA 9/21 golden cross/death cross + RSI filter
    """

    INTERFACE_VERSION = 3

    # ROI configuration (relatively conservative)
    minimal_roi = {
        "0": 0.15,
        "30": 0.08,
        "60": 0.05,
        "120": 0.02
    }

    # Stop loss -3%
    stoploss = -0.03

    # Trailing stop loss
    trailing_stop = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02
    trailing_only_offset_is_reached = True

    # Timeframe
    timeframe = '5m'

    # Load 50 candles at startup (enough to calculate EMA 21)
    startup_candle_count: int = 50

    # Order configuration
    order_types = {
        'entry': 'limit',
        'exit': 'limit',
        'stoploss': 'market',
        'stoploss_on_exchange': True
    }

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add technical indicators
        """
        # 1. Calculate EMA
        dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=9)
        dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=21)

        # 2. Calculate RSI
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)

        # 3. Calculate average volume
        dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Buy signal: EMA golden cross + RSI filter
        """
        dataframe.loc[
            (
                # Condition 1: Golden cross (fast line crosses above slow line)
                (qtpylib.crossed_above(dataframe['ema_fast'], dataframe['ema_slow'])) &

                # Condition 2: RSI not in oversold zone (avoid buying at bottom bounce)
                (dataframe['rsi'] > 30) &
                (dataframe['rsi'] < 70) &

                # Condition 3: Volume confirmation
                (dataframe['volume'] > dataframe['volume_mean']) &

                # Ensure there is volume
                (dataframe['volume'] > 0)
            ),
            'enter_long'] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Sell signal: EMA death cross or RSI overbought
        """
        dataframe.loc[
            (
                (
                    # Condition 1: Death cross (fast line crosses below slow line)
                    (qtpylib.crossed_below(dataframe['ema_fast'], dataframe['ema_slow'])) |

                    # Or condition 2: RSI overbought
                    (dataframe['rsi'] > 70)
                ) &

                # Ensure there is volume
                (dataframe['volume'] > 0)
            ),
            'exit_long'] = 1

        return dataframe
Enter fullscreen mode Exit fullscreen mode

2.3 Test Strategy

# 1. Download data (if not already)
freqtrade download-data -c config.json --days 30 --timeframe 5m

# 2. Backtest
freqtrade backtesting \
    -c config.json \
    --strategy SimpleCrossStrategy \
    --timerange 20230101-20230131

# 3. View results
# Output similar to:
# =============== SUMMARY METRICS ===============
# | Metric                 | Value               |
# |------------------------|---------------------|
# | Total Trades           | 45                  |
# | Win Rate               | 55.6%               |
# | Total Profit           | 12.50 USDT (12.5%)  |
# | Avg Profit             | 0.28 USDT           |
# | Best Trade             | 2.50 USDT (2.5%)    |
# | Worst Trade            | -0.95 USDT (-0.95%) |
# | Max Drawdown           | -3.2%               |
Enter fullscreen mode Exit fullscreen mode

Part 3: Common Technical Indicators

3.1 Trend Indicators

EMA (Exponential Moving Average)

# Fast EMA
dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=9)

# Slow EMA
dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=21)

# Buy: Golden cross
(qtpylib.crossed_above(dataframe['ema_fast'], dataframe['ema_slow']))

# Sell: Death cross
(qtpylib.crossed_below(dataframe['ema_fast'], dataframe['ema_slow']))
Enter fullscreen mode Exit fullscreen mode

SMA (Simple Moving Average)

dataframe['sma_20'] = ta.SMA(dataframe, timeperiod=20)
dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50)
Enter fullscreen mode Exit fullscreen mode

MACD (Moving Average Convergence Divergence)

macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']

# Buy: MACD crosses above signal line
(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal']))

# Sell: MACD crosses below signal line
(qtpylib.crossed_below(dataframe['macd'], dataframe['macdsignal']))
Enter fullscreen mode Exit fullscreen mode

ADX (Average Directional Index)

dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)

# Trend filter: ADX > 25 indicates clear trend
(dataframe['adx'] > 25)
Enter fullscreen mode Exit fullscreen mode

3.2 Oscillator Indicators

RSI (Relative Strength Index)

dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)

# Oversold: RSI < 30
(dataframe['rsi'] < 30)

# Overbought: RSI > 70
(dataframe['rsi'] > 70)

# Neutral zone: 30 < RSI < 70
(dataframe['rsi'] > 30) & (dataframe['rsi'] < 70)
Enter fullscreen mode Exit fullscreen mode

Stochastic (Stochastics)

stoch = ta.STOCH(dataframe)
dataframe['slowk'] = stoch['slowk']
dataframe['slowd'] = stoch['slowd']

# Oversold: Stoch < 20
(dataframe['slowk'] < 20)

# Overbought: Stoch > 80
(dataframe['slowk'] > 80)

# Golden cross
(qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
Enter fullscreen mode Exit fullscreen mode

CCI (Commodity Channel Index)

dataframe['cci'] = ta.CCI(dataframe, timeperiod=20)

# Oversold: CCI < -100
(dataframe['cci'] < -100)

# Overbought: CCI > 100
(dataframe['cci'] > 100)
Enter fullscreen mode Exit fullscreen mode

3.3 Volatility Indicators

Bollinger Bands

bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=2)
dataframe['bb_lower'] = bollinger['lower']
dataframe['bb_middle'] = bollinger['mid']
dataframe['bb_upper'] = bollinger['upper']

# Buy: Price touches lower band
(dataframe['close'] < dataframe['bb_lower'])

# Sell: Price touches upper band
(dataframe['close'] > dataframe['bb_upper'])

# Calculate bandwidth (volatility)
dataframe['bb_width'] = (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle']
Enter fullscreen mode Exit fullscreen mode

ATR (Average True Range)

dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)

# Used for dynamic stop loss
# Stop loss = Current price - 2 Γ— ATR
Enter fullscreen mode Exit fullscreen mode

3.4 Volume Indicators

Volume

# Average volume
dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()

# Volume surge (> 1.5x average)
(dataframe['volume'] > dataframe['volume_mean'] * 1.5)
Enter fullscreen mode Exit fullscreen mode

OBV (On-Balance Volume)

dataframe['obv'] = ta.OBV(dataframe)

# OBV uptrend
dataframe['obv_ema'] = ta.EMA(dataframe['obv'], timeperiod=20)
(dataframe['obv'] > dataframe['obv_ema'])
Enter fullscreen mode Exit fullscreen mode

Part 4: Advanced Practice - RSI Mean Reversion Strategy

4.1 Strategy Logic

Trading Idea:

  • RSI mean reversion: Price returns to mean after excessive deviation
  • Buy in oversold zone, wait for bounce
  • Use Bollinger Bands for additional confirmation

Specific Rules:

  • Buy conditions:

    1. RSI < 30 (oversold)
    2. Price below Bollinger lower band
    3. Increased volume (confirmation)
  • Sell conditions:

    1. RSI > 50 (return to neutral)
    2. Or price touches Bollinger middle band
    3. Or reach take profit target

4.2 Complete Code

Create user_data/strategies/RSIMeanReversionStrategy.py:

from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib

class RSIMeanReversionStrategy(IStrategy):
    """
    RSI mean reversion strategy
    Buy in oversold zone, wait for price reversion
    """

    INTERFACE_VERSION = 3

    # ROI configuration (quick take profit)
    minimal_roi = {
        "0": 0.05,   # 5% immediate take profit
        "15": 0.03,  # 3% after 15 minutes
        "30": 0.02,  # 2% after 30 minutes
        "60": 0.01   # 1% after 60 minutes
    }

    # Stop loss
    stoploss = -0.04  # -4% (slightly wider to give room for bounce)

    # Timeframe
    timeframe = '5m'

    startup_candle_count: int = 50

    # Custom parameters (optimizable)
    buy_rsi_threshold = 30
    sell_rsi_threshold = 50
    volume_factor = 1.5

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add indicators
        """
        # RSI
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)

        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=2)
        dataframe['bb_lower'] = bollinger['lower']
        dataframe['bb_middle'] = bollinger['mid']
        dataframe['bb_upper'] = bollinger['upper']

        # Volume
        dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()

        # EMA (auxiliary for trend judgment)
        dataframe['ema_50'] = ta.EMA(dataframe, timeperiod=50)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Buy signal: Oversold + price below Bollinger lower band
        """
        dataframe.loc[
            (
                # Condition 1: RSI oversold
                (dataframe['rsi'] < self.buy_rsi_threshold) &

                # Condition 2: Price below Bollinger lower band (excessive deviation)
                (dataframe['close'] < dataframe['bb_lower']) &

                # Condition 3: Volume increase (confirm real breakout)
                (dataframe['volume'] > dataframe['volume_mean'] * self.volume_factor) &

                # Condition 4: Price near or above EMA 50 (avoid downtrend)
                (dataframe['close'] > dataframe['ema_50'] * 0.95) &

                # Ensure there is volume
                (dataframe['volume'] > 0)
            ),
            'enter_long'] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Sell signal: RSI returns to neutral zone or price touches middle band
        """
        dataframe.loc[
            (
                (
                    # Condition 1: RSI returns above 50 (reversion complete)
                    (dataframe['rsi'] > self.sell_rsi_threshold) |

                    # Or condition 2: Price returns to Bollinger middle band
                    (dataframe['close'] > dataframe['bb_middle'])
                ) &

                # Ensure there is volume
                (dataframe['volume'] > 0)
            ),
            'exit_long'] = 1

        return dataframe

    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
                        current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Custom stop loss: move stop loss after profit
        """
        # If profit exceeds 2%, move stop loss to +1%
        if current_profit > 0.02:
            return 0.01

        # Otherwise use default stop loss
        return self.stoploss
Enter fullscreen mode Exit fullscreen mode

4.3 Testing and Optimization

# Backtest
freqtrade backtesting \
    -c config.json \
    --strategy RSIMeanReversionStrategy \
    --timerange 20230101-20230331

# Use Hyperopt to optimize parameters (will be detailed later)
freqtrade hyperopt \
    -c config.json \
    --strategy RSIMeanReversionStrategy \
    --hyperopt-loss SharpeHyperOptLoss \
    --epochs 100 \
    --spaces buy sell
Enter fullscreen mode Exit fullscreen mode

Part 5: Advanced Techniques

5.1 Custom Indicators

def custom_indicator(dataframe: DataFrame) -> DataFrame:
    """
    Custom indicator example: calculate price momentum
    """
    # Calculate N-period price change rate
    dataframe['price_momentum'] = (
        (dataframe['close'] - dataframe['close'].shift(10)) /
        dataframe['close'].shift(10) * 100
    )

    return dataframe

# Call in populate_indicators
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe = self.custom_indicator(dataframe)
    return dataframe
Enter fullscreen mode Exit fullscreen mode

5.2 Informative Pairs

Use longer timeframes for auxiliary decisions:

from freqtrade.strategy import informative

class MultiTimeframeStrategy(IStrategy):

    @informative('1h')
    def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add 1-hour timeframe indicators
        """
        dataframe['ema_50_1h'] = ta.EMA(dataframe, timeperiod=50)
        dataframe['rsi_1h'] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 5-minute indicators
        dataframe['ema_20'] = ta.EMA(dataframe, timeperiod=20)
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                # 5-minute conditions
                (dataframe['rsi'] < 30) &

                # 1-hour conditions (confirm major uptrend)
                (dataframe['close'] > dataframe['ema_50_1h']) &
                (dataframe['rsi_1h'] > 40) &

                (dataframe['volume'] > 0)
            ),
            'enter_long'] = 1

        return dataframe
Enter fullscreen mode Exit fullscreen mode

5.3 Custom Stop Loss

def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
                    current_rate: float, current_profit: float, **kwargs) -> float:
    """
    Dynamic stop loss based on time and profit
    """
    # Calculate holding time (minutes)
    trade_duration = (current_time - trade.open_date_utc).total_seconds() / 60

    # Phase 1 (0-30 minutes): -3% stop loss
    if trade_duration < 30:
        return -0.03

    # Phase 2 (30-60 minutes): -2% stop loss
    elif trade_duration < 60:
        return -0.02

    # Phase 3 (60+ minutes):
    # If profit > 2%, move stop loss to breakeven
    if current_profit > 0.02:
        return 0.0

    # Otherwise -1% stop loss
    return -0.01
Enter fullscreen mode Exit fullscreen mode

5.4 Custom Stake Amount

def custom_stake_amount(self, pair: str, current_time: 'datetime',
                        current_rate: float, proposed_stake: float,
                        min_stake: float, max_stake: float, **kwargs) -> float:
    """
    Adjust position size based on market volatility
    """
    dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
    last_candle = dataframe.iloc[-1].squeeze()

    # Get ATR (volatility)
    atr = last_candle['atr']
    atr_percentage = (atr / last_candle['close']) * 100

    # Reduce position in high volatility
    if atr_percentage > 2.0:
        return proposed_stake * 0.5  # 50% position

    # Increase position in low volatility
    elif atr_percentage < 1.0:
        return proposed_stake * 1.5  # 150% position

    # Normal volatility
    return proposed_stake
Enter fullscreen mode Exit fullscreen mode

Part 6: Strategy Development Workflow

6.1 Complete Development Process

1. Ideation Phase
   β–‘ Determine trading concept (trend following? mean reversion?)
   β–‘ Select timeframe
   β–‘ Choose indicators to use
   β–‘ Draw logic flowchart

2. Coding Phase
   β–‘ Create strategy file
   β–‘ Implement populate_indicators()
   β–‘ Implement populate_entry_trend()
   β–‘ Implement populate_exit_trend()
   β–‘ Add comments and explanations

3. Testing Phase
   β–‘ Syntax check: freqtrade test-strategy
   β–‘ Quick backtest: 30 days of data
   β–‘ Check if trade count is reasonable
   β–‘ Check if signals meet expectations

4. Optimization Phase
   β–‘ Use plot-dataframe to visualize signals
   β–‘ Analyze best/worst trades
   β–‘ Adjust parameters or logic
   β–‘ Backtest again to verify

5. Validation Phase
   β–‘ Complete backtest: 3-6 months of data
   β–‘ Out-of-sample testing
   β–‘ Different market environment testing
   β–‘ Compare with other strategies

6. Dry-run Phase
   β–‘ Run 1-2 weeks Dry-run
   β–‘ Verify real-time performance
   β–‘ Compare with backtest results
   β–‘ Confirm no technical issues

7. Live Phase
   β–‘ Start with small capital
   β–‘ Intensive monitoring
   β–‘ Continuous optimization
Enter fullscreen mode Exit fullscreen mode

6.2 Strategy Evaluation Criteria

βœ… Characteristics of good strategies:

1. Clear Logic
   - Simple and clear buy/sell conditions
   - Has theoretical support
   - Easy to understand and explain

2. Stable Backtest Performance
   - Total return > 15% (3 months)
   - Win rate > 50%
   - Max drawdown < 20%
   - Sharpe > 1.0

3. Reasonable Trading Frequency
   - Not overtrading (>20 times/day)
   - Not undertrading (<2 times/week)
   - Average holding time is appropriate

4. Strong Adaptability
   - Profitable in different market environments
   - Not overly dependent on specific periods
   - Not overfitted to historical data

5. Controlled Risk
   - Has clear stop loss
   - Controllable single trade risk
   - Acceptable drawdown
Enter fullscreen mode Exit fullscreen mode
❌ Characteristics of poor strategies:

1. Overly Complex
   - Using too many indicators (>5)
   - Conditions too complex
   - Difficult to understand logic

2. Overfitted
   - Perfect backtest but Dry-run fails
   - Only effective in specific periods
   - Too many parameter adjustments (>10 times)

3. Abnormal Trading Frequency
   - Dozens of trades per day (fees erode profits)
   - Or almost no trades (low capital utilization)

4. Uncontrolled Risk
   - No stop loss or stop loss too large
   - Frequent large drawdowns
   - Unreasonable risk-reward ratio

5. Unstable
   - Performance fluctuates greatly
   - Frequent consecutive losses
   - Unable to sustain profits
Enter fullscreen mode Exit fullscreen mode

6.3 Common Errors

Error 1: Using Future Data (Look-ahead Bias)

❌ Wrong example:
dataframe['future_high'] = dataframe['high'].shift(-1)
if dataframe['close'] < dataframe['future_high']:
    enter_long = 1

Problem: Uses future data, perfect backtest but fails in live trading

βœ… Correct approach:
Only use current and historical data for decisions


Error 2: Over-optimization

❌ Wrong example:
Adjust parameters 50 times to find configuration with 200% backtest return

Problem: Overfitted to historical data, inevitable failure in live trading

βœ… Correct approach:
- Parameter adjustments < 5 times
- Use out-of-sample testing
- Keep strategy simple


Error 3: Ignoring Fees and Slippage

❌ Wrong example:
High-frequency strategy, 50 trades per day
Backtest doesn't consider fees

Problem: Live trading fees erode all profits

βœ… Correct approach:
- Set real fees in config.json
- "fee": 0.001  # 0.1%
- Consider slippage impact


Error 4: Signal Repainting

❌ Wrong example:
Use unstable indicators where historical signals change

Problem: Backtest signals don't match live trading signals

βœ… Correct approach:
- Use stable indicators (EMA, SMA, RSI)
- Avoid repainting indicators
- Carefully test signal stability
Enter fullscreen mode Exit fullscreen mode

πŸ“ Practical Tasks

Task 1: Implement Simple Strategy

Based on the SimpleCrossStrategy in this lesson, implement and test:

# 1. Create strategy file
# 2. Backtest 3 months of data
# 3. Generate charts to analyze signals
freqtrade plot-dataframe \
    -c config.json \
    --strategy SimpleCrossStrategy \
    --pairs BTC/USDT \
    --indicators1 ema_fast ema_slow \
    --indicators2 rsi
Enter fullscreen mode Exit fullscreen mode

Task 2: Modify Strategy

Modify SimpleCrossStrategy, try:

  1. Change EMA periods (9/21 β†’ 12/26)
  2. Add MACD as additional confirmation
  3. Add ADX trend filter
  4. Compare backtest results before and after modification

Task 3: Develop Your Own Strategy

Based on your trading ideas, develop a completely original strategy:

  1. Write down strategy logic (text description)
  2. Choose 2-3 core indicators
  3. Implement code
  4. Backtest and analyze results
  5. Optimize at least 3 times

Task 4: Strategy Comparison

Compare the performance of the following three strategies in the same period:

  • SimpleCrossStrategy
  • RSIMeanReversionStrategy
  • Your own developed strategy

Create comparison table:

Metric Strategy 1 Strategy 2 Strategy 3
Total Return
Win Rate
Max Drawdown
Trade Count
Best Market Environment

πŸ“Œ Key Points

Three Elements of Strategy Development

1. Clear Logic
   - Can explain strategy in one sentence
   - Has theoretical support
   - Simple > Complex

2. Backtest Verification
   - At least 3 months of data
   - Out-of-sample testing
   - Different market environments

3. Live Verification
   - Dry-run 1-2 weeks
   - Small capital testing
   - Continuous monitoring and improvement
Enter fullscreen mode Exit fullscreen mode

Common Indicator Combinations

Trend Following Strategies:
- EMA/SMA crossover
- MACD
- ADX (filter)

Mean Reversion Strategies:
- RSI
- Bollinger Bands
- Volume

Breakout Strategies:
- Price highs/lows
- Volume increase
- ATR (volatility)
Enter fullscreen mode Exit fullscreen mode

Pitfalls to Avoid

1. Over-optimization
2. Using future data
3. Ignoring fees
4. Strategy too complex
5. Lack of risk management
Enter fullscreen mode Exit fullscreen mode

🎯 Next Lesson Preview

Lesson 27: Multi-Timeframe Strategies

In the next lesson, we will learn:

  • How to combine multiple timeframes
  • Major timeframe confirmation, minor timeframe entry
  • Improve signal quality
  • Practical examples

Multi-timeframe is an important technique to improve strategy stability. See you in the next lesson!


πŸŽ“ Learning Suggestions:

  1. Start Simple: Don't pursue complex strategies from the beginning
  2. Understand Principles: Know the meaning and usage of each indicator
  3. Experiment More: Try different parameters and combinations
  4. Record and Summarize: Record each modification and its effects
  5. Be Patient: Good strategies require repeated polishing

Remember: Simple and effective > complex and fancy. The best strategies are often the simplest strategies.

Top comments (0)