Fibonacci Strategy with Indodax exchange workarounds.
Timeframe
15m
Direction
Long Only
Stoploss
-10.0%
Trailing Stop
Yes
ROI
0m: 29.8%, 115m: 14.4%, 280m: 5.5%, 507m: 0.0%
Interface Version
3
Startup Candles
900
Indicators
16
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from typing import Any, Optional, Union
from freqtrade.strategy import (
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IStrategy,
IntParameter,
merge_informative_pair
)
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade, Order
from freqtrade.configuration import Configuration
from freqtrade.exceptions import OperationalException
# --------------------------------
# Add your lib to import here
import os
import sys
import json
import random
import logging
from copy import deepcopy
from itertools import product, chain
from datetime import datetime
from functools import reduce
from pathlib import Path
import talib.abstract as ta
import pandas_ta as pd_ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from itertools import permutations
STRATEGY_DIR = Path(__file__).resolve().parent
if str(STRATEGY_DIR) not in sys.path:
sys.path.append(str(STRATEGY_DIR))
from indodax_patch import (
patch_indodax_cancel_order,
patch_indodax_create_order,
patch_indodax_fetch_order,
)
# Define indicator sets (could also come from the JSON if needed)
buy_indicators = ["BB", "RSI", "TTM", "VWAP", "MACD", "DEMA", "STOCHRSI"]
sell_indicators = ["BB", "RSI", "TTM", "VWAP", "MACD", "DEMA", "STOCHRSI"]
logger = logging.getLogger(__name__)
# ✅ 1. Recursively find the first occurrence of the 'span' key
def find_span(obj):
if isinstance(obj, dict):
if "span" in obj:
return obj["span"]
for value in obj.values():
result = find_span(value)
if result is not None:
return result
elif isinstance(obj, list):
for item in obj:
result = find_span(item)
if result is not None:
return result
return None
DEFAULT_SPAN = {
"buy": {
"buy_bb_period": {
"type": "IntParameter",
"low": 10,
"high": 40,
"default": 20,
"optimize": False,
},
"buy_fast_dema": {
"type": "CategoricalParameter",
"choices": [5, 8, 13, 21, 34],
"default": 13,
"optimize": False,
},
"buy_fib_level": {
"type": "CategoricalParameter",
"choices": [0.236, 0.382, 0.618, 0.786],
"default": 0.382,
"optimize": False,
},
"buy_freqai": {
"type": "DecimalParameter",
"low": 0.50,
"high": 0.95,
"default": 0.60,
"decimals": 2,
"optimize": False,
},
"buy_long_indicator": {
"type": "CategoricalParameter",
"choices": ["BB, RSI"],
"default": "BB, RSI",
"optimize": False,
},
"buy_rsi": {
"type": "IntParameter",
"low": 10,
"high": 50,
"default": 30,
"optimize": False,
},
"buy_rsi_period": {
"type": "IntParameter",
"low": 7,
"high": 28,
"default": 14,
"optimize": False,
},
"buy_short_indicator": {
"type": "CategoricalParameter",
"choices": ["NONE"],
"default": "NONE",
"optimize": False,
},
"buy_slow_ema": {
"type": "CategoricalParameter",
"choices": [50, 100, 200],
"default": 200,
"optimize": False,
},
"buy_smoothD": {
"type": "IntParameter",
"low": 1,
"high": 10,
"default": 3,
"optimize": False,
},
"buy_smoothK": {
"type": "IntParameter",
"low": 1,
"high": 10,
"default": 3,
"optimize": False,
},
"buy_stoch_osc": {
"type": "IntParameter",
"low": 5,
"high": 50,
"default": 20,
"optimize": False,
},
"buy_swing_period": {
"type": "IntParameter",
"low": 5,
"high": 50,
"default": 20,
"optimize": False,
},
"shared_stoch_period": {
"type": "IntParameter",
"low": 7,
"high": 28,
"default": 14,
"optimize": False,
},
"shared_ttm_window": {
"type": "IntParameter",
"low": 10,
"high": 60,
"default": 20,
"optimize": False,
},
"shared_vwap_window": {
"type": "IntParameter",
"low": 10,
"high": 60,
"default": 20,
"optimize": False,
},
},
"sell": {
"sell_fast_dema": {
"type": "CategoricalParameter",
"choices": [5, 8, 13, 21, 34],
"default": 13,
"optimize": False,
},
"sell_fib_level": {
"type": "CategoricalParameter",
"choices": [0.236, 0.382, 0.618, 0.786],
"default": 0.618,
"optimize": False,
},
"sell_freqai": {
"type": "DecimalParameter",
"low": 0.05,
"high": 0.50,
"default": 0.40,
"decimals": 2,
"optimize": False,
},
"sell_long_indicator": {
"type": "CategoricalParameter",
"choices": ["DEMA, RSI"],
"default": "DEMA, RSI",
"optimize": False,
},
"sell_rsi": {
"type": "IntParameter",
"low": 50,
"high": 95,
"default": 70,
"optimize": False,
},
"sell_short_indicator": {
"type": "CategoricalParameter",
"choices": ["NONE"],
"default": "NONE",
"optimize": False,
},
"sell_slow_ema": {
"type": "CategoricalParameter",
"choices": [50, 100, 200],
"default": 200,
"optimize": False,
},
"sell_smoothD": {
"type": "IntParameter",
"low": 1,
"high": 10,
"default": 3,
"optimize": False,
},
"sell_smoothK": {
"type": "IntParameter",
"low": 1,
"high": 10,
"default": 3,
"optimize": False,
},
"sell_stoch_osc": {
"type": "IntParameter",
"low": 50,
"high": 95,
"default": 80,
"optimize": False,
},
},
"protection": {
"cooldown_lookback": {
"type": "IntParameter",
"low": 2,
"high": 48,
"default": 5,
"optimize": False,
},
"lookback_period_candles": {
"type": "IntParameter",
"low": 6,
"high": 72,
"default": 24,
"optimize": False,
},
"low_profit_trade_limit": {
"type": "IntParameter",
"low": 1,
"high": 10,
"default": 2,
"optimize": False,
},
"max_drawdown_trade_limit": {
"type": "IntParameter",
"low": 1,
"high": 20,
"default": 10,
"optimize": False,
},
"stop_duration_candles": {
"type": "IntParameter",
"low": 2,
"high": 48,
"default": 6,
"optimize": False,
},
"trade_limit": {
"type": "IntParameter",
"low": 1,
"high": 10,
"default": 2,
"optimize": False,
},
"use_low_profit": {
"type": "BooleanParameter",
"default": False,
"optimize": False,
},
"use_max_drawdown_protection": {
"type": "BooleanParameter",
"default": False,
"optimize": False,
},
"use_stop_protection": {
"type": "BooleanParameter",
"default": True,
"optimize": False,
},
},
"roi": {
"roi_p1": {
"type": "DecimalParameter",
"low": 0.01,
"high": 0.40,
"default": 0.298,
"decimals": 3,
"optimize": False,
},
"roi_p2": {
"type": "DecimalParameter",
"low": 0.01,
"high": 0.25,
"default": 0.144,
"decimals": 3,
"optimize": False,
},
"roi_p3": {
"type": "DecimalParameter",
"low": 0.00,
"high": 0.10,
"default": 0.055,
"decimals": 3,
"optimize": False,
},
"roi_t1": {
"type": "IntParameter",
"low": 15,
"high": 240,
"default": 115,
"optimize": False,
},
"roi_t2": {
"type": "IntParameter",
"low": 60,
"high": 480,
"default": 280,
"optimize": False,
},
"roi_t3": {
"type": "IntParameter",
"low": 120,
"high": 720,
"default": 507,
"optimize": False,
},
},
}
def merge_span_defaults(default_span: dict, override_span: dict) -> dict:
merged = deepcopy(default_span)
for section, values in override_span.items():
if section not in merged or not isinstance(values, dict):
merged[section] = values
continue
for key, config in values.items():
if isinstance(config, dict) and isinstance(merged[section].get(key), dict):
merged[section][key].update(config)
else:
merged[section][key] = config
return merged
def load_span_config() -> dict:
param_file = STRATEGY_DIR / "hyperopt_params.json"
if not param_file.exists():
logger.info("Using built-in Fibbo parameter defaults.")
return deepcopy(DEFAULT_SPAN)
try:
with open(param_file, encoding="utf-8") as file:
loaded_span = find_span(json.load(file))
if not isinstance(loaded_span, dict):
logger.warning("Invalid span structure in %s, using defaults.", param_file)
return deepcopy(DEFAULT_SPAN)
logger.info("Loaded Fibbo hyperopt parameters from %s", param_file)
return merge_span_defaults(DEFAULT_SPAN, loaded_span)
except json.JSONDecodeError:
logger.error("Invalid JSON in params file: %s", param_file)
except Exception as exc:
logger.error("Error loading params from %s: %s", param_file, exc)
return deepcopy(DEFAULT_SPAN)
span = load_span_config()
# ✅ 2. Helper function to construct parameters
def get_param_config(span: dict, space: str, name: str):
config = span[space][name]
param_type = config["type"]
optimize = config.get("optimize", False)
default = config["default"]
if param_type == "IntParameter":
return IntParameter(
low=config["low"],
high=config["high"],
default=default,
space=space,
optimize=optimize
)
elif param_type == "DecimalParameter":
return DecimalParameter(
low=config['low'],
high=config['high'],
default=default,
decimals=config.get('decimals', 3),
space=space,
optimize=optimize
)
elif param_type == "BooleanParameter":
return BooleanParameter(
default=default,
space=space,
optimize=optimize
)
elif param_type == "CategoricalParameter":
return CategoricalParameter(
categories=config['choices'],
default=default,
space=space,
optimize=optimize
)
else:
raise ValueError(f"Unknown parameter type: {param_type}")
# ✅ 3. Generate permutations and insert them into the span config before using them
def indicator_permutations(profiles, max_indicators=1, include_none=False):
profile_permutations = set()
if include_none:
profile_permutations.add("NONE")
if max_indicators == 1:
profile_permutations.update(profiles)
return profile_permutations
for i in range(1, len(profiles) + 1):
for perm in permutations(profiles, i):
if len(perm) <= max_indicators:
profile_permutations.add(", ".join(sorted(perm)))
return profile_permutations
# Insert computed categories into the JSON-loaded span
span["buy"]["buy_long_indicator"]["choices"] = sorted(
indicator_permutations(buy_indicators, max_indicators=4, include_none=True)
)
span["buy"]["buy_short_indicator"]["choices"] = sorted(
indicator_permutations(buy_indicators, max_indicators=4, include_none=True)
)
span["sell"]["sell_long_indicator"]["choices"] = sorted(
indicator_permutations(sell_indicators, max_indicators=4, include_none=True)
)
span["sell"]["sell_short_indicator"]["choices"] = sorted(
indicator_permutations(sell_indicators, max_indicators=4, include_none=True)
)
# Preload strategy attributes
strategy_attrs = {}
for section, keys in span.items():
for key in keys:
strategy_attrs[key] = get_param_config(span, section, key)
# 👇 Now define the strategy below
class Fibbo(IStrategy):
"""
Fibonacci Strategy with Indodax exchange workarounds.
Includes special handling for:
- Order creation delays (30s wait)
- Cancel order side requirements
"""
# 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
# Can this strategy go short?
can_short: bool = False
# Optimal timeframe for the strategy.
timeframe = "15m"
informative_timeframe = "1h"
process_only_new_candles = True
startup_candle_count = 900
# Hyperoptable parameters
stoploss = -0.1
minimal_roi = {
"0": 0.298,
"115": 0.144,
"280": 0.055,
"507": 0
}
macd_profiles = {
"1m": {
"fast": 6,
"slow": 13,
"signal": 4
},
"15m": {
"fast": 8,
"slow": 17,
"signal": 9
},
"1h": {
"fast": 12,
"slow": 26,
"signal": 9
},
}
order_time_in_force = {
"entry": "GTC",
"exit": "GTC"
}
# See the config
trailing_stop = True
use_exit_signal = True
exit_profit_only = False
use_custom_stoploss = True
ignore_roi_if_entry_signal = False
position_adjustment_enable = False
#max_entry_position_adjustment = 2
model_name = "LightGBMClassifier"
custom_pair_params: dict[str, dict[str, Any]] = {}
# Plot config
plot_config = {
"main_plot": {
"tema": {},
"sar": {"color": "white"},
},
"subplots": {
"&-s_close": {
"&-s_close": {"color": "green"}
},
"do_predict": {
"do_predict": {"color": "brown"},
},
"MACD": {
"macd": {"color": "blue"},
"macdsignal": {"color": "orange"},
},
"RSI": {
"rsi": {"color": "red"},
},
},
}
def __init__(self, config: dict) -> None:
super().__init__(config)
self.model_name = str(
self.config.get("freqaimodel")
or os.environ.get("FREQAI_MODEL")
or os.environ.get("FREQTRADE__FREQAIMODEL")
or self.model_name
)
self.custom_pair_params = getattr(self, "custom_pair_params", {}) or {}
# Override settings ONLY during hyperopt
if self.config.get('runmode') == 'hyperopt':
self.trailing_stop = True
self.use_exit_signal = False
self.use_custom_stoploss = False
self.minimal_roi = {"0": 100}
# Optional: update dynamic ROI logic
if hasattr(self, 'update_roi'):
self.update_roi()
# Optional: apply hyperopt value of max_open_trades to config
if hasattr(self, 'max_open_trades') and self.max_open_trades.value != -1:
self.config['max_open_trades'] = self.max_open_trades.value
# Make rolling window configurable
self.di_rolling_window = getattr(self, 'di_rolling_window', 200)
self.freqai_enabled = bool(self.freqai_info.get("enabled", False))
@property
def freqai_info(self) -> dict:
if hasattr(self, "_freqai_info_cache"):
return self._freqai_info_cache
freqai_config = {}
if hasattr(self, "config") and self.config:
freqai_config = self.config.get("freqai", {})
if not freqai_config and hasattr(self, "freqai") and hasattr(self.freqai, "freqai_info"):
freqai_config = self.freqai.freqai_info
if not freqai_config:
freqai_config = {
"enabled": False,
"identifier": "fibbo-default",
"fit_live_predictions_candles": 200,
"feature_parameters": {
"label_period_candles": 48,
"include_corr_pairlist": ["BTC/USDT:USDT", "ETH/USDT:USDT"],
"include_timeframes": [self.timeframe, self.informative_timeframe],
},
}
self._freqai_info_cache = freqai_config
return freqai_config
@freqai_info.setter
def freqai_info(self, value: dict) -> None:
self._freqai_info_cache = value or {}
def bot_start(self, **kwargs) -> None:
"""Called once after the bot has started and dependencies are available."""
if not self.config.get("dry_run", False):
patch_indodax_create_order()
patch_indodax_cancel_order()
patch_indodax_fetch_order()
logger.info("✅ Indodax patches applied (live mode).")
else:
logger.info(f"ℹ️ Indodax patches skipped (dry_run mode).")
def update_roi(self):
"""Update ROI based on current parameter values"""
self.minimal_roi = {
"0": float(self.roi_p1.value),
str(int(self.roi_t1.value)): float(self.roi_p2.value),
str(int(self.roi_t2.value)): float(self.roi_p3.value),
str(int(self.roi_t3.value)): 0
}
@property
def protections(self):
prot = []
# Disable protections during hyperopt if spaces contain 'all' or 'protection'
if hasattr(self, 'config'):
config: Configuration = self.config
if config.get('runmode') == 'hyperopt':
spaces = config.get('spaces', [])
if 'all' in spaces or 'protection' in spaces:
return prot
# Cooldown period to prevent over-trading
prot.append({
"method": "CooldownPeriod",
"stop_duration_candles": self.cooldown_lookback.value
})
# Stoploss guard to limit losses
if self.use_stop_protection.value:
prot.append({
"method": "StoplossGuard",
"lookback_period_candles": self.lookback_period_candles.value,
"stop_duration_candles": self.stop_duration_candles.value,
"trade_limit": self.trade_limit.value,
"only_per_pair": False
})
# Max drawdown guard
if self.use_max_drawdown_protection.value:
prot.append({
"method": "MaxDrawdown",
"lookback_period_candles": self.lookback_period_candles.value,
"stop_duration_candles": self.stop_duration_candles.value,
"trade_limit": self.max_drawdown_trade_limit.value,
"max_allowed_drawdown": 0.2,
"only_per_pair": False
})
# Low profit pairs guard
if self.use_low_profit.value:
prot.append({
"method": "LowProfitPairs",
"lookback_period_candles": self.lookback_period_candles.value,
"stop_duration": self.stop_duration_candles.value,
"trade_limit": self.low_profit_trade_limit.value,
"required_profit": 0.02,
"only_per_pair": False
})
return prot
def custom_params(self, pair: str, param: str):
return self.custom_pair_params.get(pair, {}).get(param, getattr(self, param).value)
# Optional: Custom stoploss based on FreqAI confidence
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
"""
Dynamic stoploss based on FreqAI confidence.
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# If FreqAI confidence is high, use tighter stoploss
if 'DI_values' in last_candle:
confidence = last_candle['DI_values']
# Adjust stoploss based on confidence
if confidence > 0.8:
# High confidence: tighter stoploss
return -0.05
elif confidence > 0.6:
# Medium confidence: normal stoploss
return self.stoploss
else:
# Low confidence: wider stoploss
return -0.15
return self.stoploss
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs):
"""
Custom exit logic - can be used for advanced risk management
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# Exit if entering high volatility regime with profit
if last_candle.get('%-market_regime', 0) == 3 and current_profit > 0.01:
return 'high_volatility_exit'
# Exit if model confidence drops (high DI values)
if last_candle.get('DI_values', 0) > 2.0:
return 'low_confidence_exit'
return None
# Optional: Leverage adjustment based on FreqAI
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
side: str, **kwargs) -> float:
"""
Adjust leverage based on FreqAI confidence.
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if 'DI_values' in last_candle:
confidence = last_candle['DI_values']
# Reduce leverage for low confidence predictions
if confidence < 0.5:
leverage_factor = 0.5
elif confidence < 0.7:
leverage_factor = 0.75
else:
leverage_factor = 1.0
adjusted_leverage = min(max_leverage, proposed_leverage * leverage_factor)
if adjusted_leverage != proposed_leverage:
logger.info(f"FreqAI adjusted leverage: {confidence:.2%} confidence, "
f"leverage {proposed_leverage:.1f} → {adjusted_leverage:.1f}")
return adjusted_leverage
return proposed_leverage
def ttm_squeeze(self, dataframe: DataFrame, bollinger_period: int = 20, keltner_period: int = 20, momentum_period: int = 12) -> DataFrame:
# Calculate Bollinger Bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=bollinger_period, stds=2)
# Calculate Keltner Channels
keltner = qtpylib.keltner_channel(dataframe, window=keltner_period)
# Calculate Momentum Histogram
momentum_hist = dataframe['close'] - dataframe['close'].shift(momentum_period)
# Determine squeeze conditions
squeeze_on = (bollinger['lower'] > keltner["lower"]) & (bollinger['upper'] < keltner["upper"])
squeeze_off = (bollinger['lower'] < keltner["lower"]) & (bollinger['upper'] > keltner["upper"])
dataframe['squeeze_on'] = squeeze_on
dataframe['squeeze_off'] = squeeze_off
dataframe['momentum_hist'] = momentum_hist
return dataframe
# ============ FreqAI Feature Engineering ============
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=period, stds=2.2
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
dataframe["%-bb_width-period"] = (
dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]
) / dataframe["bb_middleband-period"]
dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"]
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
dataframe["%-relative_volume-period"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
)
return dataframe
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(
self,
dataframe: DataFrame,
metadata: dict,
**kwargs
) -> DataFrame:
"""
FreqAI target definition for:
- Classifier
- ClassifierMultiTarget
- Regressor
- RegressorMultiTarget
"""
model_name = self.model_name.lower()
is_classifier = "classifier" in model_name
is_multi_target = "multitarget" in model_name
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
if is_classifier:
# ==================================================
# CLASSIFIERS
# ==================================================
if is_multi_target:
# CatboostClassifierMultiTarget
# IMPORTANT:
# - class labels must be UNIQUE across targets
# - target 1 uses {0, 1}
# - target 2 uses {2, 3}
self.freqai.class_names = [0, 1, 2, 3]
# Target 1: direction (0 = down, 1 = up)
dataframe["&s-up_or_down"] = (
dataframe["close"].shift(-label_period) > dataframe["close"]
).astype(int)
# Target 2: volatility (2 = low, 3 = high)
dataframe["&s-volatility"] = (
(
dataframe["close"].rolling(label_period).std()
> dataframe["close"].rolling(label_period).std().median()
).astype(int)
+ 2
)
else:
# CatboostClassifier (single target)
self.freqai.class_names = [0, 1]
dataframe["&s-up_or_down"] = (
dataframe["close"].shift(-label_period) > dataframe["close"]
).astype(int)
else:
# ==================================================
# REGRESSORS
# ==================================================
if is_multi_target:
# CatboostRegressorMultiTarget
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-label_period)
.rolling(label_period)
.mean()
/ dataframe["close"]
- 1
)
dataframe["&-s_range"] = (
dataframe["close"]
.shift(-label_period)
.rolling(label_period)
.max()
-
dataframe["close"]
.shift(-label_period)
.rolling(label_period)
.min()
)
else:
# CatboostRegressor
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-label_period)
.rolling(label_period)
.mean()
/ dataframe["close"]
- 1
)
return dataframe
# ============ Entry/Exit Logic ============
def informative_pairs(self):
"""
Define additional informative pairs
"""
whitelist_pairs = self.dp.current_whitelist()
corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
informative_pairs = []
for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]:
for pair in whitelist_pairs:
informative_pairs.append((pair, self.informative_timeframe))
for pair in corr_pairs:
if pair in whitelist_pairs:
continue
informative_pairs.append((pair, tf))
return informative_pairs
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata["pair"]
# --- FreqAI (robust for dynamic pairs) ---
if self.freqai is not None and self.freqai_enabled:
try:
# Start FreqAI
dataframe = self.freqai.start(dataframe, metadata, self)
# Process DI_values if available
if 'DI_values' in dataframe.columns:
# Check if we have enough data for meaningful percentile
if len(dataframe) >= self.di_rolling_window:
dataframe['di_percentile'] = (dataframe['DI_values']
.rolling(self.di_rolling_window)
.rank(pct=True))
logger.debug(f"FreqAI DI_percentile calculated for {pair}")
else:
# Not enough data yet, use neutral value
dataframe['di_percentile'] = 0.5
logger.debug(f"FreqAI: Insufficient data for {pair}, using neutral confidence")
# Log DI_values stats for debugging
logger.debug(f"DI_values - min: {dataframe['DI_values'].min():.3f}, "
f"max: {dataframe['DI_values'].max():.3f}, "
f"mean: {dataframe['DI_values'].mean():.3f}")
# Also log do_predict stats
if 'do_predict' in dataframe.columns:
buy_signals = (dataframe['do_predict'] == 1).sum()
sell_signals = (dataframe['do_predict'] == -1).sum()
logger.debug(f"FreqAI signals for {pair}: {buy_signals} buy, {sell_signals} sell")
except KeyError:
# Pair introduced dynamically without FreqAI history/model
logger.debug(f"FreqAI model not ready for {pair} - skipping AI signals")
except Exception as e:
# Extra safety: never let AI crash the strategy
logger.warning(f"FreqAI error for {pair}: {e}")
else:
if self.freqai is None:
logger.debug("FreqAI not initialized for this strategy")
# --- Classical indicators (always run) ---
# RSI
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.buy_rsi_period.value)
# VWAP (Lookahead bias Issue)
#dataframe['vwap'] = qtpylib.vwap(dataframe)
dataframe['vwap'] = qtpylib.rolling_vwap(dataframe, window=self.shared_vwap_window.value)
#dataframe['vwap'] = (((dataframe['high'] + dataframe['low'] + dataframe['close']) / 3) * dataframe['volume']).cumsum() / dataframe['volume'].cumsum()
# TTM Squeeze
dataframe = self.ttm_squeeze(dataframe)
dataframe['volume_mean'] = dataframe['volume'].rolling(self.shared_ttm_window.value).mean()
# ATR (Volatility)
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['atr_pct'] = dataframe['atr'] / dataframe['close']
# STOCHRSI (Missaligned Issue)
#stoch_rsi = ta.STOCHRSI(dataframe)
rsi_min = dataframe['rsi'].rolling(self.shared_stoch_period.value).min()
rsi_max = dataframe['rsi'].rolling(self.shared_stoch_period.value).max()
stoch_rsi = (dataframe['rsi'] - rsi_min) / ((rsi_max - rsi_min).replace(0, 1e-10))
# --- buy smoothing ---
dataframe['fastk_rsi_buy'] = (stoch_rsi * 100).rolling(self.buy_smoothK.value).mean()
dataframe['fastd_rsi_buy'] = dataframe['fastk_rsi_buy'].rolling(self.buy_smoothD.value).mean()
# --- sell smoothing ---
dataframe['fastk_rsi_sell'] = (stoch_rsi * 100).rolling(self.sell_smoothK.value).mean()
dataframe['fastd_rsi_sell'] = dataframe['fastk_rsi_sell'].rolling(self.sell_smoothD.value).mean()
# MACD (See Hyperopt Table)
macd = ta.MACD(dataframe, fastperiod=6, slowperiod=13, signalperiod=4)
dataframe['macd'] = macd['macd']
dataframe['macdhist'] = macd['macdhist']
dataframe['macdsignal'] = macd['macdsignal']
# Bollinger Bands
bollinger = ta.BBANDS(dataframe, timeperiod=max(2, int(self.buy_bb_period.value if self.buy_bb_period.value else 20)), nbdevup=2.0, nbdevdn=2.0, matype=0)
dataframe['bb_upperband'] = bollinger['upperband']
dataframe['bb_middleband'] = bollinger['middleband']
dataframe['bb_lowerband'] = bollinger['lowerband']
# EMA & DEMA
for period in span["buy"]["buy_slow_ema"]["choices"]:
dataframe[f'ema{period}'] = ta.EMA(dataframe, timeperiod=int(period))
for period in span["buy"]["buy_fast_dema"]["choices"]:
dataframe[f'dema{period}'] = ta.DEMA(dataframe, timeperiod=int(period))
# SWING high/low for Fibonacci levels
dataframe['swing_high'] = dataframe['high'].rolling(self.buy_swing_period.value).max()
dataframe['swing_low'] = dataframe['low'].rolling(self.buy_swing_period.value).min()
swing_range = dataframe['swing_high'] - dataframe['swing_low']
# LONG (retracement in uptrend)
dataframe['fib_long_0236'] = dataframe['swing_high'] - swing_range * 0.236
dataframe['fib_long_0382'] = dataframe['swing_high'] - swing_range * 0.382
dataframe['fib_long_0618'] = dataframe['swing_high'] - swing_range * 0.618
dataframe['fib_long_0786'] = dataframe['swing_high'] - swing_range * 0.786
# SHORT (retracement in downtrend)
dataframe['fib_short_0236'] = dataframe['swing_low'] + swing_range * 0.236
dataframe['fib_short_0382'] = dataframe['swing_low'] + swing_range * 0.382
dataframe['fib_short_0618'] = dataframe['swing_low'] + swing_range * 0.618
dataframe['fib_short_0786'] = dataframe['swing_low'] + swing_range * 0.786
# ---- Fetch and merge informative timeframe ----
logger.debug("Informative pairs data: %s", self.informative_pairs)
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.informative_timeframe)
if informative is None or 'close' not in informative.columns:
logger.error("Missing 'close' column in informative DataFrame for pair: %s", metadata['pair'])
return dataframe # Return original dataframe to prevent crashing
# Now it's safe to use 'close'
informative['atr'] = ta.ATR(informative, timeperiod=14)
informative['rsi'] = ta.RSI(informative, timeperiod=self.buy_rsi_period.value)
macd_inf = ta.MACD(informative, fastperiod=12, slowperiod=26, signalperiod=9)
informative['macd'] = macd_inf['macd']
informative['macdhist'] = macd_inf['macdhist']
informative['macdsignal'] = macd_inf['macdsignal']
for period in span["buy"]["buy_slow_ema"]["choices"]:
informative[f'ema{period}'] = ta.EMA(informative, timeperiod=int(period))
for period in span["buy"]["buy_fast_dema"]["choices"]:
informative[f'dema{period}'] = ta.DEMA(informative, timeperiod=int(period))
# Merge informative pair data into main dataframe
dataframe = merge_informative_pair(
dataframe,
informative,
self.timeframe,
self.informative_timeframe,
ffill=True
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Combine your Fibbo strategy with FreqAI predictions.
FreqAI columns are now available in the dataframe.
"""
logger.debug(f"Generating entry signals for {metadata['pair']}")
entry_long_conditions = []
entry_short_conditions = []
# === Your existing Fibbo conditions ===
RSI_LONG_ENTRY = dataframe['rsi'] < self.buy_rsi.value
RSI_SHORT_ENTRY = dataframe['rsi'] > self.buy_rsi.value
VWAP_LONG_ENTRY = dataframe['close'] > dataframe['vwap']
VWAP_SHORT_ENTRY = dataframe['close'] < dataframe['vwap']
BB_LONG_ENTRY = dataframe['close'] <= dataframe['bb_lowerband']
BB_SHORT_ENTRY = dataframe['close'] >= dataframe['bb_upperband']
MACD_LONG_ENTRY = dataframe['macd'] > dataframe['macdsignal']
MACD_SHORT_ENTRY = dataframe['macd'] < dataframe['macdsignal']
STOCHRSI_LONG_ENTRY = (
(dataframe['fastk_rsi_buy'] > dataframe['fastd_rsi_buy']) &
(dataframe['fastk_rsi_buy'] < self.buy_stoch_osc.value)
)
STOCHRSI_SHORT_ENTRY = (
(dataframe['fastk_rsi_buy'] < dataframe['fastd_rsi_buy']) &
(dataframe['fastk_rsi_buy'] > self.buy_stoch_osc.value)
)
DEMA_LONG_ENTRY = (
dataframe[f"dema{self.buy_fast_dema.value}"] >
dataframe[f"ema{self.buy_slow_ema.value}_{self.informative_timeframe}"]
)
DEMA_SHORT_ENTRY = (
dataframe[f"dema{self.buy_fast_dema.value}"] <
dataframe[f"ema{self.buy_slow_ema.value}_{self.informative_timeframe}"]
)
FIBBO_LONG_ENTRY = (
(dataframe['close'] >= (dataframe[f'fib_long_{str(self.buy_fib_level.value).replace(".", "")}'] * (1 - dataframe['atr_pct']))) &
(dataframe['close'] <= (dataframe[f'fib_long_{str(self.buy_fib_level.value).replace(".", "")}'] * (1 + dataframe['atr_pct'])))
)
FIBBO_SHORT_ENTRY = (
(dataframe['close'] >= (dataframe[f'fib_short_{str(self.buy_fib_level.value).replace(".", "")}'] * (1 - dataframe['atr_pct']))) &
(dataframe['close'] <= (dataframe[f'fib_short_{str(self.buy_fib_level.value).replace(".", "")}'] * (1 + dataframe['atr_pct'])))
)
# Always include FIBBO
entry_long_conditions.append(FIBBO_LONG_ENTRY)
entry_short_conditions.append(FIBBO_SHORT_ENTRY)
if "BB" in self.buy_long_indicator.value:
entry_long_conditions.append(BB_LONG_ENTRY)
if "BB" in self.buy_short_indicator.value:
entry_short_conditions.append(BB_SHORT_ENTRY)
if "RSI" in self.buy_long_indicator.value:
entry_long_conditions.append(RSI_LONG_ENTRY)
if "RSI" in self.buy_short_indicator.value:
entry_short_conditions.append(RSI_SHORT_ENTRY)
if "VWAP" in self.buy_long_indicator.value:
entry_long_conditions.append(VWAP_LONG_ENTRY)
if "VWAP" in self.buy_short_indicator.value:
entry_short_conditions.append(VWAP_SHORT_ENTRY)
if "MACD" in self.buy_long_indicator.value:
entry_long_conditions.append(MACD_LONG_ENTRY)
if "MACD" in self.buy_short_indicator.value:
entry_short_conditions.append(MACD_SHORT_ENTRY)
if "DEMA" in self.buy_long_indicator.value:
entry_long_conditions.append(DEMA_LONG_ENTRY)
if "DEMA" in self.buy_short_indicator.value:
entry_short_conditions.append(DEMA_SHORT_ENTRY)
if "STOCHRSI" in self.buy_long_indicator.value:
entry_long_conditions.append(STOCHRSI_LONG_ENTRY)
if "STOCHRSI" in self.buy_short_indicator.value:
entry_short_conditions.append(STOCHRSI_SHORT_ENTRY)
# === TTM Squeeze ENTRY (breakout model) ===
if "TTM" in self.buy_long_indicator.value:
squeeze_off = dataframe['squeeze_off']
momentum_positive = dataframe['momentum_hist'] > 0
entry_long_conditions.append(squeeze_off & momentum_positive)
if "TTM" in self.buy_short_indicator.value:
squeeze_off = dataframe['squeeze_off']
momentum_negative = dataframe['momentum_hist'] < 0
entry_short_conditions.append(squeeze_off & momentum_negative)
# === FreqAI Entry Signals ===
if 'do_predict' in dataframe.columns:
freqai_bullish = (dataframe['do_predict'] == 1)
freqai_bearish = (dataframe['do_predict'] == -1)
if 'di_percentile' in dataframe.columns:
long_conf = dataframe['di_percentile'] > float(self.buy_freqai.value)
short_conf = dataframe['di_percentile'] < float(self.sell_freqai.value)
# Enter LONG when bullish, Enter SHORT when bearish
entry_long_conditions.append(freqai_bullish & long_conf)
entry_short_conditions.append(freqai_bearish & short_conf)
else:
entry_long_conditions.append(freqai_bullish)
entry_short_conditions.append(freqai_bearish)
# Combine entry conditions with AND logic
# Enter if ALL conditions are met
if entry_long_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, entry_long_conditions),
'enter_long'
] = 1
if entry_short_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, entry_short_conditions),
'enter_short'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit logic combining Fibbo strategy with FreqAI sell signals.
"""
logger.debug(f"Generating exit signals for {metadata['pair']}")
exit_long_conditions = []
exit_short_conditions = []
# === Your existing Fibbo exit conditions ===
RSI_LONG_EXIT = dataframe['rsi'] >= self.sell_rsi.value
RSI_SHORT_EXIT = dataframe['rsi'] <= self.sell_rsi.value
VWAP_LONG_EXIT = dataframe['close'] < dataframe['vwap']
VWAP_SHORT_EXIT = dataframe['close'] > dataframe['vwap']
BB_LONG_EXIT = dataframe['close'] > dataframe['bb_middleband']
BB_SHORT_EXIT = dataframe['close'] < dataframe['bb_middleband']
MACD_LONG_EXIT = dataframe['macd'] < dataframe['macdsignal']
MACD_SHORT_EXIT = dataframe['macd'] > dataframe['macdsignal']
DEMA_LONG_EXIT = (
dataframe[f"dema{self.sell_fast_dema.value}"] <
dataframe[f"ema{self.sell_slow_ema.value}_{self.informative_timeframe}"]
)
DEMA_SHORT_EXIT = (
dataframe[f"dema{self.sell_fast_dema.value}"] >
dataframe[f"ema{self.sell_slow_ema.value}_{self.informative_timeframe}"]
)
STOCHRSI_LONG_EXIT = (
(dataframe['fastk_rsi_sell'] < dataframe['fastd_rsi_sell']) &
(dataframe['fastk_rsi_sell'] > self.sell_stoch_osc.value)
)
STOCHRSI_SHORT_EXIT = (
(dataframe['fastk_rsi_sell'] > dataframe['fastd_rsi_sell']) &
(dataframe['fastk_rsi_sell'] < self.sell_stoch_osc.value)
)
FIBBO_LONG_EXIT = (
(dataframe['close'] >= (dataframe[f'fib_long_{str(self.sell_fib_level.value).replace(".", "")}'] * (1 - dataframe['atr_pct']))) &
(dataframe['close'].shift(1) < (dataframe[f'fib_long_{str(self.sell_fib_level.value).replace(".", "")}'] * (1 - dataframe['atr_pct'])))
)
FIBBO_SHORT_EXIT = (
(dataframe['close'] <= (dataframe[f'fib_short_{str(self.sell_fib_level.value).replace(".", "")}'] * (1 + dataframe['atr_pct']))) &
(dataframe['close'].shift(1) > (dataframe[f'fib_short_{str(self.sell_fib_level.value).replace(".", "")}'] * (1 + dataframe['atr_pct'])))
)
# Always include FIBBO
exit_long_conditions.append(FIBBO_LONG_EXIT)
exit_short_conditions.append(FIBBO_SHORT_EXIT)
if "BB" in self.sell_long_indicator.value:
exit_long_conditions.append(BB_LONG_EXIT)
if "BB" in self.sell_short_indicator.value:
exit_short_conditions.append(BB_SHORT_EXIT)
if "RSI" in self.sell_long_indicator.value:
exit_long_conditions.append(RSI_LONG_EXIT)
if "RSI" in self.sell_short_indicator.value:
exit_short_conditions.append(RSI_SHORT_EXIT)
if "VWAP" in self.sell_long_indicator.value:
exit_long_conditions.append(VWAP_LONG_EXIT)
if "VWAP" in self.sell_short_indicator.value:
exit_short_conditions.append(VWAP_SHORT_EXIT)
if "DEMA" in self.sell_long_indicator.value:
exit_long_conditions.append(DEMA_LONG_EXIT)
if "DEMA" in self.sell_short_indicator.value:
exit_short_conditions.append(DEMA_SHORT_EXIT)
if "MACD" in self.sell_long_indicator.value:
exit_long_conditions.append(MACD_LONG_EXIT)
if "MACD" in self.sell_short_indicator.value:
exit_short_conditions.append(MACD_SHORT_EXIT)
if "STOCHRSI" in self.sell_long_indicator.value:
exit_long_conditions.append(STOCHRSI_LONG_EXIT)
if "STOCHRSI" in self.sell_short_indicator.value:
exit_short_conditions.append(STOCHRSI_SHORT_EXIT)
# === TTM Squeeze EXIT ===
if "TTM" in self.sell_long_indicator.value:
squeeze_on = dataframe['squeeze_on']
momentum_negative = dataframe['momentum_hist'] < 0
exit_long_conditions.append(squeeze_on & momentum_negative)
if "TTM" in self.sell_short_indicator.value:
squeeze_on = dataframe['squeeze_on']
momentum_positive = dataframe['momentum_hist'] > 0
exit_short_conditions.append(squeeze_on & momentum_positive)
# === FreqAI Exit Signals ===
if 'do_predict' in dataframe.columns:
freqai_bullish = (dataframe['do_predict'] == 1)
freqai_bearish = (dataframe['do_predict'] == -1)
if 'di_percentile' in dataframe.columns:
long_conf = dataframe['di_percentile'] > float(self.buy_freqai.value)
short_conf = dataframe['di_percentile'] < float(self.sell_freqai.value)
# Exit LONG when bearish, Exit SHORT when bullish
exit_long_conditions.append(freqai_bearish & short_conf)
exit_short_conditions.append(freqai_bullish & long_conf)
else:
exit_long_conditions.append(freqai_bearish)
exit_short_conditions.append(freqai_bullish)
# Combine exit conditions with AND logic
# Exit if ALL condition are met
if exit_long_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
'exit_long'
] = 1
if exit_short_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, exit_short_conditions),
'exit_short'
] = 1
return dataframe
# Inject hyperopt parameters AFTER class definition
for key, value in strategy_attrs.items():
setattr(Fibbo, key, value)