Timeframe
5m
Direction
Long & Short
Stoploss
-18.0%
Trailing Stop
No
ROI
0m: 7.0%
Interface Version
3
Startup Candles
20
Indicators
6
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
binance_shorts - Short-only variant of binance.py (E0V1E long strategy for Binance)
Mirrors the successful long-only binance.py strategy with inverted logic for short positions.
Designed to profit in bear markets and overbought conditions.
Entry Conditions:
1. short_1: RSI/SMA/CTI mean-reversion shorts (inverted from long buy_1)
(ewo_short removed: net loser in BT1 -2855 and BT2 -3167 USDT)
Exit Layers:
1. Base stoploss: -0.18 hard cap (6% price move at 3x leverage)
2. ROI: 7% target (faster profit-taking than long's ~disabled ROI)
3. custom_stoploss: Indicator-based trailing (fastk < cover_fastx when oversold)
4. custom_exit: 48h protection → unclog/zombie/deadfish detection
Parameter defaults sourced from E0V1E_Shorts.py (live-tested on Bybit futures, 3x leverage).
"""
from datetime import datetime, timedelta
import logging
from typing import Optional, Union
import freqtrade.vendor.qtpylib.indicators as qtpylib
import talib.abstract as ta
import pandas_ta as pta
from freqtrade.persistence import Trade
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IntParameter
logger = logging.getLogger(__name__)
def ewo(dataframe, ema_length=5, ema2_length=35):
df = dataframe.copy()
ema1 = ta.EMA(df, timeperiod=ema_length)
ema2 = ta.EMA(df, timeperiod=ema2_length)
emadif = (ema1 - ema2) / df["low"] * 100
return emadif
class binance_shorts(IStrategy):
INTERFACE_VERSION = 3
can_short = True
# Tighter ROI for shorts - proven in E0V1E_Shorts live runs
minimal_roi = {"0": 0.07} # 7% (vs essentially disabled for longs)
timeframe = "5m"
process_only_new_candles = True
startup_candle_count = 20
order_types = {
"entry": "market",
"exit": "market",
"emergency_exit": "market",
"force_entry": "market",
"force_exit": "market",
"stoploss": "market",
"stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60,
"stoploss_on_exchange_market_ratio": 0.99,
}
# Hard cap at -18% loss (6% price move at 3x leverage)
# Prevents catastrophic losses from pump tokens (PROMPT -99%, Q -84%)
stoploss = -0.18
use_custom_stoploss = True
# Max simultaneous short positions
max_short_trades = 4
# --- Short entry parameters (inverted from binance.py longs) ---
# Defaults from E0V1E_Shorts.py (live-tested)
# ewo_short params removed: net loser in backtests (BT1: -2855, BT2: -3167 USDT)
is_optimize_32 = True
sell_rsi_fast_32 = IntParameter(50, 80, default=54, space="buy", optimize=is_optimize_32)
sell_rsi_32 = IntParameter(50, 85, default=81, space="buy", optimize=is_optimize_32)
sell_sma15_32 = DecimalParameter(1.0, 1.1, default=1.058, decimals=3, space="buy", optimize=is_optimize_32)
sell_cti_32 = DecimalParameter(0, 1, default=0.86, decimals=2, space="buy", optimize=is_optimize_32)
# --- Short exit parameters (cover = close short position) ---
is_optimize_deadfish = True
cover_deadfish_bb_width = DecimalParameter(0.03, 0.75, default=0.05, space="sell", optimize=is_optimize_deadfish)
cover_deadfish_profit = DecimalParameter(0.05, 0.15, default=0.05, space="sell", optimize=is_optimize_deadfish)
cover_deadfish_bb_factor = DecimalParameter(0.80, 1.10, default=1.0, space="sell", optimize=is_optimize_deadfish)
cover_deadfish_volume_factor = DecimalParameter(1, 2.5, default=1.0, space="sell", optimize=is_optimize_deadfish)
cover_fastx = IntParameter(0, 50, default=25, space="sell", optimize=True)
def confirm_trade_entry(
self,
pair: str,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
current_time: datetime,
entry_tag: Optional[str],
side: str,
**kwargs,
) -> bool:
# Only allow shorts
if side == "long":
return False
# Enforce max short position limit
short_count = 0
trades = Trade.get_trades_proxy(is_open=True)
for trade in trades:
if trade.is_short:
short_count += 1
if short_count >= self.max_short_trades:
return False
return True
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# RSI family
dataframe["sma_15"] = ta.SMA(dataframe, timeperiod=15)
dataframe["cti"] = pta.cti(dataframe["close"], length=20)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["rsi_fast"] = ta.RSI(dataframe, timeperiod=4)
dataframe["rsi_slow"] = ta.RSI(dataframe, timeperiod=20)
# EMA / EWO
dataframe["ema_8"] = ta.EMA(dataframe, timeperiod=8)
dataframe["ema_16"] = ta.EMA(dataframe, timeperiod=16)
dataframe["EWO"] = ewo(dataframe, 50, 200)
# Stochastic
stoch_fast = ta.STOCHF(dataframe, 5, 3, 0, 3, 0)
dataframe["fastd"] = stoch_fast["fastd"]
dataframe["fastk"] = stoch_fast["fastk"]
# Bollinger Bands
bollinger2 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe["bb_lowerband2"] = bollinger2["lower"]
dataframe["bb_middleband2"] = bollinger2["mid"]
dataframe["bb_upperband2"] = bollinger2["upper"]
dataframe["bb_width"] = (dataframe["bb_upperband2"] - dataframe["bb_lowerband2"]) / dataframe["bb_middleband2"]
# Volume
dataframe["volume_mean_12"] = dataframe["volume"].rolling(12).mean().shift(1)
dataframe["volume_mean_24"] = dataframe["volume"].rolling(24).mean().shift(1)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, "enter_tag"] = ""
# --- short_1: Inverted RSI/SMA/CTI mean-reversion ---
# Long buy_1: rsi_slow declining, rsi_fast < X, rsi > X, close < sma_15 * X, cti < X
# Short short_1: rsi_slow rising, rsi_fast > X, rsi < X, close > sma_15 * X, cti > X
short_1 = (
(dataframe["rsi_slow"] > dataframe["rsi_slow"].shift(1))
& (dataframe["rsi_fast"] > self.sell_rsi_fast_32.value)
& (dataframe["rsi"] < self.sell_rsi_32.value)
& (dataframe["close"] > dataframe["sma_15"] * self.sell_sma15_32.value)
& (dataframe["cti"] > self.sell_cti_32.value)
)
dataframe.loc[short_1, "enter_tag"] = "short_1"
dataframe.loc[short_1, "enter_short"] = 1
return dataframe
def custom_stoploss(
self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs
) -> float:
"""
Indicator-based trailing stoploss for shorts.
Base stoploss (-0.18) handles catastrophic loss prevention.
This method only tightens the stop for profitable/recoverable trades.
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
# After 60 min: tight stop if oversold (fastk < cover_fastx) and small loss
if current_time - timedelta(minutes=60) > trade.open_date_utc:
if (current_candle["fastk"] < self.cover_fastx.value) and (current_profit > -0.01):
return -0.001
# After 1 day: tight stop if oversold even with moderate loss
if current_time - timedelta(days=1) > trade.open_date_utc:
if (current_candle["fastk"] < self.cover_fastx.value) and (current_profit > -0.05):
return -0.001
# Profitable shorts: exit when oversold (price bottom)
if current_profit > 0:
if current_candle["fastk"] < self.cover_fastx.value:
return -0.001
# Keep base stoploss (-0.18)
return 1.0
def custom_exit(
self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs
) -> Optional[Union[str, bool]]:
"""
3-layer exit for shorts (proven in E0V1E_Shorts):
- Hours 0-48: No forced exits, let position develop
- After 48h: unclog (>4% loss), zombie (breakeven), deadfish (low vol)
"""
trade_duration_hours = (current_time - trade.open_date_utc).total_seconds() / 3600
# Phase 1: First 48 hours - no forced exits
if trade_duration_hours < 48:
return None
# Phase 2: After 48 hours
# Unclog: force exit if losing > 3%
if current_profit < -0.03:
return "unclog"
# Zombie: force exit if stuck at breakeven
if -0.005 <= current_profit <= 0.005:
return "zombie"
# Deadfish: low volatility dead trade (inverted BB logic for shorts)
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
if (
(current_profit < self.cover_deadfish_profit.value)
and (current_candle["bb_width"] < self.cover_deadfish_bb_width.value)
and (current_candle["close"] < current_candle["bb_middleband2"] * self.cover_deadfish_bb_factor.value)
and (
current_candle["volume_mean_12"]
< current_candle["volume_mean_24"] * self.cover_deadfish_volume_factor.value
)
):
return "cover_stoploss_deadfish"
return None
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, ["exit_short", "exit_tag"]] = (0, "short_out")
return dataframe
def leverage(
self,
pair: str,
current_time: datetime,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
side: str,
**kwargs,
) -> float:
return 3.0