Skip to content

Commit 5946b95

Browse files
committed
feat : <README.md> <main.py> <indicators.py> <notifier.py> <strategy.py> <test_backtest.py> <test_grid_search.py> adding backtest in order to try our strategy and see effect annuary monthly or weekly adding a function to find the better params for bette incomes changing the strategy and change the readme in order to inform users
1 parent bcee8e5 commit 5946b95

7 files changed

Lines changed: 787 additions & 33 deletions

File tree

README.md

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Ce projet est un bot Python conçu pour surveiller le marché **BTCUSDT** (Bitco
1616
- **Indicateurs techniques** : Utilisation d'indicateurs personnalisés pour la prise de décision.
1717
- **Notifications Discord** : Envoi automatique des signaux via un webhook Discord.
1818
- **Gestion d'état** : Suivi de l'état du bot et des positions ouvertes/fermées.
19+
- **Backtests** : Backtest 1h avec capital initial, frais, SL/TP dynamiques (ATR).
20+
- **Grille de paramètres** : Recherche automatique des meilleurs seuils.
1921
- **Tests automatisés** : Scripts de test pour vérifier la connexion à l'API Kraken et simuler des scénarios.
2022

2123
---
@@ -33,6 +35,8 @@ Ce projet est un bot Python conçu pour surveiller le marché **BTCUSDT** (Bitco
3335
| `config/config.py` | Configuration de base de l'application. |
3436
| `test_connection.py` | Teste la connexion à l'API Kraken. |
3537
| `test_simulation.py` | Simule des scénarios de trading pour valider la logique du bot. |
38+
| `test_backtest.py` | Backtest 1h avec capital initial, frais et SL/TP. |
39+
| `test_grid_search.py` | Grille de paramètres pour optimiser la stratégie. |
3640
| `requirements.txt` | Liste des dépendances Python nécessaires. |
3741

3842
---
@@ -59,11 +63,12 @@ Ce projet est un bot Python conçu pour surveiller le marché **BTCUSDT** (Bitco
5963
```
6064

6165
3. **Configurer le webhook Discord** :
62-
- Créez un webhook Discord dans votre serveur (Paramètres du serveur > Intégrations > Webhooks).
63-
- Copiez l'URL du webhook et ajoutez-la dans le fichier `src/notifier.py` :
64-
```python
65-
WEBHOOK_URL = "votre_url_de_webhook"
66-
```
66+
- Créez un webhook Discord dans votre serveur (Paramètres du serveur > Intégrations > Webhooks).
67+
- Ajoutez l'URL dans le fichier `.env` :
68+
```bash
69+
DISCORD_WEBHOOK_URL=votre_url_de_webhook
70+
DISCORD_HEARTBEAT_WEBHOOK_URL=votre_url_de_webhook_heartbeat
71+
```
6772
6873
4. **Configurer l'API Kraken** :
6974
- Créez une clé API sur votre compte Kraken (Paramètres > API).
@@ -96,6 +101,16 @@ Le bot affichera les signaux générés dans la console et les enverra égalemen
96101
python test_simulation.py
97102
```
98103
104+
4. **Backtester la stratégie (1h)** :
105+
```bash
106+
python test_backtest.py
107+
```
108+
109+
5. **Lancer la grille de paramètres** :
110+
```bash
111+
python test_grid_search.py
112+
```
113+
99114
## 📊 Exemple de signal Discord
100115
101116
Voici un exemple de message envoyé via le webhook Discord :
@@ -109,9 +124,40 @@ Voici un exemple de message envoyé via le webhook Discord :
109124
## 📝 Personnalisation
110125
111126
- **Ajouter des indicateurs** : Modifiez le fichier `src/indicators.py` pour ajouter vos propres indicateurs techniques.
112-
- **Changer la stratégie** : Adaptez la logique dans `main.py` pour modifier la stratégie de trading.
127+
- **Changer la stratégie** : Adaptez la logique dans `src/strategy.py`.
113128
- **Personnaliser les notifications** : Modifiez le format des messages dans `src/notifier.py`.
114129
130+
### Paramètres de stratégie (via `.env`)
131+
132+
```bash
133+
# Seuils de régime
134+
CHOP_TREND_MAX=55
135+
CHOP_RANGE_MIN=65
136+
137+
# Force de tendance / volatilité
138+
EMA_GAP_MIN=0.0006
139+
ATR_PCT_MIN=0.001
140+
141+
# RSI pullback
142+
RSI_PULLBACK_LONG_MIN=48
143+
RSI_PULLBACK_SHORT_MAX=52
144+
145+
# Range optionnel (true/false)
146+
USE_RANGE=false
147+
```
148+
149+
### Paramètres de backtest (via `.env`)
150+
151+
```bash
152+
INITIAL_CAPITAL=10
153+
FEE_RATE=0.0004
154+
USE_ATR_STOPS=true
155+
ATR_MULT_SL=1.5
156+
ATR_MULT_TP=2.5
157+
COOLDOWN_BARS=3
158+
LONG_ONLY=true
159+
```
160+
115161
## ⚠️ Avertissements
116162
117163
Ce bot est fourni à titre éducatif. Ne tradez pas avec de l'argent réel sans avoir testé et validé la stratégie.

main.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from src.indicators import TechnicalIndicators
66
from src.notifier import DiscordNotifier
77
from src.state_manager import StateManager
8+
from src.strategy import Strategy
89
from dotenv import load_dotenv
910
import os
1011
from datetime import datetime
@@ -19,6 +20,7 @@ def analyze_market():
1920
timeframe = os.getenv('TIMEFRAME', '1h')
2021
exchange_name = os.getenv('EXCHANGE', 'kraken')
2122
send_heartbeat = os.getenv('SEND_HEARTBEAT', 'false').lower() == 'true'
23+
data_limit = int(os.getenv('DATA_LIMIT', '500'))
2224

2325
print(f"\n{'='*60}")
2426
print(f"🤖 BOT ACTIF - Analyse en cours...")
@@ -34,7 +36,7 @@ def analyze_market():
3436
notifier = DiscordNotifier()
3537

3638
# Récupération des données
37-
df = fetcher.get_ohlcv(timeframe=timeframe, limit=200)
39+
df = fetcher.get_ohlcv(timeframe=timeframe, limit=max(260, data_limit))
3840

3941
if df is None:
4042
print("❌ Impossible de récupérer les données")
@@ -49,9 +51,8 @@ def analyze_market():
4951
# Calcul des indicateurs
5052
df = TechnicalIndicators.add_all_indicators(df)
5153

52-
# Dernières valeurs
53-
last = df.iloc[-1]
54-
prev = df.iloc[-2]
54+
# Dernière bougie clôturée (évite la bougie en cours)
55+
last = df.iloc[-2]
5556

5657
# Affichage de l'analyse
5758
print(f"📈 Situation actuelle :")
@@ -60,33 +61,22 @@ def analyze_market():
6061
print(f" EMA 50 : ${last['ema_50']:,.2f}")
6162
print(f" EMA 200 : ${last['ema_200']:,.2f}")
6263
print(f" RSI : {last['rsi']:.2f}")
64+
print(f" CHOP : {last['chop']:.2f}")
65+
print(f" Support : {last['support']:.2f}" if last['support'] == last['support'] else " Support : N/A")
66+
print(f" Résistance: {last['resistance']:.2f}" if last['resistance'] == last['resistance'] else " Résistance: N/A")
6367
print(f" Tendance : {last['trend']}")
6468
print(f" Volume : {last['volume']:,.2f}")
6569

6670
# Affichage du dernier signal
6771
last_signal = state_manager.get_last_signal()
6872
print(f"\n🔔 Dernier signal envoyé : {last_signal if last_signal else 'Aucun'}")
6973

70-
# Détermination du signal actuel
71-
current_signal = None
72-
73-
# Logique ACHAT
74-
if (last['trend'] == 'BULLISH' and
75-
last['rsi'] < 70 and
76-
last['rsi'] > 30 and
77-
prev['ema_20'] <= prev['ema_50'] and last['ema_20'] > last['ema_50']):
78-
current_signal = 'BUY'
79-
80-
# Logique VENTE
81-
elif (last['trend'] == 'BEARISH' or
82-
last['rsi'] > 75 or
83-
(prev['ema_20'] >= prev['ema_50'] and last['ema_20'] < last['ema_50'])):
84-
current_signal = 'SELL'
85-
86-
else:
87-
current_signal = 'NEUTRAL'
74+
# Détermination du signal actuel via la stratégie 1h
75+
strategy_signal = Strategy.generate_signal(df)
76+
current_signal = strategy_signal.signal
8877

8978
print(f"🎯 Signal détecté : {current_signal}")
79+
print(f"🧠 Raison : {strategy_signal.reason}")
9080

9181
# ═══════════════════════════════════════════════════════════
9282
# INITIALISATION DES VARIABLES (IMPORTANT !)
@@ -106,9 +96,13 @@ def analyze_market():
10696
price=last['close'],
10797
indicators={
10898
'rsi': f"{last['rsi']:.2f}",
109-
'trend': last['trend'],
99+
'trend': strategy_signal.context.get('trend', last['trend']),
110100
'ema_20': f"${last['ema_20']:,.2f}",
111-
'ema_50': f"${last['ema_50']:,.2f}"
101+
'ema_50': f"${last['ema_50']:,.2f}",
102+
'ema_200': f"${last['ema_200']:,.2f}",
103+
'chop': f"{last['chop']:.2f}",
104+
'support': f"${last['support']:,.2f}" if last['support'] == last['support'] else "N/A",
105+
'resistance': f"${last['resistance']:,.2f}" if last['resistance'] == last['resistance'] else "N/A"
112106
}
113107
)
114108
status = "🟢 Nouveau signal BUY envoyé"
@@ -121,9 +115,13 @@ def analyze_market():
121115
price=last['close'],
122116
indicators={
123117
'rsi': f"{last['rsi']:.2f}",
124-
'trend': last['trend'],
118+
'trend': strategy_signal.context.get('trend', last['trend']),
125119
'ema_20': f"${last['ema_20']:,.2f}",
126-
'ema_50': f"${last['ema_50']:,.2f}"
120+
'ema_50': f"${last['ema_50']:,.2f}",
121+
'ema_200': f"${last['ema_200']:,.2f}",
122+
'chop': f"{last['chop']:.2f}",
123+
'support': f"${last['support']:,.2f}" if last['support'] == last['support'] else "N/A",
124+
'resistance': f"${last['resistance']:,.2f}" if last['resistance'] == last['resistance'] else "N/A"
127125
}
128126
)
129127
status = "🔴 Nouveau signal SELL envoyé"
@@ -169,7 +167,10 @@ def analyze_market():
169167
fields=[
170168
{"name": "💰 Prix", "value": f"${last['close']:,.2f}", "inline": True},
171169
{"name": "📊 RSI", "value": f"{last['rsi']:.2f}", "inline": True},
170+
{"name": "🧭 CHOP", "value": f"{last['chop']:.2f}", "inline": True},
172171
{"name": "Tendance", "value": trend_display, "inline": True},
172+
{"name": "Support", "value": f"${last['support']:,.2f}" if last['support'] == last['support'] else "N/A", "inline": True},
173+
{"name": "Résistance", "value": f"${last['resistance']:,.2f}" if last['resistance'] == last['resistance'] else "N/A", "inline": True},
173174
{"name": "🕐 Heure", "value": discord_time, "inline": False}
174175
]
175176
)

src/indicators.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,88 @@ def calculate_rsi(df, period=14):
2323
return rsi
2424

2525
@staticmethod
26-
def add_all_indicators(df):
26+
def calculate_choppiness(df, period=14):
27+
"""Choppiness Index (CHOP)"""
28+
high = df['high']
29+
low = df['low']
30+
close = df['close']
31+
32+
prev_close = close.shift(1)
33+
true_range = pd.concat(
34+
[
35+
(high - low),
36+
(high - prev_close).abs(),
37+
(low - prev_close).abs()
38+
],
39+
axis=1
40+
).max(axis=1)
41+
42+
atr_sum = true_range.rolling(window=period).sum()
43+
highest_high = high.rolling(window=period).max()
44+
lowest_low = low.rolling(window=period).min()
45+
46+
price_range = (highest_high - lowest_low)
47+
chop = 100 * np.log10(atr_sum / price_range) / np.log10(period)
48+
chop = chop.replace([np.inf, -np.inf], np.nan)
49+
50+
return chop
51+
52+
@staticmethod
53+
def calculate_atr(df, period=14):
54+
"""Average True Range (ATR)"""
55+
high = df['high']
56+
low = df['low']
57+
close = df['close']
58+
59+
prev_close = close.shift(1)
60+
true_range = pd.concat(
61+
[
62+
(high - low),
63+
(high - prev_close).abs(),
64+
(low - prev_close).abs()
65+
],
66+
axis=1
67+
).max(axis=1)
68+
69+
atr = true_range.rolling(window=period).mean()
70+
return atr
71+
72+
@staticmethod
73+
def add_support_resistance(df, lookback=50):
74+
"""Ajoute des niveaux simples de support / résistance (rolling)"""
75+
df['support'] = df['low'].rolling(window=lookback, min_periods=lookback).min().shift(1)
76+
df['resistance'] = df['high'].rolling(window=lookback, min_periods=lookback).max().shift(1)
77+
return df
78+
79+
@staticmethod
80+
def add_all_indicators(df, chop_period=14, sr_lookback=50, atr_period=14):
2781
"""Ajoute tous les indicateurs au DataFrame"""
2882
# EMAs
2983
df['ema_20'] = TechnicalIndicators.calculate_ema(df, 20)
3084
df['ema_50'] = TechnicalIndicators.calculate_ema(df, 50)
3185
df['ema_200'] = TechnicalIndicators.calculate_ema(df, 200)
3286

87+
# Pente EMA200
88+
df['ema_200_slope'] = df['ema_200'].diff()
89+
df['ema_50_slope'] = df['ema_50'].diff()
90+
df['ema_200_slope_10'] = df['ema_200'].diff(10)
91+
df['ema_50_slope_10'] = df['ema_50'].diff(10)
92+
3393
# RSI
3494
df['rsi'] = TechnicalIndicators.calculate_rsi(df, 14)
95+
df['rsi_delta'] = df['rsi'].diff()
96+
97+
# Volume
98+
df['volume_sma_20'] = df['volume'].rolling(window=20).mean()
99+
100+
# Choppiness Index
101+
df['chop'] = TechnicalIndicators.calculate_choppiness(df, chop_period)
102+
103+
# ATR
104+
df['atr'] = TechnicalIndicators.calculate_atr(df, atr_period)
105+
106+
# Supports / résistances
107+
df = TechnicalIndicators.add_support_resistance(df, lookback=sr_lookback)
35108

36109
# Tendance simple (EMA 20 > 50)
37110
df['trend'] = np.where(df['ema_20'] > df['ema_50'], 'BULLISH', 'BEARISH')

src/notifier.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ def send_buy_signal(self, symbol, price, indicators):
115115
{"name": "📈 Tendance", "value": indicators.get('trend', 'N/A'), "inline": True}
116116
]
117117

118+
extra_fields = [
119+
("EMA 20", indicators.get('ema_20')),
120+
("EMA 50", indicators.get('ema_50')),
121+
("EMA 200", indicators.get('ema_200')),
122+
("CHOP", indicators.get('chop')),
123+
("Support", indicators.get('support')),
124+
("Résistance", indicators.get('resistance'))
125+
]
126+
127+
for name, value in extra_fields:
128+
if value not in (None, "N/A"):
129+
fields.append({"name": name, "value": f"{value}", "inline": True})
130+
118131
self.send_message(
119132
title=f"🟢 SIGNAL ACHAT - {symbol}" + (" [TEST]" if self.test_mode else ""),
120133
description="Conditions d'achat remplies !",
@@ -130,6 +143,19 @@ def send_sell_signal(self, symbol, price, indicators):
130143
{"name": "📉 Tendance", "value": indicators.get('trend', 'N/A'), "inline": True}
131144
]
132145

146+
extra_fields = [
147+
("EMA 20", indicators.get('ema_20')),
148+
("EMA 50", indicators.get('ema_50')),
149+
("EMA 200", indicators.get('ema_200')),
150+
("CHOP", indicators.get('chop')),
151+
("Support", indicators.get('support')),
152+
("Résistance", indicators.get('resistance'))
153+
]
154+
155+
for name, value in extra_fields:
156+
if value not in (None, "N/A"):
157+
fields.append({"name": name, "value": f"{value}", "inline": True})
158+
133159
self.send_message(
134160
title=f"🔴 SIGNAL VENTE - {symbol}" + (" [TEST]" if self.test_mode else ""),
135161
description="Conditions de vente remplies !",

0 commit comments

Comments
 (0)