Timeframe
5m
Direction
Long & Short
Stoploss
-0.8%
Trailing Stop
Yes
ROI
0m: 1.0%, 10m: 0.6%, 25m: 0.3%, 45m: 0.0%
Interface Version
3
Startup Candles
240
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
```python
"""
ROXLab Freqtrade Strategy
Strategy Name : RoxScalpJune2026
Author : Kenshin Himura @ roxlab.org
Project : ROXLab Freqtrade Research
Market Type : Futures / Perpetual
Primary Mode : Dry-run first, backtest required before live trading
Timeframe : 5m
Style : Defensive EMA pullback scalping with long/short support
Description
-----------
RoxScalpJune2026 is a defensive futures scalping strategy designed for
volatile and risk-off market conditions. The strategy avoids blind dip buying,
martingale, and unlimited DCA. It trades with the dominant short-term trend,
uses EMA200 as the main regime filter, and looks for pullback continuation
around EMA20 and EMA50.
Core Logic
----------
- Long entries are allowed only when price is above EMA200.
- Short entries are allowed only when price is below EMA200.
- Entry requires pullback reclaim/rejection near EMA20.
- RSI direction must confirm momentum.
- Volume must expand above its rolling average.
- ATR percentage is used to avoid dead markets and excessively chaotic candles.
- Exit is handled through ROI, trailing stop, and momentum fade signals.
- Position adjustment is disabled.
- Leverage defaults to 1x.
Plus
----
- Supports both long and short trading in futures mode.
- Uses trend filter to avoid fighting the dominant market direction.
- Avoids martingale and uncontrolled averaging.
- Uses volume confirmation to reduce weak entries.
- Uses ATR-based volatility filtering to avoid low-quality market states.
- Uses tight stoploss and fast trailing logic, suitable for scalping tests.
- Clean and readable indicator structure, easy to audit and modify.
- Suitable for dry-run and controlled backtesting before live deployment.
Minus
-----
- It can miss strong reversal moves because it prefers trend continuation.
- It may generate few trades during sideways or low-volume market conditions.
- It can be whipsawed when market rapidly crosses EMA levels.
- It does not use higher-timeframe confirmation yet.
- It does not include dynamic pair selection yet.
- It does not include protection handlers inside the strategy file.
- It is not optimized by hyperopt yet.
- It should not be used live without backtesting and dry-run validation.
Development Plan
----------------
1. Add 15m or 1h informative timeframe confirmation.
2. Add market regime classification: trend, range, high-volatility chop.
3. Add pair-specific volatility tuning.
4. Add optional ADX or slope filter to reduce chop entries.
5. Add custom stoploss based on ATR instead of fixed percentage only.
6. Add custom exit logic for faster loss cutting during momentum failure.
7. Add protection presets for cooldown, stoploss guard, and drawdown control.
8. Add hyperoptable parameters for EMA, RSI, ATR, ROI, and trailing values.
9. Add performance report workflow for weekly dry-run evaluation.
10. Create separate aggressive, balanced, and defensive profiles.
Risk Notice
-----------
This strategy is research code. It does not guarantee profit. Market conditions,
exchange liquidity, spread, funding, slippage, and execution delay can materially
affect performance. Always backtest, dry-run, and start with small exposure.
"""
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import pandas as pd
class RoxScalpJune2026(IStrategy):
"""
RoxScalpJune2026
Defensive futures scalping strategy for risk-off / volatile market conditions.
Core idea:
- Trade with trend, not against it.
- Long only above EMA200.
- Short only below EMA200.
- Entry on pullback recovery/rejection around EMA20.
- Require RSI confirmation and volume expansion.
- Fast exit, tight stop, no DCA, no martingale.
Designed for dry-run and backtesting first.
"""
INTERFACE_VERSION = 3
timeframe = "5m"
startup_candle_count = 240
can_short = True
# Conservative risk profile.
stoploss = -0.008
minimal_roi = {
"0": 0.010,
"10": 0.006,
"25": 0.003,
"45": 0.000,
}
trailing_stop = True
trailing_stop_positive = 0.004
trailing_stop_positive_offset = 0.008
trailing_only_offset_is_reached = True
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
position_adjustment_enable = False
def leverage(
self,
pair: str,
current_time,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
"""
Start with low leverage. Increase only after real backtest and dry-run evidence.
"""
return min(1.0, max_leverage)
@staticmethod
def _rsi(series: pd.Series, period: int = 14) -> pd.Series:
delta = series.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, 1e-10)
return 100 - (100 / (1 + rs))
@staticmethod
def _atr(dataframe: DataFrame, period: int = 14) -> pd.Series:
high = dataframe["high"]
low = dataframe["low"]
close = dataframe["close"]
prev_close = close.shift(1)
true_range = pd.concat(
[
high - low,
(high - prev_close).abs(),
(low - prev_close).abs(),
],
axis=1,
).max(axis=1)
return true_range.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema20"] = dataframe["close"].ewm(span=20, adjust=False).mean()
dataframe["ema50"] = dataframe["close"].ewm(span=50, adjust=False).mean()
dataframe["ema200"] = dataframe["close"].ewm(span=200, adjust=False).mean()
dataframe["rsi"] = self._rsi(dataframe["close"], 14)
dataframe["volume_sma20"] = dataframe["volume"].rolling(20).mean()
dataframe["atr"] = self._atr(dataframe, 14)
dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
dataframe["ema_spread"] = (
dataframe["ema50"] - dataframe["ema200"]
) / dataframe["close"]
dataframe["green_candle"] = dataframe["close"] > dataframe["open"]
dataframe["red_candle"] = dataframe["close"] < dataframe["open"]
dataframe["rsi_rising"] = dataframe["rsi"] > dataframe["rsi"].shift(1)
dataframe["rsi_falling"] = dataframe["rsi"] < dataframe["rsi"].shift(1)
dataframe["volume_expansion"] = dataframe["volume"] > dataframe["volume_sma20"] * 1.05
dataframe["valid_volatility"] = (
(dataframe["atr_pct"] > 0.0010)
& (dataframe["atr_pct"] < 0.0250)
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, "enter_long"] = 0
dataframe.loc[:, "enter_short"] = 0
dataframe.loc[:, "enter_tag"] = None
long_trend = (
(dataframe["close"] > dataframe["ema200"])
& (dataframe["ema50"] > dataframe["ema200"])
& (dataframe["ema_spread"] > 0)
)
long_pullback_reclaim = (
(dataframe["close"].shift(1) < dataframe["ema20"].shift(1))
& (dataframe["close"] > dataframe["ema20"])
& (dataframe["close"] > dataframe["ema50"])
)
long_momentum = (
(dataframe["rsi"] > 45)
& (dataframe["rsi"] < 68)
& dataframe["rsi_rising"]
& dataframe["green_candle"]
)
dataframe.loc[
(
long_trend
& long_pullback_reclaim
& long_momentum
& dataframe["volume_expansion"]
& dataframe["valid_volatility"]
),
["enter_long", "enter_tag"],
] = (1, "long_ema_pullback_reclaim")
short_trend = (
(dataframe["close"] < dataframe["ema200"])
& (dataframe["ema50"] < dataframe["ema200"])
& (dataframe["ema_spread"] < 0)
)
short_pullback_reject = (
(dataframe["close"].shift(1) > dataframe["ema20"].shift(1))
& (dataframe["close"] < dataframe["ema20"])
& (dataframe["close"] < dataframe["ema50"])
)
short_momentum = (
(dataframe["rsi"] < 55)
& (dataframe["rsi"] > 32)
& dataframe["rsi_falling"]
& dataframe["red_candle"]
)
dataframe.loc[
(
short_trend
& short_pullback_reject
& short_momentum
& dataframe["volume_expansion"]
& dataframe["valid_volatility"]
),
["enter_short", "enter_tag"],
] = (1, "short_ema_pullback_reject")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, "exit_long"] = 0
dataframe.loc[:, "exit_short"] = 0
dataframe.loc[:, "exit_tag"] = None
dataframe.loc[
(
(dataframe["close"] < dataframe["ema50"])
| (
(dataframe["rsi"] > 74)
& (dataframe["close"] < dataframe["close"].shift(1))
)
),
["exit_long", "exit_tag"],
] = (1, "long_momentum_fade")
dataframe.loc[
(
(dataframe["close"] > dataframe["ema50"])
| (
(dataframe["rsi"] < 26)
& (dataframe["close"] > dataframe["close"].shift(1))
)
),
["exit_short", "exit_tag"],
] = (1, "short_momentum_fade")
return dataframe
```