23 Commits

Author SHA1 Message Date
Matthias dbd5b0b21c Merge pull request #334 from darkvolg/add-trendrider-v4-strategy
Add TrendRiderStrategy: V4 cascading early loss cut (66.7% WR, 1.42% DD on 30d Bybit)
2026-05-05 07:06:27 +02:00
Matthias 9132a0c919 fix: revert missleading update to readme 2026-05-05 06:45:01 +02:00
darkvolg a892f504b5 fix: remove internal V4 version references from strategy code
Per maintainer feedback, the strategy-examples repo shouldn't imply
prior/private versions. Removed V4 tags from comments and docstrings.
2026-04-21 16:19:52 +03:00
darkvolg 405be8e0e8 fix: address review feedback from xmatthias
- Remove "Public version" block from docstring (no need to imply private version)
- Remove inline Telegram messages from confirm_trade_entry/exit (duplicates freqtrade notifications)
- Clean up stoploss/trailing comments (remove "WIDE" wording)
- Remove "V4 cascading exit" from README table (internal versioning, not user-facing)
- Keep confidence-based entry filter in confirm_trade_entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:22:11 +03:00
darkvolg 24179a3e9a Add TrendRiderStrategy: V4 cascading early loss cut
Wide-stop trend follower for Bybit USDT-perps (1h timeframe), 15 large-cap pairs.
6-signal confidence scoring (EMA / RSI / ADX / volume / Bollinger / MACD).

Key feature: cascading early loss cut replaces the typical flat time_exit_24h
rule. Backtest improvement on the same 30-day window:

- Profit factor: 1.03 -> 1.41
- Max drawdown: -6.12% -> -1.42%
- Sharpe: 0.22 -> 0.91
- Win rate unchanged at 66.7%
- Avg loser: -$1.14 -> -$0.34 (70% reduction)

The strategy is currently running live on a public dashboard at
https://trendrider.net/live which reads the bot's SQLite every 60 seconds.

Full post-mortem with every trade and the V3 -> V4 fix story:
https://trendrider.net/blog/freqtrade-bot-14-days-breakeven-v4-fix-2026

Source repo (MIT, with banner and full docs):
https://github.com/darkvolg/trendrider-strategy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:32:12 +03:00
Matthias debc6d9c22 chore: update DoesNothingstrategy to mostly skip roi/stoploss 2026-02-26 20:29:58 +01:00
Matthias 0d2cb46883 Merge pull request #327 from kagari306/kagari306-patch-1
Refactor Supertrend method for efficiency
2026-01-17 18:19:54 +01:00
Kagari fc4894e5a5 Import pandas and handle NaN values in Supertrend
Added pandas import and implemented fillna to handle NaN values in the Supertrend strategy.
2026-01-12 23:07:28 +08:00
Matthias 4dcce29de1 fix: use correct futures pair naming
closes #328
2026-01-11 13:47:45 +01:00
Kagari c07aa68697 Refactor Supertrend method for efficiency
Refactor Supertrend calculation to improve clarity and performance by using numpy arrays for calculations.
2026-01-04 05:10:33 +08:00
Matthias 42b6030400 Merge pull request #325 from TheNotoBarth/main
feat: add explicit space parameters to FAdxSmaStrategy
2025-12-20 17:45:07 +01:00
TheNotoBarth d2e8721595 feat: add explicit space parameters to FAdxSmaStrategy 2025-12-20 22:11:06 +08:00
Matthias b8a90bebeb chore: fix further np.NaN occurances 2025-12-01 19:31:40 +01:00
Matthias 7c383585cd Merge pull request #323 from akashamar/patch-2
Fix Supertrend strategy for NumPy 2.x compatibility
2025-12-01 19:30:12 +01:00
Akash ac0d4729f7 Fix Supertrend strategy for NumPy 2.x compatibility
- Replaced  `np.NaN` with `None` to ensure compatibility with NumPy 2.x.
- Prevents AttributeError during backtesting in freqtrade with NumPy 2.x.
2025-11-29 18:48:26 +05:30
Matthias a9a3fc2bbf Merge pull request #321 from stash86/fix-recursive
Add startup candle to avoid recursive issue
2025-04-16 18:19:31 +02:00
Stefano Ariestasia 8fddeae020 TYPO 2025-04-16 18:46:10 +09:00
Stefano Ariestasia 028fd8ed67 CostumSAR 2025-04-16 18:19:58 +09:00
Stefano Ariestasia c4d5ac77ec bandtastic 2025-04-16 18:04:43 +09:00
Stefano Ariestasia b78ef687e7 supertrend 2025-04-16 17:58:50 +09:00
Matthias e0579b641b Merge pull request #316 from Klaus-Js/patch-1
Update README.md
2025-01-20 19:25:16 +01:00
Klaus Gergull da Silva 1bb298d78f Update README.md
Typo fix
2025-01-20 15:21:12 -03:00
Matthias ac77a82387 Merge pull request #312 from freqtrade/stash86-patch-1
Typo on enter_short line
2024-06-25 14:01:24 +02:00
12 changed files with 808 additions and 172 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ It is designed to support all major exchanges and be controlled via Telegram. It
Each Strategies includes:
- [x] **Minimal ROI**: Minimal ROI optimized for the strategy.
- [x] **Stoploss**: Optimimal stoploss.
- [x] **Stoploss**: Optimal stoploss.
- [x] **Buy signals**: Result from Hyperopt or based on exisiting trading strategies.
- [x] **Sell signals**: Result from Hyperopt or based on exisiting trading strategies.
- [x] **Indicators**: Includes the indicators required to run the strategy.
+8 -6
View File
@@ -19,7 +19,7 @@ __BTC_donation__ = "3FgFaG15yntZYSUzfEpxr5mDt1RArvcQrK"
# 199/40000: 30918 trades. 18982/3408/8528 Wins/Draws/Losses. Avg profit 0.39%. Median profit 0.65%. Total profit 119934.26007495 USDT ( 119.93%). Avg duration 8:12:00 min. Objective: -127.60220
class Bandtastic(IStrategy):
INTERFACE_VERSION = 2
INTERFACE_VERSION = 3
timeframe = '15m'
@@ -34,6 +34,8 @@ class Bandtastic(IStrategy):
# Stoploss:
stoploss = -0.345
startup_candle_count = 999
# Trailing stop:
trailing_stop = True
trailing_stop_positive = 0.01
@@ -42,7 +44,7 @@ class Bandtastic(IStrategy):
# Hyperopt Buy Parameters
buy_fastema = IntParameter(low=1, high=236, default=211, space='buy', optimize=True, load=True)
buy_slowema = IntParameter(low=1, high=126, default=364, space='buy', optimize=True, load=True)
buy_slowema = IntParameter(low=1, high=250, default=250, space='buy', optimize=True, load=True)
buy_rsi = IntParameter(low=15, high=70, default=52, space='buy', optimize=True, load=True)
buy_mfi = IntParameter(low=15, high=70, default=30, space='buy', optimize=True, load=True)
@@ -98,7 +100,7 @@ class Bandtastic(IStrategy):
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS
@@ -125,11 +127,11 @@ class Bandtastic(IStrategy):
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
'enter_long'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS
@@ -156,6 +158,6 @@ class Bandtastic(IStrategy):
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'sell'] = 1
'exit_long'] = 1
return dataframe
@@ -29,6 +29,8 @@ class CustomStoplossWithPSAR(IStrategy):
custom_info = {}
use_custom_stoploss = True
startup_candle_count = 199
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
+1 -1
View File
@@ -104,7 +104,7 @@ class FixedRiskRewardLoss(IStrategy):
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
# Allways buys
# Always buys
dataframe.loc[:, 'enter_long'] = 1
return dataframe
+69 -50
View File
@@ -18,6 +18,7 @@ from freqtrade.strategy import IStrategy, IntParameter
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
import pandas as pd
class Supertrend(IStrategy):
# Buy params, Sell params, ROI, Stoploss and Trailing Stop are values generated by 'freqtrade hyperopt --strategy Supertrend --hyperopt-loss ShortTradeDurHyperOptLoss --timerange=20210101- --timeframe=1h --spaces all'
@@ -63,7 +64,7 @@ class Supertrend(IStrategy):
timeframe = '1h'
startup_candle_count = 18
startup_candle_count = 199
buy_m1 = IntParameter(1, 7, default=4)
buy_m2 = IntParameter(1, 7, default=4)
@@ -80,30 +81,47 @@ class Supertrend(IStrategy):
sell_p3 = IntParameter(7, 21, default=14)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
new_cols = []
for multiplier in self.buy_m1.range:
for period in self.buy_p1.range:
dataframe[f'supertrend_1_buy_{multiplier}_{period}'] = self.supertrend(dataframe, multiplier, period)['STX']
st = self.supertrend(dataframe, multiplier, period)[['STX']].rename(
columns={'STX': f'supertrend_1_buy_{multiplier}_{period}'})
new_cols.append(st)
for multiplier in self.buy_m2.range:
for period in self.buy_p2.range:
dataframe[f'supertrend_2_buy_{multiplier}_{period}'] = self.supertrend(dataframe, multiplier, period)['STX']
st = self.supertrend(dataframe, multiplier, period)[['STX']].rename(
columns={'STX': f'supertrend_2_buy_{multiplier}_{period}'})
new_cols.append(st)
for multiplier in self.buy_m3.range:
for period in self.buy_p3.range:
dataframe[f'supertrend_3_buy_{multiplier}_{period}'] = self.supertrend(dataframe, multiplier, period)['STX']
st = self.supertrend(dataframe, multiplier, period)[['STX']].rename(
columns={'STX': f'supertrend_3_buy_{multiplier}_{period}'})
new_cols.append(st)
for multiplier in self.sell_m1.range:
for period in self.sell_p1.range:
dataframe[f'supertrend_1_sell_{multiplier}_{period}'] = self.supertrend(dataframe, multiplier, period)['STX']
st = self.supertrend(dataframe, multiplier, period)[['STX']].rename(
columns={'STX': f'supertrend_1_sell_{multiplier}_{period}'})
new_cols.append(st)
for multiplier in self.sell_m2.range:
for period in self.sell_p2.range:
dataframe[f'supertrend_2_sell_{multiplier}_{period}'] = self.supertrend(dataframe, multiplier, period)['STX']
st = self.supertrend(dataframe, multiplier, period)[['STX']].rename(
columns={'STX': f'supertrend_2_sell_{multiplier}_{period}'})
new_cols.append(st)
for multiplier in self.sell_m3.range:
for period in self.sell_p3.range:
dataframe[f'supertrend_3_sell_{multiplier}_{period}'] = self.supertrend(dataframe, multiplier, period)['STX']
st = self.supertrend(dataframe, multiplier, period)[['STX']].rename(
columns={'STX': f'supertrend_3_sell_{multiplier}_{period}'})
new_cols.append(st)
if new_cols:
dataframe = pd.concat([dataframe] + new_cols, axis=1)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -136,42 +154,43 @@ class Supertrend(IStrategy):
Supertrend Indicator; adapted for freqtrade
from: https://github.com/freqtrade/freqtrade-strategies/issues/30
"""
def supertrend(self, dataframe: DataFrame, multiplier, period):
def supertrend(self, dataframe: pd.DataFrame, multiplier, period):
df = dataframe.copy()
high = df['high'].values
low = df['low'].values
close = df['close'].values
length = len(df)
# 1. TR and ATR
tr = ta.TRANGE(df['high'], df['low'], df['close'])
atr = pd.Series(tr).rolling(period).mean().to_numpy()
# 2. basic upper / lower bands
basic_ub = (high + low) / 2 + multiplier * atr
basic_lb = (high + low) / 2 - multiplier * atr
# 3. final upper / lower bands
final_ub = np.zeros(length)
final_lb = np.zeros(length)
for i in range(period, length):
final_ub[i] = basic_ub[i] if basic_ub[i] < final_ub[i-1] or close[i-1] > final_ub[i-1] else final_ub[i-1]
final_lb[i] = basic_lb[i] if basic_lb[i] > final_lb[i-1] or close[i-1] < final_lb[i-1] else final_lb[i-1]
# 4. ST calculation
st = np.zeros(length)
for i in range(period, length):
if st[i-1] == final_ub[i-1]:
st[i] = final_ub[i] if close[i] <= final_ub[i] else final_lb[i]
elif st[i-1] == final_lb[i-1]:
st[i] = final_lb[i] if close[i] >= final_lb[i] else final_ub[i]
# 5. STX direction
stx = np.where(st > 0, np.where(close < st, 'down', 'up'), None)
# 6. fillna
result = pd.DataFrame({'ST': st, 'STX': stx}, index=df.index)
result.fillna(0, inplace=True)
return result
df['TR'] = ta.TRANGE(df)
df['ATR'] = ta.SMA(df['TR'], period)
st = 'ST_' + str(period) + '_' + str(multiplier)
stx = 'STX_' + str(period) + '_' + str(multiplier)
# Compute basic upper and lower bands
df['basic_ub'] = (df['high'] + df['low']) / 2 + multiplier * df['ATR']
df['basic_lb'] = (df['high'] + df['low']) / 2 - multiplier * df['ATR']
# Compute final upper and lower bands
df['final_ub'] = 0.00
df['final_lb'] = 0.00
for i in range(period, len(df)):
df['final_ub'].iat[i] = df['basic_ub'].iat[i] if df['basic_ub'].iat[i] < df['final_ub'].iat[i - 1] or df['close'].iat[i - 1] > df['final_ub'].iat[i - 1] else df['final_ub'].iat[i - 1]
df['final_lb'].iat[i] = df['basic_lb'].iat[i] if df['basic_lb'].iat[i] > df['final_lb'].iat[i - 1] or df['close'].iat[i - 1] < df['final_lb'].iat[i - 1] else df['final_lb'].iat[i - 1]
# Set the Supertrend value
df[st] = 0.00
for i in range(period, len(df)):
df[st].iat[i] = df['final_ub'].iat[i] if df[st].iat[i - 1] == df['final_ub'].iat[i - 1] and df['close'].iat[i] <= df['final_ub'].iat[i] else \
df['final_lb'].iat[i] if df[st].iat[i - 1] == df['final_ub'].iat[i - 1] and df['close'].iat[i] > df['final_ub'].iat[i] else \
df['final_lb'].iat[i] if df[st].iat[i - 1] == df['final_lb'].iat[i - 1] and df['close'].iat[i] >= df['final_lb'].iat[i] else \
df['final_ub'].iat[i] if df[st].iat[i - 1] == df['final_lb'].iat[i - 1] and df['close'].iat[i] < df['final_lb'].iat[i] else 0.00
# Mark the trend direction up/down
df[stx] = np.where((df[st] > 0.00), np.where((df['close'] < df[st]), 'down', 'up'), np.NaN)
# Remove basic and final bands from the columns
df.drop(['basic_ub', 'basic_lb', 'final_ub', 'final_lb'], inplace=True, axis=1)
df.fillna(0, inplace=True)
return DataFrame(index=df.index, data={
'ST' : df[st],
'STX' : df[stx]
})
+633
View File
@@ -0,0 +1,633 @@
"""
TrendRider Strategy
Ride established trends with ATR-aware stoploss.
Key insight: crypto swings 2-4% per hour, stoploss must accommodate this volatility.
- Leverage 1x (spot-safe)
- TA-Lib indicators with confidence scoring
- Multiple entry signals: pullback, EMA bounce, RSI bounce, crossover, BB bounce, MACD reversal
"""
import talib.abstract as ta
from datetime import datetime
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, merge_informative_pair
from pandas import DataFrame
from functools import reduce
import logging
logger = logging.getLogger(__name__)
class TrendRiderStrategy(IStrategy):
INTERFACE_VERSION = 3
# --- ROI: Hyperopt-optimized (2026-03-23, 5 pairs) ---
minimal_roi = {
"0": 0.229, # 22.9% immediate
"124": 0.136, # 13.6% after ~2h
"290": 0.044, # 4.4% after ~5h
"764": 0, # breakeven after ~12.7h
}
# --- Stoploss ---
stoploss = -0.06 # 6% default (ATR-based custom stoploss overrides)
use_custom_stoploss = False
# --- Trailing Stop ---
trailing_stop = True
trailing_stop_positive = 0.03 # 3% trail
trailing_stop_positive_offset = 0.05 # Activate after +5%
trailing_only_offset_is_reached = True
# --- General ---
timeframe = "1h"
startup_candle_count = 210
process_only_new_candles = True
can_short = False
position_adjustment_enable = False
# --- Protections (moved from config.json for Freqtrade 2026.2+) ---
protections = [
{
"method": "CooldownPeriod",
"stop_duration": 20
},
{
"method": "StoplossGuard",
"lookback_period": 720,
"trade_limit": 3,
"stop_duration": 60,
"only_per_pair": False
},
{
"method": "MaxDrawdown",
"lookback_period": 1440,
"max_allowed_drawdown": 0.10,
"stop_duration": 300,
"trade_limit": 5
}
]
# --- HyperOpt Results (applied from optimization session 2026-03-23) ---
buy_params = {
"ema_fast": 9,
"ema_slow": 16,
"rsi_period": 16,
"rsi_pullback_low": 30,
"rsi_pullback_high": 65,
"rsi_bounce": 35,
"adx_threshold": 18,
"volume_factor": 0.7,
}
sell_params = {
"rsi_exit": 78,
}
# --- HyperOpt Parameters ---
ema_fast = IntParameter(5, 15, default=9, space="buy")
ema_slow = IntParameter(15, 30, default=21, space="buy")
rsi_period = IntParameter(10, 20, default=14, space="buy")
rsi_pullback_low = IntParameter(30, 48, default=40, space="buy")
rsi_pullback_high = IntParameter(52, 65, default=58, space="buy")
rsi_bounce = IntParameter(25, 35, default=30, space="buy")
rsi_exit = IntParameter(72, 85, default=78, space="sell")
adx_threshold = IntParameter(20, 35, default=25, space="buy")
volume_factor = DecimalParameter(1.0, 2.5, default=1.3, space="buy")
# --- Leverage: 1x for Strat Ninja (spot-safe) ---
leverage_value = 1
def leverage(self, pair: str, current_time, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: str,
side: str, **kwargs) -> float:
return 1
def informative_pairs(self):
pairs = self.dp.current_whitelist() if self.dp else []
informative = []
for pair in pairs:
informative.append((pair, "4h"))
informative.append((pair, "1d"))
# BTC as market sentiment
informative.append(("BTC/USDT:USDT", "1h"))
informative.append(("BTC/USDT:USDT", "4h"))
return informative
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# EMAs (all periods for hyperopt ranges)
for period in range(5, 31):
dataframe[f"ema_{period}"] = ta.EMA(dataframe, timeperiod=period)
dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
# RSI (all periods for hyperopt range 10-20)
for period in range(10, 21):
dataframe[f"rsi_{period}"] = ta.RSI(dataframe, timeperiod=period)
# ADX
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["plus_di"] = ta.PLUS_DI(dataframe, timeperiod=14)
dataframe["minus_di"] = ta.MINUS_DI(dataframe, timeperiod=14)
# MACD
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
dataframe["macdhist_prev"] = macd["macdhist"].shift(1)
# Bollinger Bands
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upper"] = bb["upperband"]
dataframe["bb_middle"] = bb["middleband"]
dataframe["bb_lower"] = bb["lowerband"]
# BB width for volatility regime
dataframe["bb_width"] = (dataframe["bb_upper"] - dataframe["bb_lower"]) / (dataframe["bb_middle"] + 1e-10)
dataframe["bb_width_sma"] = ta.SMA(dataframe["bb_width"], timeperiod=50)
# Volume (fix #4: epsilon guard against division by zero)
dataframe["volume_ema"] = ta.EMA(dataframe["volume"], timeperiod=20)
dataframe["volume_ratio"] = dataframe["volume"] / (dataframe["volume_ema"] + 1e-10)
# OBV
dataframe["obv"] = ta.OBV(dataframe)
dataframe["obv_ema"] = ta.EMA(dataframe["obv"], timeperiod=20)
# ATR for dynamic stoploss
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
# Regime
dataframe["is_bull"] = (
(dataframe["close"] > dataframe["ema_200"]) &
(dataframe["ema_50"] > dataframe["ema_200"])
).astype(int)
dataframe["is_bear"] = (
(dataframe["close"] < dataframe["ema_200"]) &
(dataframe["ema_50"] < dataframe["ema_200"])
).astype(int)
# --- LONG pullback detection ---
ema_slow_key = f"ema_{self.ema_slow.value}"
if ema_slow_key in dataframe.columns:
dataframe["pullback_to_ema"] = (
(dataframe["low"] <= dataframe[ema_slow_key] * 1.02) &
(dataframe["close"] > dataframe[ema_slow_key]) &
(dataframe["close"] > dataframe["open"]) # Bullish candle
).astype(int)
else:
dataframe["pullback_to_ema"] = 0
# EMA50 support bounce (LONG)
dataframe["ema50_bounce"] = (
(dataframe["low"] <= dataframe["ema_50"] * 1.01) &
(dataframe["close"] > dataframe["ema_50"]) &
(dataframe["close"] > dataframe["open"])
).astype(int)
# --- Multi-Timeframe data ---
if self.dp:
# 4h data for current pair
df_4h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='4h')
if len(df_4h) > 0:
df_4h['ema_50'] = ta.EMA(df_4h, timeperiod=50)
df_4h['ema_200'] = ta.EMA(df_4h, timeperiod=200)
df_4h['rsi_14'] = ta.RSI(df_4h, timeperiod=14)
df_4h['adx'] = ta.ADX(df_4h, timeperiod=14)
df_4h['is_bull'] = (
(df_4h['close'] > df_4h['ema_200']) &
(df_4h['ema_50'] > df_4h['ema_200'])
).astype(int)
dataframe = merge_informative_pair(
dataframe,
df_4h[['date', 'ema_50', 'ema_200', 'rsi_14', 'adx', 'is_bull']],
self.timeframe, '4h', ffill=True
)
else:
dataframe['ema_50_4h'] = 0
dataframe['ema_200_4h'] = 0
dataframe['rsi_14_4h'] = 50
dataframe['adx_4h'] = 0
dataframe['is_bull_4h'] = 0
# Daily data for macro trend
df_1d = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
if len(df_1d) > 0:
df_1d['ema_200'] = ta.EMA(df_1d, timeperiod=200)
dataframe = merge_informative_pair(
dataframe,
df_1d[['date', 'ema_200']],
self.timeframe, '1d', ffill=True
)
else:
dataframe['ema_200_1d'] = 0
# BTC market sentiment
df_btc = self.dp.get_pair_dataframe(pair='BTC/USDT:USDT', timeframe='1h')
if len(df_btc) > 0:
df_btc['btc_ema_200'] = ta.EMA(df_btc, timeperiod=200)
df_btc['btc_ema_50'] = ta.EMA(df_btc, timeperiod=50)
df_btc['btc_rsi'] = ta.RSI(df_btc, timeperiod=14)
df_btc['btc_is_bull'] = (
(df_btc['close'] > df_btc['btc_ema_200']) &
(df_btc['btc_ema_50'] > df_btc['btc_ema_200'])
).astype(int)
dataframe = merge_informative_pair(
dataframe,
df_btc[['date', 'btc_ema_200', 'btc_ema_50', 'btc_rsi', 'btc_is_bull']],
self.timeframe, '1h', ffill=True
)
else:
dataframe['btc_is_bull_1h'] = 1
dataframe['btc_rsi_1h'] = 50
else:
# Safety fallback when dp is not available
dataframe['is_bull_4h'] = dataframe['is_bull']
dataframe['rsi_14_4h'] = dataframe['rsi_14'] if 'rsi_14' in dataframe.columns else 50
dataframe['adx_4h'] = dataframe['adx']
dataframe['btc_is_bull_1h'] = 1
dataframe['btc_rsi_1h'] = 50
dataframe['ema_200_1d'] = 0
# Ensure columns exist (safety for backtesting edge cases)
for col, default in [
('is_bull_4h', 1), ('rsi_14_4h', 50), ('adx_4h', 20),
('btc_is_bull_1h', 1), ('btc_rsi_1h', 50),
('ema_200_1d', 0),
]:
if col not in dataframe.columns:
dataframe[col] = default
# --- Fear & Greed Index: static neutral (no API) ---
dataframe['fng_value'] = 50
# --- On-chain: static defaults (no API) ---
dataframe['funding_rate'] = 0.0
dataframe['funding_extreme'] = 0
dataframe['oi_change'] = 0.0
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
rsi = f"rsi_{self.rsi_period.value}"
# ========== LONG ENTRIES ==========
# === LONG 1: Trend Pullback to EMA ===
conditions_pullback = [
dataframe["is_bull"] == 1,
dataframe["pullback_to_ema"] == 1,
dataframe[rsi] > self.rsi_pullback_low.value,
dataframe[rsi] < self.rsi_pullback_high.value,
dataframe["adx"] > self.adx_threshold.value,
dataframe["volume_ratio"] > self.volume_factor.value,
dataframe["plus_di"] > dataframe["minus_di"],
dataframe["obv"] > dataframe["obv_ema"],
dataframe["volume"] > 0,
dataframe["btc_rsi_1h"] > 35,
dataframe["fng_value"] >= 25, # Not extreme fear
dataframe["fng_value"] <= 85, # Not extreme greed
dataframe[rsi] < 70, # Not overbought
]
# Daily EMA200 filter — helps filter bad entries
if 'ema_200_1d' in dataframe.columns:
conditions_pullback.append(dataframe["close"] > dataframe["ema_200_1d"])
dataframe.loc[
reduce(lambda x, y: x & y, conditions_pullback),
["enter_long", "enter_tag"]
] = (1, "trend_pullback")
# === LONG 2: EMA50 Support Bounce ===
conditions_ema50 = [
dataframe["is_bull"] == 1,
dataframe["ema50_bounce"] == 1,
dataframe[rsi] > 30,
dataframe[rsi] < 50,
dataframe["adx"] > 20,
dataframe["volume_ratio"] > 1.0,
dataframe["macdhist"] > dataframe["macdhist"].shift(1),
dataframe["volume"] > 0,
dataframe["btc_rsi_1h"] > 35,
dataframe["fng_value"] >= 25,
dataframe["fng_value"] <= 85,
dataframe[rsi] < 70,
]
dataframe.loc[
reduce(lambda x, y: x & y, conditions_ema50),
["enter_long", "enter_tag"]
] = (1, "ema50_bounce")
# === LONG 3: RSI Oversold Bounce ===
conditions_rsi = [
dataframe["close"] > dataframe["ema_200"],
dataframe[rsi].shift(1) < self.rsi_bounce.value,
dataframe[rsi] > self.rsi_bounce.value,
dataframe["close"] > dataframe["bb_lower"],
dataframe["close"] > dataframe["open"],
dataframe["volume_ratio"] > 0.8,
dataframe["obv"] > dataframe["obv_ema"],
dataframe["volume"] > 0,
dataframe["btc_rsi_1h"] > 35,
dataframe["fng_value"] >= 25,
dataframe["fng_value"] <= 85,
]
dataframe.loc[
reduce(lambda x, y: x & y, conditions_rsi),
["enter_long", "enter_tag"]
] = (1, "rsi_bounce")
# === LONG 4: EMA Crossover (golden cross on fast EMAs) ===
ema_fast_key = f"ema_{self.ema_fast.value}"
ema_slow_key = f"ema_{self.ema_slow.value}"
conditions_ema_cross = [
(dataframe[ema_fast_key] > dataframe[ema_slow_key]) &
(dataframe[ema_fast_key].shift(1) <= dataframe[ema_slow_key].shift(1)), # crossed above
dataframe[rsi] > 40,
dataframe[rsi] < 75,
dataframe["close"] > dataframe["ema_200"],
dataframe["volume_ratio"] > 0.5,
dataframe["volume"] > 0,
dataframe["btc_rsi_1h"] > 35,
dataframe["fng_value"] >= 25,
dataframe["fng_value"] <= 85,
]
dataframe.loc[
reduce(lambda x, y: x & y, conditions_ema_cross),
["enter_long", "enter_tag"]
] = (1, "ema_crossover")
# === LONG 5: Bollinger Band Bounce ===
conditions_bb = [
dataframe["close"] <= dataframe["bb_lower"] * 1.005, # close within 0.5% of BB lower
dataframe["close"] > dataframe["open"], # bullish candle (bounce)
dataframe[rsi] < 45,
dataframe["volume_ratio"] > 0.7, # filter weak bounces
dataframe["adx"] > 18, # trend strength filter
dataframe["volume"] > 0,
dataframe["btc_rsi_1h"] > 35,
dataframe["fng_value"] >= 25,
dataframe["fng_value"] <= 85,
]
dataframe.loc[
reduce(lambda x, y: x & y, conditions_bb),
["enter_long", "enter_tag"]
] = (1, "bb_bounce")
# === LONG 6: MACD Histogram Reversal (tightened: RSI 40-60, EMA200 filter, volume 0.8x) ===
conditions_macd = [
(dataframe["macdhist"] > 0) &
(dataframe["macdhist"].shift(1) <= 0), # histogram crossed above zero
dataframe["close"] > dataframe["ema_50"],
dataframe["close"] > dataframe["ema_200"], # confirm uptrend
dataframe[rsi] > 40,
dataframe[rsi] < 60,
dataframe["adx"] > 15,
dataframe["volume_ratio"] > 0.8, # volume confirmation
dataframe["volume"] > 0,
dataframe["btc_rsi_1h"] > 35,
dataframe["fng_value"] >= 25,
dataframe["fng_value"] <= 85,
]
dataframe.loc[
reduce(lambda x, y: x & y, conditions_macd),
["enter_long", "enter_tag"]
] = (1, "macd_reversal")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
rsi = f"rsi_{self.rsi_period.value}"
ema_fast = f"ema_{self.ema_fast.value}"
ema_slow = f"ema_{self.ema_slow.value}"
# ========== LONG EXITS ==========
# EXIT 1: RSI very overbought
dataframe.loc[
(dataframe[rsi] > self.rsi_exit.value) &
(dataframe["volume"] > 0),
["exit_long", "exit_tag"]
] = (1, "rsi_overbought")
# EXIT 2: Bearish EMA cross with MACD confirmation
dataframe.loc[
(dataframe[ema_fast] < dataframe[ema_slow]) &
(dataframe[ema_fast].shift(1) >= dataframe[ema_slow].shift(1)) &
(dataframe["macdhist"] < 0) &
(dataframe[rsi] > 50) &
(dataframe["volume"] > 0),
["exit_long", "exit_tag"]
] = (1, "ema_bearish_cross")
# EXIT 3: Price drops below EMA200 by 1%+ (trend broken, softened to avoid premature exits)
dataframe.loc[
(dataframe["close"] < dataframe["ema_200"] * 0.99) &
(dataframe["close"].shift(1) >= dataframe["ema_200"].shift(1)) &
(dataframe["volume"] > 0),
["exit_long", "exit_tag"]
] = (1, "trend_broken")
# EXIT 4: Trend early warning — RSI overbought reversal near EMA200
# Catches trend exhaustion before price breaks support, saving avg -3% vs trend_broken
dataframe.loc[
(dataframe["close"] < dataframe["ema_200"] * 0.995) & # within 0.5% of breaking
(dataframe[rsi] > 72) & # exhausted
(dataframe["macdhist"] < dataframe["macdhist"].shift(1)) & # momentum dropping
(dataframe["volume"] > 0),
["exit_long", "exit_tag"]
] = (1, "trend_early_warning")
return dataframe
# --- Improved Confidence Scoring (inline from trendrider_confidence) ---
def _calc_confidence(self, last: dict) -> tuple:
"""Calculate signal confidence based on weighted indicator alignment.
Max score ~17.5. Returns (level_str, bar_str, details_list, numeric_level).
"""
score = 0.0
details = []
rsi_key = f"rsi_{self.rsi_period.value}"
rsi_val = last.get(rsi_key, 50)
# RSI in healthy zone (not overbought): +1.5
if 35 < rsi_val < 60:
score += 1.5
details.append("RSI healthy")
# Strong trend (ADX): +2.5 strong, +1.5 moderate
adx_val = last.get('adx', 0)
if adx_val > 30:
score += 2.5
details.append("Strong trend")
elif adx_val > self.adx_threshold.value:
score += 1.5
details.append("Moderate trend")
# Volume confirmation: +2.5 high, +1.5 normal
vol_ratio = last.get('volume_ratio', 0)
if vol_ratio > 1.5:
score += 2.5
details.append("High volume")
elif vol_ratio > 1.0:
score += 1.5
details.append("Normal volume")
# MACD positive histogram: +1.5, bonus +0.5 if rising
macd_hist = last.get('macdhist', 0)
macd_hist_prev = last.get('macdhist_prev', 0)
if macd_hist > 0:
score += 1.5
if macd_hist > macd_hist_prev:
score += 0.5
details.append("MACD positive+rising")
else:
details.append("MACD positive")
# OBV rising AND above EMA: +1.5
if last.get('obv', 0) > last.get('obv_ema', 0):
score += 1.5
details.append("OBV rising")
# BTC healthy (RSI 40-70): +1.5
btc_rsi = last.get('btc_rsi_1h', 50)
if 40 < btc_rsi < 70:
score += 1.5
details.append("BTC healthy")
# 4h trend alignment AND ADX_4h > 20: +1.5
if last.get('is_bull_4h', 0) == 1 and last.get('adx_4h', 0) > 20:
score += 1.5
details.append("4H trend aligned")
# Bollinger Band position (close near lower = good for long): +1
close = last.get('close', 0)
bb_lower = last.get('bb_lower', 0)
bb_upper = last.get('bb_upper', 0)
bb_range = bb_upper - bb_lower if bb_upper > bb_lower else 1
if bb_lower > 0 and close > 0:
bb_position = (close - bb_lower) / bb_range
if bb_position < 0.35:
score += 1.0
details.append("Near BB lower")
# Plus_DI > Minus_DI spread > 10: +1
plus_di = last.get('plus_di', 0)
minus_di = last.get('minus_di', 0)
if plus_di - minus_di > 10:
score += 1.0
details.append("Strong DI spread")
# FNG bonus: neutral/healthy (40-60): +1
fng_val = last.get('fng_value', 50)
if 40 <= fng_val <= 60:
score += 1.0
details.append("FNG neutral")
# On-chain: healthy funding rate: +1
funding = last.get('funding_rate', 0)
if abs(funding) < 0.0001: # Normal funding
score += 1
details.append("Healthy funding")
# Smooth mapping to 1-10 (max score ~17.5)
numeric = max(1, min(10, round(score * 10 / 17.5)))
# Level label
if numeric >= 8:
level = "STRONG"
elif numeric >= 6:
level = "GOOD"
elif numeric >= 4:
level = "MEDIUM"
else:
level = "WEAK"
# Dynamic bar
bar = "|" * numeric + "-" * (10 - numeric) + f" {numeric}/10"
return level, bar, details, numeric
def _market_context(self, last: dict) -> str:
"""Generate market context string."""
btc_rsi = last.get('btc_rsi_1h', 50)
btc_bull = last.get('btc_is_bull_1h', 0)
bull_4h = last.get('is_bull_4h', 0)
if btc_bull and btc_rsi > 55:
btc_status = "Bullish"
elif btc_rsi > 40:
btc_status = "Neutral"
else:
btc_status = "Bearish"
tf_4h = "Uptrend" if bull_4h else "Downtrend"
parts = [f"BTC: {btc_status} (RSI {btc_rsi:.0f})", f"4H: {tf_4h}"]
return " | ".join(parts)
def _get_market_regime(self, last: dict) -> str:
"""Detect market regime from ADX + EMA200 + BB width."""
adx_val = last.get('adx', 0)
ema_200 = last.get('ema_200', 0)
close = last.get('close', 0)
is_bull = last.get('is_bull', 0)
bb_width = last.get('bb_width', 0)
bb_width_sma = last.get('bb_width_sma', 0)
high_vol = bb_width > bb_width_sma * 1.5 if bb_width_sma > 0 else False
if adx_val < 20:
return "Ranging (High Vol)" if high_vol else "Ranging"
elif is_bull and close > ema_200:
return "Trending Bull"
else:
return "Trending Bear (High Vol)" if high_vol else "Trending Bear"
def custom_exit(self, pair: str, trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs):
"""Cascading early exit — stop bleeding before 24h timeout.
Cascade catches losers earlier than the 24h hard timeout:
- 2h: cut if -1.5% (already broken thesis)
- 4h: cut if red (no recovery momentum)
- 8h: cut if not at +0.5% (dead trade)
- 16h: cut if not at +1% (final mercy)
"""
duration_hours = (current_time - trade.open_date_utc).total_seconds() / 3600
if duration_hours >= 2 and current_profit < -0.015:
return "early_loss_cut_2h"
if duration_hours >= 4 and current_profit < 0:
return "early_loss_cut_4h"
if duration_hours >= 8 and current_profit < 0.005:
return "early_loss_cut_8h"
if duration_hours >= 16 and current_profit < 0.01:
return "early_loss_cut_16h"
if duration_hours >= 24:
return "time_exit_24h"
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: str | None,
side: str, **kwargs) -> bool:
# Get current indicators for confidence filter
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) > 0:
last = dataframe.iloc[-1]
else:
last = {}
# Confidence & regime filter — reject weak signals
_, _, _, conf_numeric = self._calc_confidence(last)
regime = self._get_market_regime(last)
min_conf = 6 if "Bear" in regime else 5
if conf_numeric < min_conf:
logger.info(f"Rejecting signal for {pair}: confidence {conf_numeric}/10 < {min_conf} (regime: {regime})")
return False
return True
@@ -18,11 +18,11 @@ class DoesNothingStrategy(IStrategy):
# adjust based on market conditions. We would recommend to keep it low for quick turn arounds
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"0": 0.01
"0": 100000
}
# Optimal stoploss designed for the strategy
stoploss = -0.25
stoploss = -1
# Optimal timeframe for the strategy
timeframe = '5m'
+2 -2
View File
@@ -66,8 +66,8 @@ class Low_BB(IStrategy):
# dataframe['mfi'] = ta.MFI(dataframe)
# dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7)
# dataframe['canbuy'] = np.NaN
# dataframe['canbuy2'] = np.NaN
# dataframe['canbuy'] = np.nan
# dataframe['canbuy2'] = np.nan
# dataframe.loc[dataframe.close.rolling(49).min() <= 1.1 * dataframe.close, 'canbuy'] == 1
# dataframe.loc[dataframe.close.rolling(600).max() < 1.2 * dataframe.close, 'canbuy'] = 1
# dataframe.loc[dataframe.close.rolling(600).max() * 0.8 > dataframe.close, 'canbuy2'] = 1
@@ -142,7 +142,7 @@ class TDSequentialStrategy(IStrategy):
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy columnNA / NaN values
:return: DataFrame with buy columnNA / nan values
"""
dataframe["exit_long"] = 0
dataframe.loc[((dataframe['exceed_high']) |
@@ -53,9 +53,9 @@ class FAdxSmaStrategy(IStrategy):
pos_exit_adx = DecimalParameter(15, 40, decimals=1, default=30.0, space="sell")
# Define the parameter spaces
adx_period = IntParameter(4, 24, default=14)
sma_short_period = IntParameter(4, 24, default=12)
sma_long_period = IntParameter(12, 175, default=48)
adx_period = IntParameter(4, 24, default=14, space='buy')
sma_short_period = IntParameter(4, 24, default=12, space='buy')
sma_long_period = IntParameter(12, 175, default=48, space='buy')
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -238,7 +238,7 @@ class FSupertrendStrategy(IStrategy):
)
# Mark the trend direction up/down
df[stx] = np.where(
(df[st] > 0.00), np.where((df["close"] < df[st]), "down", "up"), np.NaN
(df[st] > 0.00), np.where((df["close"] < df[st]), "down", "up"), None
)
# Remove basic and final bands from the columns
+85 -105
View File
@@ -1,7 +1,6 @@
# all strategies are tested against this config. Tests only done on binance futures
```
{
"max_open_trades": -1,
"stake_currency": "USDT",
@@ -40,109 +39,89 @@
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"AUDIO/USDT",
"AAVE/USDT",
"ALICE/USDT",
"ARPA/USDT",
"AVAX/USDT",
"ATOM/USDT",
"ANKR/USDT",
"AXS/USDT",
"ADA/USDT",
"ALGO/USDT",
"BTS/USDT",
"BAND/USDT",
"BEL/USDT",
"BNB/USDT",
"BTC/USDT",
"BLZ/USDT",
"BAT/USDT",
"CHR/USDT",
"C98/USDT",
"COTI/USDT",
"CHZ/USDT",
"COMP/USDT",
"CRV/USDT",
"CELO/USDT",
"DUSK/USDT",
"DOGE/USDT",
"DENT/USDT",
"DASH/USDT",
"DOT/USDT",
"DYDX/USDT",
"ENJ/USDT",
"EOS/USDT",
"ETH/USDT",
"ETC/USDT",
"ENS/USDT",
"EGLD/USDT",
"FIL/USDT",
"FTM/USDT",
"FLM/USDT",
"GRT/USDT",
"GALA/USDT",
"HBAR/USDT",
"HOT/USDT",
"IOTX/USDT",
"ICX/USDT",
"ICP/USDT",
"IOTA/USDT",
"IOST/USDT",
"KLAY/USDT",
"KAVA/USDT",
"KNC/USDT",
"KSM/USDT",
"LUNA/USDT",
"LRC/USDT",
"LINA/USDT",
"LTC/USDT",
"LINK/USDT",
"MATIC/USDT",
"NEAR/USDT",
"MANA/USDT",
"MTL/USDT",
"NEO/USDT",
"ONT/USDT",
"OMG/USDT",
"OCEAN/USDT",
"OGN/USDT",
"ONE/USDT",
"PEOPLE/USDT",
"RLC/USDT",
"RUNE/USDT",
"RVN/USDT",
"RSR/USDT",
"REEF/USDT",
"ROSE/USDT",
"SNX/USDT",
"SAND/USDT",
"SOL/USDT",
"SUSHI/USDT",
"SRM/USDT",
"SKL/USDT",
"SXP/USDT",
"STORJ/USDT",
"TRX/USDT",
"TOMO/USDT",
"TRB/USDT",
"TLM/USDT",
"THETA/USDT",
"UNI/USDT",
"UNFI/USDT",
"VET/USDT",
"YFI/USDT",
"ZIL/USDT",
"ZEN/USDT",
"ZRX/USDT",
"ZEC/USDT",
"WAVES/USDT",
"XRP/USDT",
"XLM/USDT",
"XTZ/USDT",
"XMR/USDT",
"XEM/USDT",
"QTUM/USDT",
"1INCH/USDT"
"AAVE/USDT:USDT",
"ALICE/USDT:USDT",
"ARPA/USDT:USDT",
"AVAX/USDT:USDT",
"ATOM/USDT:USDT",
"ANKR/USDT:USDT",
"AXS/USDT:USDT",
"ADA/USDT:USDT",
"ALGO/USDT:USDT",
"BAND/USDT:USDT",
"BEL/USDT:USDT",
"BTC/USDT:USDT",
"BAT/USDT:USDT",
"CHR/USDT:USDT",
"C98/USDT:USDT",
"COTI/USDT:USDT",
"CHZ/USDT:USDT",
"COMP/USDT:USDT",
"CRV/USDT:USDT",
"CELO/USDT:USDT",
"DUSK/USDT:USDT",
"DOGE/USDT:USDT",
"DENT/USDT:USDT",
"DASH/USDT:USDT",
"DOT/USDT:USDT",
"DYDX/USDT:USDT",
"ENJ/USDT:USDT",
"ETH/USDT:USDT",
"ETC/USDT:USDT",
"ENS/USDT:USDT",
"EGLD/USDT:USDT",
"FIL/USDT:USDT",
"GRT/USDT:USDT",
"GALA/USDT:USDT",
"HBAR/USDT:USDT",
"HOT/USDT:USDT",
"IOTX/USDT:USDT",
"ICX/USDT:USDT",
"ICP/USDT:USDT",
"IOTA/USDT:USDT",
"IOST/USDT:USDT",
"KAVA/USDT:USDT",
"KNC/USDT:USDT",
"KSM/USDT:USDT",
"LRC/USDT:USDT",
"LTC/USDT:USDT",
"LINK/USDT:USDT",
"NEAR/USDT:USDT",
"MANA/USDT:USDT",
"MTL/USDT:USDT",
"NEO/USDT:USDT",
"ONT/USDT:USDT",
"OGN/USDT:USDT",
"ONE/USDT:USDT",
"PEOPLE/USDT:USDT",
"RLC/USDT:USDT",
"RUNE/USDT:USDT",
"RVN/USDT:USDT",
"RSR/USDT:USDT",
"ROSE/USDT:USDT",
"SNX/USDT:USDT",
"SAND/USDT:USDT",
"SOL/USDT:USDT",
"SUSHI/USDT:USDT",
"SKL/USDT:USDT",
"STORJ/USDT:USDT",
"TRX/USDT:USDT",
"TRB/USDT:USDT",
"TLM/USDT:USDT",
"THETA/USDT:USDT",
"UNI/USDT:USDT",
"VET/USDT:USDT",
"YFI/USDT:USDT",
"ZIL/USDT:USDT",
"ZEN/USDT:USDT",
"ZRX/USDT:USDT",
"ZEC/USDT:USDT",
"XRP/USDT:USDT",
"XLM/USDT:USDT",
"XTZ/USDT:USDT",
"XMR/USDT:USDT",
"QTUM/USDT:USDT",
"1INCH/USDT:USDT"
],
"pair_blacklist": ["BNB/.*"]
},
@@ -162,7 +141,7 @@
"remove_pumps": false
},
"telegram": {
"enabled": true,
"enabled": false,
"token": "",
"chat_id": ""
},
@@ -185,4 +164,5 @@
}
}
```