Simple VWAP v1 - Stratégie d'exposition maximale au marché
Timeframe
4h
Direction
Long Only
Stoploss
-100.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
300
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# =============================================================
# Fichier rédigée par Moutonneux pour Freqtrade France 🇫🇷
# ➤ Soutenez-nous sur : https://coff.ee/freqtrade_france et obtenez davantage de stratégies
#
# 📌 Ce code est fourni à des fins personnelles ou éducatives.
# ❌ Il ne doit pas être partagé, distribué, ni publié dans un autre cadre.
# =============================================================
"""
Simple VWAP v1 - Stratégie d'exposition constante au marché
Cette stratégie adopte une approche contrariante : au lieu de chercher des entrées
parfaites et rares, elle vise à être exposée au marché en permanence en ouvrant
jusqu'à 40 positions simultanément avec des conditions d'entrée très permissives.
Philosophie : "Mieux vaut être dans le marché 90% du temps avec de petits gains
et petites pertes que de rater les grandes opportunités"
Timeframe : 4h
Positions max : 40
"""
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.persistence import Trade
import math
from functools import reduce
from datetime import datetime
from typing import Optional, Dict
import logging
logger = logging.getLogger(__name__)
from freqtrade.strategy import (
CategoricalParameter,
DecimalParameter,
IntParameter,
IStrategy
)
import ta as clean_ta
from technical import qtpylib
def VWAPB(dataframe, window_size=20, num_of_std=1):
"""
Calcule les bandes VWAP (Volume Weighted Average Price)
Les bandes VWAP sont similaires aux bandes de Bollinger mais utilisent
le prix pondéré par le volume au lieu d'une simple moyenne mobile.
Args:
dataframe: DataFrame contenant les données OHLCV
window_size: Période de calcul (par défaut 20)
num_of_std: Nombre d'écarts-types pour les bandes (par défaut 1)
Returns:
Tuple contenant (bande_basse, vwap, bande_haute)
"""
df = dataframe.copy()
df['vwap'] = qtpylib.rolling_vwap(df, window=window_size)
rolling_std = df['vwap'].rolling(window=window_size).std()
df['vwap_low'] = df['vwap'] - (rolling_std * num_of_std)
df['vwap_high'] = df['vwap'] + (rolling_std * num_of_std)
return df['vwap_low'], df['vwap'], df['vwap_high']
def lerp(a: float, b: float, t: float) -> float:
"""
Interpolation linéaire entre deux valeurs
Utilisée pour ajuster progressivement les montants de DCA
en cas de remplissage partiel des ordres.
Args:
a: Valeur de départ
b: Valeur d'arrivée
t: Facteur d'interpolation (0 = a, 1 = b)
Returns:
Valeur interpolée entre a et b
"""
return (1 - t) * a + t * b
class simple_vwap_v1(IStrategy):
"""
Simple VWAP v1 - Stratégie d'exposition maximale au marché
Cette stratégie utilise une approche unique avec :
- Des conditions d'entrée ultra-permissives (VWAP touch)
- Un filtre de confirmation sur 8 bougies consécutives
- Des sorties fréquentes basées sur EMA et CCI
- Un système de DCA intelligent avec jusqu'à 4 rechargements
- Une gestion dynamique de la taille des positions selon la volatilité
"""
# =================================================================
# Configuration de base de la stratégie
# =================================================================
INTERFACE_VERSION = 3
timeframe = '4h'
can_short = False
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
startup_candle_count = 300 # Besoin d'historique pour l'EMA 180
position_adjustment_enable = True
# Configuration des ordres
order_types = {
'entry': 'limit',
'exit': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
order_time_in_force = {'entry': 'GTC', 'exit': 'GTC'}
# ROI et stoploss par défaut (désactivés car gérés manuellement)
minimal_roi = {"0": 100.0}
stoploss = -1
trailing_stop = False
# =================================================================
# Configuration du graphique pour l'interface
# =================================================================
plot_config = {
"main_plot": {
# Indicateurs principaux sur le graphique des prix
"EMA": {"color": "#ffffff", "type": "line"},
"C-EMA": {"color": "#ff0000", "type": "scatter", "scatterSymbolSize": 10},
"VWAP_low": {"color": "#53c3eb", "type": "line"},
"I-VWAP": {"color": "#00ff00", "type": "scatter", "scatterSymbolSize": 10},
},
"subplots": {
# CCI dans un sous-graphique séparé
"CCI": {
"CCI": {"color": "#ffffff", "type": "line"},
"C-CCI": {"color": "#ff0000", "type": "scatter", "scatterSymbolSize": 6},
}
}
}
# =================================================================
# Paramètres de gestion du risque (optimisables)
# =================================================================
# Stoploss personnalisé : détermine la perte maximale acceptée
# Valeurs : -0.45 à -0.2 (de -45% à -20%)
# Par défaut : -0.37 (-37%)
my_custom_stoploss = DecimalParameter(
-0.45, -0.2,
decimals=2,
default=-0.37,
space="sell",
optimize=True
)
# Ratio du capital total à utiliser pour le trading
# Permet de garder une réserve de sécurité
# Valeurs : 0.25 à 1.0 (25% à 100% du capital)
# Par défaut : 0.35 (35%)
tradable_balance_ratio = CategoricalParameter(
[0.25, 0.35, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
default=0.35,
space="buy",
optimize=True
)
# =================================================================
# Paramètres DCA (Dollar Cost Averaging)
# =================================================================
# Déclencheur du premier safety order
# Si la position perd plus que cette valeur, on peut recharger
# Valeurs : -0.03 à 0.0 (-3% à 0%)
# Par défaut : -0.012 (-1.2%)
initial_safety_order_trigger = DecimalParameter(
-0.03, 0.0,
decimals=3,
default=-0.012,
space="buy",
optimize=True
)
# Nombre maximum de safety orders (rechargements)
# Plus ce nombre est élevé, plus on peut moyenner à la baisse
# Valeurs : 3 à 12
# Par défaut : 4
max_so_multiplier_orig = IntParameter(
3, 12,
default=4,
space="buy",
optimize=True
)
# Espacement entre les safety orders
# Plus la valeur est élevée, plus les ordres sont espacés
# Valeurs : 1 à 10
# Par défaut : 3
safety_order_step_scale = IntParameter(
1, 10,
default=3,
space="buy",
optimize=True
)
# Multiplicateur de volume pour chaque safety order
# Détermine de combien augmente la taille de chaque ordre suivant
# Valeurs : 1.3 à 2.3
# Par défaut : 1.8
safety_order_volume_scale = DecimalParameter(
1.3, 2.3,
decimals=1,
default=1.8,
space="buy",
optimize=True
)
# =================================================================
# Variables internes
# =================================================================
cust_proposed_initial_stakes = {}
max_so_multiplier = 4 # Sera recalculé dynamiquement
# =================================================================
# Méthodes de gestion des positions
# =================================================================
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, **kwargs):
"""
Gestion personnalisée des sorties avec stoploss ajustable
Cette méthode est appelée à chaque tick pour vérifier si une position
doit être fermée. Elle implémente un stoploss personnalisé qui peut
être optimisé indépendamment du stoploss par défaut de Freqtrade.
Args:
pair: Paire de trading (ex: BTC/USDT)
trade: Objet Trade contenant les infos de la position
current_time: Timestamp actuel
current_rate: Prix actuel
current_profit: Profit/perte actuel en ratio (0.1 = +10%)
Returns:
'stop_loss' si le stoploss est atteint, None sinon
"""
# Vérifier d'abord les conditions de sortie par défaut
tag = super().custom_sell(pair, trade, current_time, current_rate,
current_profit, **kwargs)
if tag:
return tag
# Appliquer notre stoploss personnalisé
if current_profit <= self.my_custom_stoploss.value:
return 'stop_loss'
return None
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str,
amount: float, rate: float, time_in_force: str,
exit_reason: str, current_time: datetime, **kwargs) -> bool:
"""
Confirmation finale avant fermeture d'une position
Nettoie les données internes quand une position est complètement fermée.
Returns:
True pour confirmer la sortie
"""
# Nettoyer les données si on ferme complètement la position
if trade.amount == amount and pair in self.cust_proposed_initial_stakes:
del self.cust_proposed_initial_stakes[pair]
# Nettoyage spécifique pour backtest/hyperopt
if self.dp.runmode.value in ("backtest", "hyperopt"):
for t in self._open_trades:
if t.get('pair') == pair:
self._open_trades.remove(t)
break
return True
def custom_stake_amount(self, pair: str, current_time: datetime,
current_rate: float, proposed_stake: float,
min_stake: float, max_stake: float, **kwargs) -> float:
"""
Calcul intelligent de la taille de position
Cette méthode ajuste la taille de chaque position en fonction de :
1. La volatilité actuelle (ATR)
2. Le nombre maximum de positions
3. Le ratio de capital alloué au trading
4. Les minimums requis par l'exchange
La réduction basée sur la volatilité protège le capital lors
des périodes de forte volatilité.
Args:
pair: Paire de trading
current_time: Timestamp actuel
current_rate: Prix actuel
proposed_stake: Montant proposé par Freqtrade
min_stake: Montant minimum de l'exchange
max_stake: Montant maximum autorisé
Returns:
Montant ajusté pour la position
"""
base_stake = proposed_stake
# =============================================================
# Ajustement basé sur la volatilité
# =============================================================
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is not None and not dataframe.empty:
# Calculer l'ATR sur les 15 dernières bougies
volatility_window = 15
recent_high = dataframe['high'].iloc[-volatility_window:]
recent_low = dataframe['low'].iloc[-volatility_window:]
recent_close = dataframe['close'].iloc[-1]
# ATR en pourcentage du prix
recent_atr_pct = (recent_high - recent_low).mean() / recent_close
# Réduire la taille si volatilité élevée
volatility_threshold = 0.102 # 10.2%
if recent_atr_pct > volatility_threshold:
# Volatilité très élevée : réduire de 50%
volatility_adjustment = 0.5
elif recent_atr_pct > volatility_threshold * 0.6:
# Volatilité modérée : réduire de 25%
volatility_adjustment = 0.75
else:
# Volatilité normale : taille complète
volatility_adjustment = 1.0
base_stake *= volatility_adjustment
# =============================================================
# Calcul du quota par position
# =============================================================
ratio = self.tradable_balance_ratio.value
max_trades = self.config.get("max_open_trades", 40)
# Capital disponible total
available_wallet = self.wallets.get_total_stake_amount()
# Quota = (Capital * Ratio) / Nombre de positions max
quota = (available_wallet * ratio) / max_trades
# Ne pas dépasser le quota calculé
base_stake = min(base_stake, quota)
# =============================================================
# Application des limites
# =============================================================
# Minimum requis par Hyperliquid
stake_final = max(15.0, base_stake)
# Ne pas dépasser le maximum autorisé
stake_final = min(stake_final, max_stake)
return stake_final
def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
min_stake: float, max_stake: float, **kwargs) -> Optional[float]:
"""
Système de DCA (Dollar Cost Averaging) intelligent
Cette méthode implémente un système de rechargement progressif
des positions perdantes. Les rechargements sont déclenchés à des
seuils de perte croissants et avec des montants croissants.
Exemple avec les paramètres par défaut :
- 1er DCA à -1.2% avec 1.8x le montant initial
- 2e DCA à -3.6% avec 3.24x le montant initial
- 3e DCA à -10.8% avec 5.83x le montant initial
- 4e DCA à -32.4% avec 10.5x le montant initial
Args:
trade: Position actuelle
current_time: Timestamp
current_rate: Prix actuel
current_profit: Profit/perte actuel
min_stake: Montant minimum
max_stake: Montant maximum
Returns:
Montant à ajouter à la position ou None
"""
# Ne rien faire si on n'est pas en perte suffisante
if current_profit > self.initial_safety_order_trigger.value:
return None
# Récupérer l'historique des ordres
filled_buys = trade.select_filled_orders(trade.entry_side)
count_of_buys = len(filled_buys)
# Vérifier qu'on n'a pas atteint la limite de DCA
if not (1 <= count_of_buys <= self.max_so_multiplier_orig.value):
return None
# =============================================================
# Calcul du seuil de déclenchement pour ce DCA
# =============================================================
if self.safety_order_step_scale.value == 1:
# Espacement linéaire
safety_order_trigger = abs(self.initial_safety_order_trigger.value) * count_of_buys
else:
# Espacement exponentiel (recommandé)
initial_trigger = abs(self.initial_safety_order_trigger.value)
scale = self.safety_order_step_scale.value
if scale > 1:
# Formule pour progression géométrique croissante
numerator = scale * (pow(scale, count_of_buys - 1) - 1)
denominator = scale - 1
safety_order_trigger = initial_trigger + (initial_trigger * scale * numerator / denominator)
else:
# Formule pour progression géométrique décroissante
numerator = 1 - pow(scale, count_of_buys - 1)
denominator = 1 - scale
safety_order_trigger = initial_trigger + (initial_trigger * scale * numerator / denominator)
# =============================================================
# Vérifier si on a atteint le seuil de DCA
# =============================================================
if current_profit > (-1 * abs(safety_order_trigger)):
return None
# =============================================================
# Calcul du montant du DCA
# =============================================================
try:
# Montant du premier ordre
actual_initial_stake = filled_buys[0].cost
# Calculer le montant pour ce DCA
stake_amount = actual_initial_stake * pow(
self.safety_order_volume_scale.value,
count_of_buys - 1
)
# Compensation pour les ordres partiellement remplis
if trade.pair in self.cust_proposed_initial_stakes:
proposed_initial = self.cust_proposed_initial_stakes[trade.pair]
if proposed_initial > 0:
# Calculer ce qui aurait dû être investi
already_bought = sum(buy.cost for buy in filled_buys)
theoretical_bought = proposed_initial * sum(
pow(self.safety_order_volume_scale.value, i)
for i in range(count_of_buys)
)
# Ajuster si nécessaire
if theoretical_bought > already_bought:
compensation = theoretical_bought - already_bought
stake_amount += compensation * 0.4 # 40% de compensation
return stake_amount
except Exception as e:
logger.error(f'Erreur DCA pour {trade.pair}: {str(e)}')
return None
def get_max_so_multiplier(self):
"""
Calcule le multiplicateur total nécessaire pour tous les DCA
Cette valeur est utilisée pour réserver suffisamment de capital
dès le premier ordre afin de pouvoir effectuer tous les DCA prévus.
Exemple : avec 4 DCA et un multiplicateur de 1.8 :
- Total = 1 + 1.8 + 3.24 + 5.83 + 10.5 = 22.37
- Le premier ordre ne représentera que 1/22.37 du capital alloué
Returns:
Multiplicateur total pour la répartition du capital
"""
max_orders = self.max_so_multiplier_orig.value
scale = self.safety_order_volume_scale.value
if max_orders <= 0:
return 1
if scale == 1:
# Cas simple : tous les ordres ont la même taille
return max_orders + 1
# Calcul de la somme d'une série géométrique
# 1 + scale + scale² + ... + scale^(n-1)
if scale > 1:
total = 1 + scale * (pow(scale, max_orders) - 1) / (scale - 1)
else:
total = 1 + scale * (1 - pow(scale, max_orders)) / (1 - scale)
return total
# =================================================================
# Indicateurs techniques
# =================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Calcul des indicateurs techniques
Cette méthode calcule tous les indicateurs nécessaires :
1. EMA 180 : Tendance de fond sur ~30 jours
2. CCI 160 : Momentum et surachat/survente
3. VWAP : Prix moyen pondéré par volume
Les indicateurs préfixés par 'I-' sont pour les entrées (vert)
Les indicateurs préfixés par 'C-' sont pour les sorties (rouge)
"""
# =============================================================
# Indicateurs principaux
# =============================================================
# EMA 180 périodes (30 jours en 4h)
dataframe['EMA'] = clean_ta.trend.ema_indicator(
close=dataframe['close'],
window=180
)
# CCI (Commodity Channel Index) 160 périodes
dataframe['CCI'] = clean_ta.trend.CCIIndicator(
high=dataframe['high'],
low=dataframe['low'],
close=dataframe['close'],
window=160,
constant=0.0175 # Constante personnalisée
).cci()
# VWAP avec bandes (5 périodes, 1.05 écart-type)
dataframe["VWAP_low"], _, _ = VWAPB(
dataframe,
window_size=5,
num_of_std=1.05
)
# =============================================================
# Indicateurs pour l'affichage graphique
# =============================================================
# Signal d'entrée : VWAP touché
dataframe['I-VWAP'] = np.where(
(dataframe['VWAP_low'] < dataframe['high']),
dataframe['VWAP_low'],
np.nan
)
# Signal de sortie : EMA en baisse
dataframe['C-EMA'] = np.where(
(dataframe['EMA'] < dataframe['EMA'].shift(50)),
dataframe['close'],
np.nan
)
# Signal de sortie : CCI négatif et en baisse
dataframe['C-CCI'] = np.where(
((dataframe['CCI'] < 0) & (dataframe['CCI'] < dataframe['CCI'].shift(1))),
0, # Ligne zéro pour la visibilité
np.nan
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Conditions d'entrée en position
La stratégie n'a qu'une seule condition très simple :
- Le prix doit toucher la bande basse du VWAP
MAIS avec une subtilité importante : le signal doit persister
pendant 8 bougies consécutives (32 heures) avant d'ouvrir.
Cette confirmation filtre les faux signaux tout en gardant
une approche très ouverte aux entrées.
"""
conditions = []
# Condition unique : prix touche la bande basse VWAP
conditions.append((dataframe['VWAP_low'] < dataframe['high']))
# Marquer les bougies qui remplissent la condition
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'enter_long'
] = 1
# =============================================================
# Filtre de confirmation : 8 bougies consécutives
# =============================================================
# Le signal n'est validé que s'il persiste pendant 8 bougies
# Cela évite les entrées sur des pics isolés
cumulative_window = 8
dataframe['enter_long'] = dataframe['enter_long'].rolling(
window=cumulative_window
).min()
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Conditions de sortie de position
La stratégie utilise deux conditions de sortie (en OU) :
1. EMA en baisse sur 50 périodes (tendance qui s'essouffle)
2. CCI négatif ET en baisse (momentum baissier)
Ces conditions se déclenchent fréquemment, ce qui fait que
les positions sont rarement gardées très longtemps.
"""
conditions = []
# Condition 1 : EMA décroît sur 50 périodes
ema_shift = 50
conditions.append(
(dataframe['EMA'] < dataframe['EMA'].shift(ema_shift))
)
# Condition 2 : CCI négatif ET en baisse
conditions.append(
((dataframe['CCI'] < 0) &
(dataframe['CCI'] < dataframe['CCI'].shift(1)))
)
# Sortir si AU MOINS UNE condition est remplie (OU logique)
if conditions:
dataframe.loc[
reduce(lambda x, y: x | y, conditions),
'exit_long'
] = 1
return dataframe