A comprehensive strategy using TA-Lib candlestick patterns and multiple indicators, adjusted to avoid lookahead bias (e.g., Ichimoku shift). Uses .value for IntParameter/DecimalParameter, preventing “object not callable” errors. Explicitly supports shorting (can_short = True), but also requires: - `enable_short: true` in your config - An exchange that supports margin/futures - A negative stoploss
Timeframe
1h
Direction
Long & Short
Stoploss
-20.0%
Trailing Stop
Yes
ROI
N/A
Interface Version
N/A
Startup Candles
52
Indicators
8
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
import logging
import numpy as np
import pandas as pd
from pandas import DataFrame
from datetime import datetime
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import IStrategy
import talib.abstract as ta
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter, BooleanParameter
# Optional for additional indicators
import pandas_ta as pta
logger = logging.getLogger(__name__)
class CompleteIndicatorStrategy2(IStrategy):
"""
A comprehensive strategy using TA-Lib candlestick patterns and multiple indicators,
adjusted to avoid lookahead bias (e.g., Ichimoku shift).
Uses .value for IntParameter/DecimalParameter, preventing “object not callable” errors.
Explicitly supports shorting (can_short = True), but also requires:
- `enable_short: true` in your config
- An exchange that supports margin/futures
- A negative stoploss
"""
# =====================================================================
# Strategy Core Settings
# =====================================================================
# Allow short trades
can_short = True
# A negative stoploss is required to handle potential short trade losses
stoploss = -0.20 # Example: 20% stop
# =====================================================================
# Strategy Hyperopt Parameters
# =====================================================================
# Leverage
leverage_level = IntParameter(low=1, high=5, default=2, space="buy")
# Score Thresholds (when to go long/short)
long_threshold = IntParameter(low=1, high=30, default=11, space="buy")
short_threshold = IntParameter(low=-30, high=-1, default=-4, space="sell")
# Trailing Stop
trailing_stop = True
trailing_stop_positive = 0.03
trailing_stop_positive_opt = DecimalParameter(0.01, 0.10, default=0.03, space="sell")
# Bullish Candlestick Pattern Weights
hammer_weight = IntParameter(low=1, high=5, default=3, space="buy")
morning_star_weight = IntParameter(low=1, high=5, default=3, space="buy")
three_white_soldiers_weight = IntParameter(low=1, high=5, default=3, space="buy")
piercing_weight = IntParameter(low=1, high=3, default=1, space="buy")
# Bearish Candlestick Pattern Weights
shooting_star_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
evening_star_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
three_black_crows_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
dark_cloud_cover_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")
# Engulfing & Harami & 3 Inside & Rising/Falling 3 Methods
bullish_engulfing_weight = IntParameter(low=1, high=5, default=3, space="buy")
bearish_engulfing_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
harami_bullish_weight = IntParameter(low=1, high=3, default=1, space="buy")
harami_bearish_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")
three_inside_up_weight = IntParameter(low=1, high=3, default=1, space="buy")
three_inside_down_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")
rising_three_methods_weight = IntParameter(low=1, high=3, default=1, space="buy")
falling_three_methods_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")
# Indicator Weights
macd_positive_weight = IntParameter(low=1, high=5, default=2, space="buy")
macd_negative_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")
rsi_oversold_weight = IntParameter(low=1, high=5, default=3, space="buy")
rsi_overbought_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
stoch_oversold_weight = IntParameter(low=1, high=5, default=3, space="buy")
stoch_overbought_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
adx_strong_trend_weight = IntParameter(low=1, high=5, default=2, space="buy")
adx_weak_trend_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")
sma_gap_positive_weight = IntParameter(low=1, high=5, default=2, space="buy")
sma_gap_negative_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")
ichimoku_bullish_weight = IntParameter(low=1, high=5, default=2, space="buy")
ichimoku_bearish_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")
# =====================================================================
# Timeframe & Startup
# =====================================================================
timeframe = '1h'
startup_candle_count = 52 # For Ichimoku + other extended indicators
def bot_start(self, **kwargs) -> None:
"""
Sync hyperopt-backed values into the runtime config fields that Freqtrade
validates as plain scalars during trading startup.
"""
super().bot_start(**kwargs)
self.trailing_stop_positive = float(self.trailing_stop_positive_opt.value)
logger.info("Using trailing_stop_positive=%s", self.trailing_stop_positive)
# =====================================================================
# POPULATE INDICATORS
# =====================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Add indicators and candlestick patterns to the dataframe.
Convert to numeric, handle missing data, shift Ichimoku to avoid lookahead.
"""
# Candlestick patterns (mostly 0 or +100)
dataframe['hammer'] = pd.to_numeric(ta.CDLHAMMER(dataframe), errors='coerce').fillna(0)
dataframe['shooting_star'] = pd.to_numeric(ta.CDLSHOOTINGSTAR(dataframe), errors='coerce').fillna(0)
dataframe['morning_star'] = pd.to_numeric(ta.CDLMORNINGSTAR(dataframe), errors='coerce').fillna(0)
dataframe['evening_star'] = pd.to_numeric(ta.CDLEVENINGSTAR(dataframe), errors='coerce').fillna(0)
dataframe['three_white_soldiers'] = pd.to_numeric(ta.CDL3WHITESOLDIERS(dataframe), errors='coerce').fillna(0)
dataframe['three_black_crows'] = pd.to_numeric(ta.CDL3BLACKCROWS(dataframe), errors='coerce').fillna(0)
dataframe['piercing'] = pd.to_numeric(ta.CDLPIERCING(dataframe), errors='coerce').fillna(0)
dataframe['dark_cloud_cover'] = pd.to_numeric(ta.CDLDARKCLOUDCOVER(dataframe), errors='coerce').fillna(0)
# Candlestick patterns (± sign possible)
dataframe['engulfing'] = pd.to_numeric(ta.CDLENGULFING(dataframe), errors='coerce').fillna(0)
dataframe['harami'] = pd.to_numeric(ta.CDLHARAMI(dataframe), errors='coerce').fillna(0)
dataframe['three_inside'] = pd.to_numeric(ta.CDL3INSIDE(dataframe), errors='coerce').fillna(0)
dataframe['rise_fall_3_methods'] = pd.to_numeric(ta.CDLRISEFALL3METHODS(dataframe), errors='coerce').fillna(0)
# SMAs
dataframe['sma_50'] = pd.to_numeric(ta.SMA(dataframe, timeperiod=50), errors='coerce').fillna(0)
dataframe['sma_200'] = pd.to_numeric(ta.SMA(dataframe, timeperiod=200), errors='coerce').fillna(0)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = pd.to_numeric(macd['macd'], errors='coerce').fillna(0)
dataframe['macd_signal'] = pd.to_numeric(macd['macdsignal'], errors='coerce').fillna(0)
# RSI
dataframe['rsi'] = pd.to_numeric(ta.RSI(dataframe), errors='coerce').fillna(0)
# Stochastic
stoch = ta.STOCH(dataframe)
dataframe['stoch_k'] = pd.to_numeric(stoch['slowk'], errors='coerce').fillna(0)
dataframe['stoch_d'] = pd.to_numeric(stoch['slowd'], errors='coerce').fillna(0)
# ADX, +DI, -DI
dataframe['adx'] = pd.to_numeric(ta.ADX(dataframe), errors='coerce').fillna(0)
dataframe['plus_di'] = pd.to_numeric(ta.PLUS_DI(dataframe), errors='coerce').fillna(0)
dataframe['minus_di'] = pd.to_numeric(ta.MINUS_DI(dataframe), errors='coerce').fillna(0)
# Bollinger Bands
bb = ta.BBANDS(dataframe)
dataframe['bb_upper'] = pd.to_numeric(bb['upperband'], errors='coerce').fillna(0)
dataframe['bb_middle'] = pd.to_numeric(bb['middleband'], errors='coerce').fillna(0)
dataframe['bb_lower'] = pd.to_numeric(bb['lowerband'], errors='coerce').fillna(0)
# Parabolic SAR
dataframe['sar'] = pd.to_numeric(ta.SAR(dataframe), errors='coerce').fillna(0)
# Ichimoku (shift leading spans 26 to avoid lookahead)
high9 = dataframe['high'].rolling(window=9).max()
low9 = dataframe['low'].rolling(window=9).min()
conversion_line = (high9 + low9) / 2
high26 = dataframe['high'].rolling(window=26).max()
low26 = dataframe['low'].rolling(window=26).min()
base_line = (high26 + low26) / 2
high52 = dataframe['high'].rolling(window=52).max()
low52 = dataframe['low'].rolling(window=52).min()
leading_span_a = ((conversion_line + base_line) / 2).shift(26)
leading_span_b = ((high52 + low52) / 2).shift(26)
dataframe['conversion_line'] = pd.to_numeric(conversion_line, errors='coerce').fillna(0)
dataframe['base_line'] = pd.to_numeric(base_line, errors='coerce').fillna(0)
dataframe['ichimoku_span_a'] = pd.to_numeric(leading_span_a, errors='coerce').fillna(0)
dataframe['ichimoku_span_b'] = pd.to_numeric(leading_span_b, errors='coerce').fillna(0)
# Ensure volume is numeric
dataframe['volume'] = pd.to_numeric(dataframe['volume'], errors='coerce').fillna(0)
return dataframe
# =====================================================================
# CALCULATE SCORE
# =====================================================================
def calculate_score(self, row: pd.Series) -> float:
"""
Combine candlestick patterns and indicator signals into a single numeric score.
+ve => bullish, -ve => bearish. Access IntParameter/DecimalParameter with .value.
"""
score = 0.0
# ---------- One-sided candlestick patterns ----------
if row['hammer'] > 0:
score += self.hammer_weight.value
if row['shooting_star'] > 0:
score += self.shooting_star_weight.value
if row['morning_star'] > 0:
score += self.morning_star_weight.value
if row['evening_star'] > 0:
score += self.evening_star_weight.value
if row['three_white_soldiers'] > 0:
score += self.three_white_soldiers_weight.value
if row['three_black_crows'] > 0:
score += self.three_black_crows_weight.value
if row['piercing'] > 0:
score += self.piercing_weight.value
if row['dark_cloud_cover'] > 0:
score += self.dark_cloud_cover_weight.value
# ---------- Two-sided candlestick patterns (check sign) ----------
if row['engulfing'] > 0:
score += self.bullish_engulfing_weight.value
elif row['engulfing'] < 0:
score += self.bearish_engulfing_weight.value
if row['harami'] > 0:
score += self.harami_bullish_weight.value
elif row['harami'] < 0:
score += self.harami_bearish_weight.value
if row['three_inside'] > 0:
score += self.three_inside_up_weight.value
elif row['three_inside'] < 0:
score += self.three_inside_down_weight.value
if row['rise_fall_3_methods'] > 0:
score += self.rising_three_methods_weight.value
elif row['rise_fall_3_methods'] < 0:
score += self.falling_three_methods_weight.value
# ---------- MACD ----------
if row['macd'] > row['macd_signal']:
score += self.macd_positive_weight.value
elif row['macd'] < row['macd_signal']:
score += self.macd_negative_weight.value
# ---------- RSI ----------
if row['rsi'] < 30:
score += self.rsi_oversold_weight.value
elif row['rsi'] > 70:
score += self.rsi_overbought_weight.value
# ---------- Stochastic ----------
if row['stoch_k'] < 20 and row['stoch_k'] > row['stoch_d']:
score += self.stoch_oversold_weight.value
elif row['stoch_k'] > 80 and row['stoch_k'] < row['stoch_d']:
score += self.stoch_overbought_weight.value
# ---------- ADX ----------
if row['adx'] > 25:
if row['plus_di'] > row['minus_di']:
score += self.adx_strong_trend_weight.value
elif row['minus_di'] > row['plus_di']:
score += self.adx_weak_trend_weight.value
# ---------- SMA gap ----------
if row['sma_50'] != 0:
gap_50 = (row['close'] - row['sma_50']) / row['sma_50']
if gap_50 > 0:
score += self.sma_gap_positive_weight.value
elif gap_50 < 0:
score += self.sma_gap_negative_weight.value
# ---------- Ichimoku ----------
if row['conversion_line'] > row['base_line'] and row['close'] > row['conversion_line']:
score += self.ichimoku_bullish_weight.value
elif row['conversion_line'] < row['base_line'] and row['close'] < row['conversion_line']:
score += self.ichimoku_bearish_weight.value
return score
# =====================================================================
# POPULATE ENTRY TREND
# =====================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Create 'enter_long' and 'enter_short' signals based on the 'score' thresholds.
Remember, you need `enable_short=true` in your config and an exchange that supports margin/futures.
"""
# Calculate the row-wise score
dataframe['score'] = dataframe.apply(self.calculate_score, axis=1)
# Long if score >= long_threshold
dataframe['enter_long'] = 0
dataframe.loc[
dataframe['score'] >= self.long_threshold.value,
'enter_long'
] = 1
# Short if score <= short_threshold
dataframe['enter_short'] = 0
dataframe.loc[
dataframe['score'] <= self.short_threshold.value,
'enter_short'
] = 1
return dataframe
# =====================================================================
# POPULATE EXIT TREND
# =====================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Basic exit logic: exit positions if 'score' crosses the opposite side of zero.
"""
dataframe['exit_long'] = 0
dataframe['exit_short'] = 0
# Exit long if the current score is negative
dataframe.loc[dataframe['score'] < 0, 'exit_long'] = 1
# Exit short if the current score is positive
dataframe.loc[dataframe['score'] > 0, 'exit_short'] = 1
return dataframe
# =====================================================================
# LEVERAGE (REQUIRED FOR FUTURES)
# =====================================================================
def leverage(
self,
pair: str,
current_time: datetime,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
side: str,
**kwargs
) -> float:
"""
Optional leverage management for futures trading.
Return a float, not an IntParameter object.
"""
# Make sure to return the .value so freqtrade receives a numeric value.
return float(self.leverage_level.value)