Full V3-style strategy adapted from NostalgiaForInfinityX7.
Timeframe
5m
Direction
Long Only
Stoploss
-35.0%
Trailing Stop
No
ROI
0m: 4.0%
Interface Version
N/A
Startup Candles
200
Indicators
9
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# Full reproduction (approximate, well-documented) of NostalgiaForInfinityX7's System V3 rebuy logic
# "NostalgiaV3_Full.py"
# - Purpose: deliver a near-complete, production-style implementation of the V3 rebuy system
# adapted to Freqtrade strategy interface for futures (3x).
# - Notes: This is an interpretation and re-implementation based on the provided
# NostalgiaForInfinityX7.py. It preserves the original's structure: protections,
# multi-step rebuy thresholds, derisk layers, multi-target TP, and entry/exit interplay.
# - IMPORTANT: Before running in production, TEST THOROUGHLY in backtest / dry_run.
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
from typing import Optional, Tuple, List
from datetime import datetime, timedelta
import talib as ta
import pandas as pd
import numpy as np
from freqtrade.persistence import Trade
# Helper dataclass-like container isn't required; using plain structures for portability
class NostalgiaForInfinityX7 (IStrategy):
"""Full V3-style strategy adapted from NostalgiaForInfinityX7.
Features implemented:
- Extensive indicator set used by original: RSI, StochRSI, Aroon, ROC, EMAs, SMA, BBands
- Entry conditions approximating original modes (Normal / Rapid / TopCoins removed for brevity)
- System V3 rebuy: multi-step rebuy with configurable stakes & thresholds
- Rebuy behavior: first N_fast steps execute on threshold; later steps require trend confirmation
- De-risk (derisk) multi-level partial sells
- Multi-target take-profit (split exits)
- Config-driven overrides via config['nfi_parameters']
This file is intentionally verbose and commented to aid study and modification.
"""
# Strategy metadata
timeframe = "5m"
minimal_roi = {"0": 0.04}
stoploss = -0.35 # default stoploss (strategy uses internal derisking + multi-target TP)
trailing_stop = False
process_only_new_candles = True
# default indicator parameters
rsi_short = 3
rsi_long = 14
ema_fast = 9
ema_slow = 26
sma_mid = 16
# default v3 settings (can be overridden by config['nfi_parameters'])
DEFAULT_REBUY_STAKES = [1.0, 1.0, 1.0, 1.0, 1.0]
DEFAULT_REBUY_THRESHOLDS = [-0.04, -0.06, -0.08, -0.10, -0.14]
DEFAULT_DERISK = -0.60
DEFAULT_MAX_REBUYS = 5
DEFAULT_REBUY_COOLDOWN = 0 # minutes between rebuy checks per step (0 = no cooldown)
# multi-target take profit structure (fractions of position)
DEFAULT_TP_TARGETS = [0.25, 0.25, 0.5] # fractional exits summing to 1.0
DEFAULT_TP_PCT = [0.02, 0.05, 0.12] # price targets relative to average entry
startup_candle_count = 200
############################################################################
# Config helpers
############################################################################
def _get_cfg(self, key, default):
try:
return self.config.get("nfi_parameters", {}).get(key, default)
except Exception:
return default
def _load_rebuy_settings(self):
stakes = self._get_cfg("system_v3_rebuy_mode_stakes_futures", self.DEFAULT_REBUY_STAKES)
thresholds = self._get_cfg("system_v3_rebuy_mode_thresholds_futures", self.DEFAULT_REBUY_THRESHOLDS)
derisk = self._get_cfg("rebuy_mode_derisk_futures", self.DEFAULT_DERISK)
max_rebuys = self._get_cfg("system_v3_max_rebuys", self.DEFAULT_MAX_REBUYS)
cooldown = self._get_cfg("system_v3_rebuy_cooldown_minutes", self.DEFAULT_REBUY_COOLDOWN)
# normalize lengths
if len(thresholds) < len(stakes):
thresholds = thresholds + [thresholds[-1]] * (len(stakes) - len(thresholds))
if len(stakes) < len(thresholds):
stakes = stakes + [stakes[-1]] * (len(thresholds) - len(stakes))
return stakes, thresholds, derisk, max_rebuys, cooldown
############################################################################
# Indicators
############################################################################
def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame:
df = df.copy()
# RSI short
df[f"RSI_{self.rsi_short}"] = ta.RSI(df["close"].values, timeperiod=self.rsi_short)
# Build shifted approximations for higher timeframes (5m base)
df[f"RSI_{self.rsi_short}_15m"] = df[f"RSI_{self.rsi_short}"].shift(3)
df[f"RSI_{self.rsi_short}_1h"] = df[f"RSI_{self.rsi_short}"].shift(12)
df[f"RSI_{self.rsi_short}_4h"] = df[f"RSI_{self.rsi_short}"].shift(48)
df[f"RSI_{self.rsi_long}"] = ta.RSI(df["close"].values, timeperiod=self.rsi_long)
# EMAs / SMA
df[f"EMA_{self.ema_fast}"] = ta.EMA(df["close"].values, timeperiod=self.ema_fast)
df[f"EMA_{self.ema_slow}"] = ta.EMA(df["close"].values, timeperiod=self.ema_slow)
df[f"SMA_{self.sma_mid}"] = ta.SMA(df["close"].values, timeperiod=self.sma_mid)
# StochRSI (approx): use STOCH on RSI_long
rsi = df[f"RSI_{self.rsi_long}"]
try:
stochk, stochd = ta.STOCH(rsi.fillna(50).values, rsi.fillna(50).values, rsi.fillna(50).values, 14, 3, 0, 3, 0)
except Exception:
stochk = pd.Series([50] * len(df))
stochd = pd.Series([50] * len(df))
df["STOCHRSIk_14_14_3_3"] = stochk
df["STOCHRSId_14_14_3_3"] = stochd
# Aroon oscillator approximation
try:
df["AROONOSC_14"] = ta.AROONOSC(df["high"].values, df["low"].values, timeperiod=14)
except Exception:
df["AROONOSC_14"] = 0
# ROC9 and MACD for momentum
df["ROC_9"] = ta.ROC(df["close"].values, timeperiod=9)
macd, macdsig, macdhist = ta.MACD(df["close"].values, fastperiod=12, slowperiod=26, signalperiod=9)
df["MACD"] = macd
df["MACD_SIGNAL"] = macdsig
# Bollinger bands
try:
upper, middle, lower = ta.BBANDS(df["close"].values, timeperiod=20, nbdevup=2, nbdevdn=2)
df["BBU_20_2.0"] = upper
df["BBM_20_2.0"] = middle
df["BBL_20_2.0"] = lower
except Exception:
df["BBU_20_2.0"] = df["close"]
df["BBL_20_2.0"] = df["close"]
# Helpers used by original strategy
df["close_min_48"] = df["close"].rolling(48).min()
df["close_max_48"] = df["close"].rolling(48).max()
df["num_empty_288"] = df["volume"].isna().astype(int).rolling(288, min_periods=1).sum()
return df
############################################################################
# Entry
############################################################################
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
df = df.copy()
df["enter_long"] = False
df["enter_short"] = False
df["enter_tag"] = ""
# A simplified long condition inspired by original (condition #4)
long_cond = (
(df["num_empty_288"] <= 60)
& (df[f"RSI_{self.rsi_short}_15m"] > 3.0)
& ((df[f"RSI_{self.rsi_short}"] > 3.0) | (df[f"RSI_{self.rsi_short}_1h"] > 5.0) | (df[f"RSI_{self.rsi_short}_4h"] > 10.0))
& ((df["STOCHRSIk_14_14_3_3"] < 50.0) | (df[f"RSI_{self.rsi_short}_15m"] > 10.0))
& (df["close"] < (df[f"SMA_{self.sma_mid}"] * 0.985))
)
df.loc[long_cond, "enter_long"] = True
df.loc[long_cond, "enter_tag"] = "nfi_long"
# Short condition inspired by original (condition #501)
short_cond = (
(df["num_empty_288"] <= 60)
& (df[f"RSI_{self.rsi_short}_1h"] >= 5.0)
& (df[f"RSI_{self.rsi_short}_4h"] >= 20.0)
& (df[f"EMA_{self.ema_fast}"] > (df[f"EMA_{self.ema_slow}"] * 1.04))
& (df["close"] > (df[f"EMA_{self.ema_fast}"] * 1.03))
& (df["AROONOSC_14"] > 0)
)
df.loc[short_cond, "enter_short"] = True
df.loc[short_cond, "enter_tag"] = "nfi_short"
return df
############################################################################
# Profit calculation helper
############################################################################
def calc_total_profit(self, trade: Trade, filled_entries, filled_exits, exit_rate: float) -> Tuple[float, float]:
"""Return (profit_stake, profit_ratio) approximations based on filled entries."""
total_cost = 0.0
total_amount = 0.0
for e in filled_entries:
total_cost += e.safe_filled * e.safe_price
total_amount += e.safe_filled
if total_amount <= 0:
return 0.0, 0.0
current_value = total_amount * exit_rate
profit_stake = current_value - total_cost
profit_ratio = profit_stake / total_cost if total_cost > 0 else 0.0
return profit_stake, profit_ratio
############################################################################
# Rebuy V3 implementation (full behavior)
############################################################################
def long_rebuy_adjust_trade_position_v3(
self,
trade: Trade,
enter_tags,
current_time: datetime,
current_rate: float,
current_profit: float,
min_stake: Optional[float],
max_stake: float,
current_entry_rate: float,
current_exit_rate: float,
current_entry_profit: float,
current_exit_profit: float,
df: Optional[DataFrame] = None,
**kwargs,
) -> Optional[float]:
# Load rebuy settings from config (or use defaults)
rebuy_stakes, rebuy_thresholds, derisk_cfg, max_rebuys, cooldown_minutes = self._load_rebuy_settings()
# normalize stake values (consider leverage in trade object if present)
leverage = max(1.0, getattr(trade, 'leverage', 1.0))
min_stake_val = (min_stake or 0.0) / leverage
max_stake_val = max_stake / leverage
# Do not execute if there are pending orders
if trade.has_open_orders:
return None
filled_entries = trade.select_filled_orders(trade.entry_side)
filled_exits = trade.select_filled_orders(trade.exit_side)
if len(filled_entries) == 0:
return None
# amount of the first slice (initial entry) - used to scale rebuy amounts
slice_amount = getattr(filled_entries[0], 'cost', 0.0) if len(filled_entries) else 0.0
# Profit vs last entry, used to compare with thresholds
last_entry_price = filled_entries[-1].safe_price
slice_profit_entry = (current_rate - last_entry_price) / last_entry_price
# If we've already rebought too many times, stop
current_rebuy_count = len(filled_entries) - 1 # minus first entry
if current_rebuy_count >= max_rebuys:
return None
# Compute total profit stake for derisk evaluation
profit_stake, profit_ratio = self.calc_total_profit(trade, filled_entries, filled_exits, current_rate)
# De-risk: if total loss relative to initial slice exceeds configured limit -> partial sell
derisk_limit = slice_amount * derisk_cfg
if derisk_cfg is not None and profit_stake < derisk_limit:
# Compute sell amount (approx) - keep at least 'min_stake_val' in position
sell_amount = trade.amount * current_rate / leverage - (min_stake_val * 1.55)
if sell_amount > min_stake_val:
# Return negative amount meaning sell this quantity to de-risk
return -sell_amount
# Iterate thresholds in order and find first matched
for i, th in enumerate(rebuy_thresholds):
# Only attempt rebuy that corresponds to current re-buy count (sequential)
if current_rebuy_count != i:
continue
if slice_profit_entry < th:
# First two steps: immediate rebuy without trend check
if i < 2:
buy_amount = slice_amount * rebuy_stakes[i] / leverage
# ensure at least min order size
if buy_amount < (min_stake_val * 1.5):
buy_amount = min_stake_val * 1.5
# enforce max stake limit
if buy_amount > max_stake_val:
return None
return buy_amount
# Later steps: require trend confirmation. Use EMA cross + momentum checks
else:
if df is None or len(df) == 0:
return None
ema_fast_val = df[f"EMA_{self.ema_fast}"].iloc[-1]
ema_slow_val = df[f"EMA_{self.ema_slow}"].iloc[-1]
rsi_short_val = df[f"RSI_{self.rsi_short}"].iloc[-1]
macd_val = df["MACD"].iloc[-1]
# trend condition: EMA_fast > EMA_slow AND short RSI showing improvement OR MACD positive
trend_ok = (ema_fast_val > ema_slow_val) and (rsi_short_val > df[f"RSI_{self.rsi_short}"].iloc[-3] or macd_val > 0)
if trend_ok:
buy_amount = slice_amount * rebuy_stakes[i] / leverage
if buy_amount < (min_stake_val * 1.5):
buy_amount = min_stake_val * 1.5
if buy_amount > max_stake_val:
return None
return buy_amount
else:
# trend not confirmed, skip rebuy for now
return None
return None
def short_rebuy_adjust_trade_position_v3(self, *args, **kwargs) -> Optional[float]:
# Mirror long logic for short positions.
# For shorts the trend confirmation in later steps should invert EMA test (fast < slow) and negative MACD.
# We'll call the long function but adjust df values beforehand by swapping EMA sign logic
# Simpler approach: reuse long logic but invert trend checks via a small wrapper.
# Extract df from kwargs if present and create a modified df copy with inverted MACD sign
df = kwargs.get('df', None)
if df is not None and len(df) > 0:
# we can swap EMA columns by adding small wrapper object, but easier is to run dedicated logic here
return self._short_rebuy_logic(*args, **kwargs)
else:
return self._short_rebuy_logic(*args, **kwargs)
def _short_rebuy_logic(self, trade: Trade, enter_tags, current_time: datetime, current_rate: float, current_profit: float,
min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float, df: Optional[DataFrame] = None, **kwargs) -> Optional[float]:
# Same structure as long rebuy but invert trend confirmation criteria
rebuy_stakes, rebuy_thresholds, derisk_cfg, max_rebuys, cooldown_minutes = self._load_rebuy_settings()
leverage = max(1.0, getattr(trade, 'leverage', 1.0))
min_stake_val = (min_stake or 0.0) / leverage
max_stake_val = max_stake / leverage
if trade.has_open_orders:
return None
filled_entries = trade.select_filled_orders(trade.entry_side)
filled_exits = trade.select_filled_orders(trade.exit_side)
if len(filled_entries) == 0:
return None
slice_amount = getattr(filled_entries[0], 'cost', 0.0)
last_entry_price = filled_entries[-1].safe_price
slice_profit_entry = (current_rate - last_entry_price) / last_entry_price
current_rebuy_count = len(filled_entries) - 1
if current_rebuy_count >= max_rebuys:
return None
profit_stake, profit_ratio = self.calc_total_profit(trade, filled_entries, filled_exits, current_rate)
derisk_limit = slice_amount * derisk_cfg
if derisk_cfg is not None and profit_stake < derisk_limit:
sell_amount = trade.amount * current_rate / leverage - (min_stake_val * 1.55)
if sell_amount > min_stake_val:
return -sell_amount
for i, th in enumerate(rebuy_thresholds):
if current_rebuy_count != i:
continue
if slice_profit_entry < th:
if i < 2:
buy_amount = slice_amount * rebuy_stakes[i] / leverage
if buy_amount < (min_stake_val * 1.5):
buy_amount = min_stake_val * 1.5
if buy_amount > max_stake_val:
return None
return buy_amount
else:
if df is None or len(df) == 0:
return None
ema_fast_val = df[f"EMA_{self.ema_fast}"].iloc[-1]
ema_slow_val = df[f"EMA_{self.ema_slow}"].iloc[-1]
rsi_short_val = df[f"RSI_{self.rsi_short}"].iloc[-1]
macd_val = df["MACD"].iloc[-1]
# For shorts: trend_ok when EMA_fast < EMA_slow and RSI dropping or MACD negative
trend_ok = (ema_fast_val < ema_slow_val) and (rsi_short_val < df[f"RSI_{self.rsi_short}"].iloc[-3] or macd_val < 0)
if trend_ok:
buy_amount = slice_amount * rebuy_stakes[i] / leverage
if buy_amount < (min_stake_val * 1.5):
buy_amount = min_stake_val * 1.5
if buy_amount > max_stake_val:
return None
return buy_amount
else:
return None
return None
############################################################################
# Exit logic (multi-target TP and stoploss handled separately)
############################################################################
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
df = df.copy()
# Multi-target TP triggers based on price relative to SMA or entry — here simplified by SMA targets
df["exit_long"] = df["close"] > (df[f"SMA_{self.sma_mid}"] * 1.06)
df["exit_short"] = df["close"] < (df[f"SMA_{self.sma_mid}"] * 0.94)
return df
############################################################################
# Optional: custom stoploss to integrate system-level doom stops (large dumps)
############################################################################
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float:
# If an extreme doom threshold is set in config, exit hard
doom_threshold = self._get_cfg("system_v3_stop_threshold_doom_futures", None)
if doom_threshold is not None and current_profit < -abs(doom_threshold):
# return value between -1 and 1 representing new stoploss; -1 means immediate market exit
return -0.999
# otherwise return default stoploss (no change)
return self.stoploss
# End of full V3 strategy implementation