Grid Trading Spot — Capture la volatilité via des paliers de prix.
Timeframe
5m
Direction
Long Only
Stoploss
-5.0%
Trailing Stop
Yes
ROI
0m: 5.0%, 120m: 3.0%, 360m: 2.0%, 720m: 1.0%
Interface Version
3
Startup Candles
50
Indicators
2
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# ══════════════════════════════════════════════════════════════
# anis solidscale - Elite Spot Trading Suite
# STRATÉGIE : GridTradingSpot
# CATÉGORIE : 1 — Fondations
# OUTIL : Freqtrade (IStrategy)
# ══════════════════════════════════════════════════════════════
#
# DESCRIPTION :
# Grid Trading adaptatif : place automatiquement des ordres d'achat
# et de vente sur des paliers de prix réguliers pour capturer la
# volatilité latérale du marché. Fonctionne EXCLUSIVEMENT en Spot
# (Long-Only, zéro levier).
#
# LOGIQUE :
# 1. L'ATR définit dynamiquement l'espacement entre les niveaux
# du grid (plus de volatilité = grid plus large)
# 2. Le RSI filtre les entrées (pas d'achat si surachat)
# 3. Chaque niveau est un take-profit par rapport au niveau précédent
#
# CONFIGURABILITÉ :
# → TOUS les paramètres sont des IntParameter/DecimalParameter
# → Modifiables via l'hyperopt OU manuellement dans le code
# → Aucune valeur codée en dur
#
# INTÉGRATION :
# → Utilise utils.indicators.CommonIndicators (pas de duplication)
# → Utilise utils.logging_utils.TradeLogger (SQLite par stratégie)
# → Utilise utils.telegram_notifier.TelegramNotifier
# ══════════════════════════════════════════════════════════════
import sys
from pathlib import Path
from datetime import datetime, timezone
import numpy as np
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
# ── Ajout du chemin parent pour importer les utils partagés ──
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from utils.indicators import CommonIndicators
from utils.logging_utils import TradeLogger
from utils.telegram_notifier import TelegramNotifier
class GridTradingSpot(IStrategy):
"""
Grid Trading Spot — Capture la volatilité via des paliers de prix.
PRINCIPES ANIS SOLIDSCALE :
✅ Long-Only (Spot) — can_short = False
✅ DRY_RUN par défaut
✅ Tous les paramètres configurables
✅ Logging SQLite + CSV par stratégie
✅ Notifications Telegram
"""
# ══════════════════════════════════════════════════════════
# MÉTADONNÉES FREQTRADE
# ══════════════════════════════════════════════════════════
INTERFACE_VERSION = 3
# CHOIX : can_short = False est le verrou principal qui empêche
# toute vente à découvert. C'est la règle n°1 anis solidscale.
can_short = False
# ── Timeframe ──
# CHOIX : 5m pour capturer les micro-mouvements du grid.
# Un timeframe plus court (1m) génèrerait trop de bruit.
timeframe = "5m"
# ── Nombre de bougies nécessaires pour que les indicateurs soient stables ──
startup_candle_count = 50
# ══════════════════════════════════════════════════════════
# PARAMÈTRES CONFIGURABLES (tous modifiables)
# ══════════════════════════════════════════════════════════
# ── Grid : nombre de niveaux ──
# CHOIX : 10 niveaux par défaut. Plus de niveaux = plus de trades mais
# plus de capital distribué. Range [3, 30] pour supporter du micro-grid
# au macro-grid.
grid_levels = IntParameter(3, 30, default=10, space="buy",
optimize=True, load=True)
# ── Grid : espacement entre niveaux (% du prix) ──
# CHOIX : 1.0% par défaut. Multiplié par l'ATR ratio pour s'adapter
# à la volatilité. Range [0.3, 5.0] pour supporter les marchés calmes
# et les marchés crypto très volatils.
grid_spacing_pct = DecimalParameter(0.3, 5.0, default=1.0, decimals=1,
space="buy", optimize=True, load=True)
# ── RSI : période de calcul ──
rsi_period = IntParameter(7, 30, default=14, space="buy",
optimize=True, load=True)
# ── RSI : seuil maximum pour autoriser un achat ──
# CHOIX : 65 par défaut. Pas d'achat si RSI > ce seuil (surachat)
rsi_buy_max = IntParameter(50, 80, default=65, space="buy",
optimize=True, load=True)
# ── ATR : période pour la volatilité adaptative ──
atr_period = IntParameter(7, 30, default=14, space="buy",
optimize=True, load=True)
# ── ATR : multiplicateur pour adapter le grid ──
# CHOIX : Si ATR est élevé, on élargit le grid pour éviter les
# faux signaux. 1.0 = pas d'adaptation, 2.0 = adaptation forte.
atr_multiplier = DecimalParameter(0.5, 3.0, default=1.0, decimals=1,
space="buy", optimize=True, load=True)
# ── ROI : Objectifs de sortie configurables ──
# CHOIX : ROI minimal basé sur l'espacement du grid.
# Le grid vise naturellement 1 niveau de profit.
minimal_roi = {
"0": 0.05, # 5% max si pas de signal de sortie
"120": 0.03, # 3% après 2h
"360": 0.02, # 2% après 6h
"720": 0.01, # 1% après 12h
}
# ── Stoploss : configurable ──
# CHOIX : -5% comme filet de sécurité sous le grid complet.
# Le trailing stop est plus important ici.
stoploss = -0.05
# ── Trailing Stop ──
trailing_stop = True
trailing_stop_positive = 0.01 # Active à +1%
trailing_stop_positive_offset = 0.02 # Depuis +2%
trailing_only_offset_is_reached = True
# ══════════════════════════════════════════════════════════
# INITIALISATION
# ══════════════════════════════════════════════════════════
def __init__(self, config: dict) -> None:
super().__init__(config)
# ── Logger SQLite dédié à cette stratégie ──
self._trade_logger = TradeLogger(strategy_name="GridTradingSpot")
# ── Notificateur Telegram ──
self._notifier = TelegramNotifier()
# ── Notification de démarrage ──
is_dry = config.get("dry_run", True)
self._notifier.send_startup_message("GridTradingSpot", dry_run=is_dry)
# ══════════════════════════════════════════════════════════
# INDICATEURS
# ══════════════════════════════════════════════════════════
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Ajoute les indicateurs techniques au DataFrame.
REFACTORING : Utilise CommonIndicators (utils/indicators.py)
au lieu de recalculer les indicateurs ici. Cela garantit que
le calcul est identique partout et facilite la maintenance.
"""
# ── RSI : filtrage des entrées ──
dataframe = CommonIndicators.add_rsi(
dataframe, period=self.rsi_period.value
)
# ── ATR : adaptation dynamique du grid ──
dataframe = CommonIndicators.add_atr(
dataframe, period=self.atr_period.value
)
# ── Calcul des niveaux de grid dynamiques ──
# CHOIX : Le grid est centré sur le prix actuel, avec des niveaux
# espacés de grid_spacing_pct * (1 + ATR_ratio * atr_multiplier).
# L'ATR ratio normalise l'ATR par rapport au prix pour obtenir un %.
atr_col = f"atr_{self.atr_period.value}"
dataframe["atr_ratio"] = dataframe[atr_col] / dataframe["close"]
# Espacement effectif = base * (1 + atr_ratio * multiplier)
dataframe["effective_spacing"] = (
self.grid_spacing_pct.value / 100.0
* (1 + dataframe["atr_ratio"] * self.atr_multiplier.value)
)
# ── Niveau de prix d'achat (grid inférieur) ──
dataframe["grid_buy_level"] = (
dataframe["close"] * (1 - dataframe["effective_spacing"])
)
# ── Niveau de prix de vente (grid supérieur) ──
dataframe["grid_sell_level"] = (
dataframe["close"] * (1 + dataframe["effective_spacing"])
)
return dataframe
# ══════════════════════════════════════════════════════════
# SIGNAUX D'ENTRÉE
# ══════════════════════════════════════════════════════════
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Conditions d'entrée LONG (Spot uniquement).
LOGIQUE DU GRID :
1. Le prix actuel est au niveau du grid d'achat (le low de la bougie
touche le grid_buy_level)
2. Le RSI est sous le seuil de surachat (filtre de qualité)
3. Le volume n'est pas anormalement bas (liquidité suffisante)
SÉCURITÉ : La vérification de liquidité (volume) est conforme
au principe §1 "Vérification systématique de la liquidité".
"""
rsi_col = f"rsi_{self.rsi_period.value}"
dataframe.loc[
(
# Condition 1 : Le prix touche le niveau d'achat du grid
(dataframe["low"] <= dataframe["grid_buy_level"])
&
# Condition 2 : RSI pas en surachat
(dataframe[rsi_col] < self.rsi_buy_max.value)
&
# Condition 3 : Volume suffisant (> 50% de la moyenne)
(dataframe["volume"] > dataframe["volume"].rolling(20).mean() * 0.5)
&
# Condition 4 : Volume non nul (sécurité)
(dataframe["volume"] > 0)
),
"enter_long",
] = 1
return dataframe
# ══════════════════════════════════════════════════════════
# SIGNAUX DE SORTIE
# ══════════════════════════════════════════════════════════
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Conditions de sortie (vente Spot).
LOGIQUE :
1. Le prix atteint le niveau de vente du grid (take profit naturel)
2. OU le RSI est en zone de surachat extrême (> 75)
Le trailing stop et minimal_roi fournissent des sorties additionnelles.
"""
rsi_col = f"rsi_{self.rsi_period.value}"
dataframe.loc[
(
# Condition 1 : Prix atteint le grid de vente
(dataframe["high"] >= dataframe["grid_sell_level"])
|
# Condition 2 : RSI en surachat extrême
(dataframe[rsi_col] > 75)
)
&
(dataframe["volume"] > 0),
"exit_long",
] = 1
return dataframe
# ══════════════════════════════════════════════════════════
# CALLBACKS PERSONNALISÉS
# ══════════════════════════════════════════════════════════
def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force,
current_time, entry_tag, side, **kwargs) -> bool:
"""
Confirmation avant chaque entrée.
LOGGING : Enregistre chaque tentative d'entrée dans SQLite.
TELEGRAM : Envoie une alerte sur l'entrée.
"""
is_dry = self.config.get("dry_run", True)
self._trade_logger.log_trade(
pair=pair, side="buy", price=rate, amount=amount,
dry_run=is_dry, extra_info=f"entry_tag:{entry_tag}",
)
self._notifier.send_trade_alert(
strategy_name="GridTradingSpot", pair=pair, side="buy",
price=rate, amount=amount, dry_run=is_dry,
)
return True
def confirm_trade_exit(self, pair, trade, order_type, amount, rate,
time_in_force, exit_reason, current_time,
**kwargs) -> bool:
"""
Confirmation avant chaque sortie.
LOGGING : Enregistre la sortie avec le PnL calculé.
"""
is_dry = self.config.get("dry_run", True)
pnl = trade.calc_profit_ratio(rate) * 100
self._trade_logger.log_trade(
pair=pair, side="sell", price=rate, amount=amount,
pnl=pnl, dry_run=is_dry,
extra_info=f"exit_reason:{exit_reason}",
)
self._notifier.send_trade_alert(
strategy_name="GridTradingSpot", pair=pair, side="sell",
price=rate, amount=amount, pnl=pnl, dry_run=is_dry,
)
return True