Timeframe
1h
Direction
Long Only
Stoploss
-2.5%
Trailing Stop
Yes
ROI
0m: 1000.0%
Interface Version
3
Startup Candles
250
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
from datetime import timedelta
from typing import Optional
import pandas as pd
from pandas import DataFrame
import talib.abstract as ta
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative
class SpotMTFMomentumStrategy(IStrategy):
INTERFACE_VERSION = 3
can_short = False
timeframe = "1h"
process_only_new_candles = True
startup_candle_count = 250
minimal_roi = {"0": 10.0}
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
@property
def protections(self):
return [
{
"method": "MaxDrawdown",
"lookback_period_candles": 200,
"trade_limit": 20,
"stop_duration_candles": 48,
"max_allowed_drawdown": 0.10,
},
{
"method": "CooldownPeriod",
"stop_duration_candles": 2,
},
]
@staticmethod
def _crossed_above(series_a: pd.Series, series_b: pd.Series) -> pd.Series:
return (series_a > series_b) & (series_a.shift(1) <= series_b.shift(1))
@staticmethod
def _crossed_below(series_a: pd.Series, series_b: pd.Series) -> pd.Series:
return (series_a < series_b) & (series_a.shift(1) >= series_b.shift(1))
@informative("4h")
def populate_indicators_4h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20)
dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
dataframe["atr_ratio"] = dataframe["atr"] / dataframe["close"]
dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean()
dataframe["recent_high"] = dataframe["high"].shift(1).rolling(12).max()
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
long_signal = (
self._crossed_above(dataframe["ema_20"], dataframe["ema_50"])
& (dataframe["close_4h"] > dataframe["ema_50_4h"])
& (dataframe["ema_50_4h"] > dataframe["ema_50_4h"].shift(3))
& (dataframe["close"] > dataframe["ema_200"])
& (dataframe["ema_50"] > dataframe["ema_200"])
& dataframe["rsi"].between(52, 65)
& (dataframe["adx"] > 20)
& dataframe["atr_ratio"].between(0.005, 0.04)
& (dataframe["volume"] > dataframe["volume_mean"])
& (dataframe["close"] > dataframe["recent_high"])
& (dataframe["volume"] > 0)
)
dataframe.loc[long_signal, ["enter_long", "enter_tag"]] = (1, "mtf_momentum_long")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
exit_signal = (
(
self._crossed_below(dataframe["ema_20"], dataframe["ema_50"])
| (dataframe["close_4h"] < dataframe["ema_50_4h"])
)
& (dataframe["volume"] > 0)
)
dataframe.loc[exit_signal, "exit_long"] = 1
return dataframe
def custom_exit(
self,
pair: str,
trade: Trade,
current_time,
current_rate: float,
current_profit: float,
**kwargs,
) -> Optional[str]:
if trade.open_date_utc and current_time - trade.open_date_utc >= timedelta(hours=48):
return "time_stop_48h"
return None
def custom_stake_amount(
self,
pair: str,
current_time,
current_rate: float,
proposed_stake: float,
min_stake: float | None,
max_stake: float,
leverage: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
closed_trades = [
trade
for trade in Trade.get_trades_proxy(is_open=False)
if trade.close_date_utc and trade.close_date_utc <= current_time
]
closed_trades.sort(key=lambda trade: trade.close_date_utc, reverse=True)
kelly_fraction = 0.50
sample = closed_trades[:20]
if len(sample) >= 10:
wins = [trade.close_profit for trade in sample if trade.close_profit and trade.close_profit > 0]
losses = [abs(trade.close_profit) for trade in sample if trade.close_profit and trade.close_profit < 0]
if wins and losses:
win_rate = len(wins) / len(sample)
avg_win = sum(wins) / len(wins)
avg_loss = sum(losses) / len(losses)
payoff = avg_win / avg_loss if avg_loss else 0.0
edge = win_rate - ((1 - win_rate) / payoff) if payoff > 0 else 0.0
kelly_fraction = min(0.75, max(0.25, edge * 0.5))
last_three = sample[:3]
loss_streak = len(last_three) == 3 and all((trade.close_profit or 0) < 0 for trade in last_three)
risk_cap_stake = min(max_stake, proposed_stake * 4.0)
stake = min(proposed_stake * kelly_fraction, risk_cap_stake, max_stake)
if loss_streak:
stake *= 0.5
if min_stake is not None:
stake = max(stake, min_stake)
return min(stake, max_stake)