This is the final, robust hyperoptable version. It separates static defaults from hyperopt spaces and correctly assigns parameters to the 'buy' and 'sell' spaces for full compatibility.
Timeframe
4h
Direction
Long Only
Stoploss
-28.2%
Trailing Stop
Yes
ROI
0m: 86.2%, 1538m: 31.2%, 3044m: 10.0%, 7553m: 0.0%
Interface Version
3
Startup Candles
N/A
Indicators
3
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# --- Do not remove these libs ---
import numpy as np
import pandas as pd
from pandas import DataFrame
from datetime import datetime
from typing import Optional, List
from freqtrade.strategy import (IStrategy, IntParameter, DecimalParameter, CategoricalParameter)
from freqtrade.persistence import Trade
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class FuturesSwingStrategy(IStrategy):
"""
This is the final, robust hyperoptable version.
It separates static defaults from hyperopt spaces and correctly assigns
parameters to the 'buy' and 'sell' spaces for full compatibility.
"""
INTERFACE_VERSION = 3
timeframe = '4h'
can_short: bool = True
# --- Static Default Parameters for Backtesting & Initial Validation ---
minimal_roi = {
"0": 0.862,
"1538": 0.312,
"3044": 0.1,
"7553": 0
}
stoploss = -0.282
trailing_stop = True
trailing_stop_positive = 0.015
trailing_stop_positive_offset = 0.041
trailing_only_offset_is_reached = True
# --- Hyperoptable Entry Parameters (buy space) ---
# These are defined directly in the class body.
# The 'space' argument assigns them to the 'buy' hyperopt space.
ema_fast_period = IntParameter(10, 60, default=19, space="buy", optimize=True)
ema_slow_period = IntParameter(100, 250, default=175, space="buy", optimize=True)
atr_period = IntParameter(5, 20, default=11, space="buy", optimize=True)
atr_multiplier = DecimalParameter(1.0, 5.0, default=1.0, decimals=1, space="buy", optimize=True)
# --- Strategy Settings ---
startup_candle_count: int = 250
order_types = {
'entry': 'market',
'exit': 'market',
'stoploss': 'market',
'stoploss_on_exchange': False,
}
order_time_in_force = {'entry': 'gtc', 'exit': 'gtc'}
# --- Hyperopt Space for Exit Parameters ---
def populate_exit_trend_space(self) -> List[any]:
"""
Defines the hyperopt space for parameters used in exit logic.
These are defined here because they are validated by Freqtrade at startup.
"""
return [
DecimalParameter(0.05, 0.25, default=0.15, name='stoploss'),
IntParameter(10, 240, name='roi_t1'),
DecimalParameter(0.01, 0.10, name='roi_p1'),
CategoricalParameter([True, False], default=True, name='trailing_stop'),
DecimalParameter(0.01, 0.10, default=0.03, name='trailing_stop_positive_offset'),
DecimalParameter(0.005, 0.05, default=0.01, name='trailing_stop_positive'),
CategoricalParameter([True, False], default=True, name='trailing_only_offset_is_reached'),
]
# --- Indicator & Signal Logic ---
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
# EMAs - uses the value from the hyperoptable parameter
dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=self.ema_fast_period.value)
dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=self.ema_slow_period.value)
# ATR - uses the value from the hyperoptable parameter
dataframe['atr'] = ta.ATR(dataframe, timeperiod=self.atr_period.value)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['ema_fast'] > dataframe['ema_slow']) &
(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal']))
),
'enter_long'] = 1
dataframe.loc[
(
(dataframe['ema_fast'] < dataframe['ema_slow']) &
(qtpylib.crossed_below(dataframe['macd'], dataframe['macdsignal']))
),
'enter_short'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Exits are primarily handled by ROI, Stoploss, and Trailing Stop
return dataframe
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if last_candle is not None and 'atr' in last_candle and last_candle['atr'] > 0:
return (last_candle['atr'] * self.atr_multiplier.value) / current_rate
# Fallback to the stoploss defined in the exit space or static default
return self.stoploss
def leverage(self, **kwargs) -> float:
return 3.0