Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
# API Authentication
# Générez une clé sécurisée avec: python -c "import secrets; print(secrets.token_urlsafe(32))"
# Format: key:name:role1:role2,...
# Exemple: abc123:admin:admin,def456:readonly:user
API_KEYS=your_api_key_here:admin:admin
# Ou utilisez une clé par défaut (moins sécurisé):
# DEFAULT_API_KEY=your_default_api_key_here
# 🔐 Configuration Telegram (Optionnel)
# Copiez ce fichier en .env et remplissez vos valeurs réelles
# ⚠️ NE COMMITEZ JAMAIS le fichier .env sur GitHub !

# Configuration Telegram
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
# Token du bot Telegram (obtenu via @BotFather)
TELEGRAM_BOT_TOKEN=votre_token_ici

# Paper Trading
# Chat ID Telegram (obtenu via @userinfobot ou @getidsbot)
# Pour les groupes/channels, utilisez un ID négatif (ex: -123456789)
TELEGRAM_CHAT_ID=votre_chat_id_ici

# Mode Paper Trading (true/false)
PAPER_TRADING_MODE=false

# Capital initial pour Paper Trading (USDT)
PAPER_TRADING_INITIAL_CAPITAL=1000.0

# Notification Types
# Types de notifications Telegram (true/false)
TELEGRAM_NOTIFY_POSITION_OPENED=true
TELEGRAM_NOTIFY_POSITION_CLOSED=true
# ... etc
TELEGRAM_NOTIFY_TP_ESCALIER=true
TELEGRAM_NOTIFY_EARLY_INVALIDATION=true
TELEGRAM_NOTIFY_ERROR=true
TELEGRAM_NOTIFY_RECONNECTION=true
TELEGRAM_NOTIFY_DAILY_SUMMARY=false
TELEGRAM_NOTIFY_RECOVERY_MODE=true
TELEGRAM_NOTIFY_SETUP_REJECTED=false

# Mode Debug (true/false)
DEBUG=false
9 changes: 8 additions & 1 deletion core/analytics_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,14 @@ def get_trades(
params.append(limit)

cursor.execute(query, params)
return [dict(row) for row in cursor.fetchall()]
trades = []
for row in cursor.fetchall():
trade = dict(row)
# 🔥 FIX: S'assurer que duration_seconds est présent si duration existe
if 'duration' in trade and trade.get('duration') is not None and 'duration_seconds' not in trade:
trade['duration_seconds'] = trade['duration']
trades.append(trade)
return trades

def clear_all_trades(self):
"""Vider tous les trades de la base de données (pour réinitialiser les stats au démarrage)"""
Expand Down
30 changes: 28 additions & 2 deletions core/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,10 +672,36 @@ async def analyze_pair(

if not orderbook_check['valid']:
required_str = '≥1.1' if best_setup['direction'] == 'LONG' else '≤0.95'
logger.warning(
f"⚠️ {symbol} - Setup {best_setup['direction']} rejeté : "
info_msg = (
f"ℹ️ {symbol} - Setup {best_setup['direction']} rejeté : "
f"Orderbook défavorable (ratio={orderbook_check['ratio']:.2f}, required={required_str})"
)
logger.info(info_msg)
# 🔥 FIX: Envoyer le log au frontend via websocket_manager (INFO au lieu de WARNING)
try:
from core.websocket_manager import get_websocket_manager
from datetime import datetime
import asyncio
ws_mgr = get_websocket_manager()
if ws_mgr:
try:
loop = asyncio.get_running_loop()
async def send_log():
# Format identique à add_log dans main.py
entry = {
'timestamp': datetime.now().strftime('%H:%M:%S'),
'level': 'INFO', # 🔥 FIX: INFO au lieu de WARNING
'message': f"ℹ️ Setup {best_setup['direction']} rejeté",
'detail': f"{symbol}: Orderbook défavorable (ratio={orderbook_check['ratio']:.2f}, required={required_str})",
'raw_message': f"Setup {best_setup['direction']} rejeté"
}
await ws_mgr.emit('log', entry)
loop.create_task(send_log())
except RuntimeError:
# Pas de loop en cours, ignorer (le log est déjà dans logger.info)
pass
except Exception as log_err:
logger.debug(f"Impossible d'envoyer log au frontend: {log_err}")
return None

# Bonus si orderbook très favorable
Expand Down
18 changes: 15 additions & 3 deletions core/analyzer/market_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ async def check_spread(client, symbol: str, spread_cache: Dict) -> Dict:
# Récupérer orderbook
orderbook = await client.fetch_order_book(symbol, limit=5)

best_bid = orderbook['bids'][0][0] if orderbook['bids'] else 0
best_ask = orderbook['asks'][0][0] if orderbook['asks'] else 0
# 🔥 FIX: Vérifier que orderbook n'est pas None et contient bids/asks
if orderbook is None:
# 🔥 FIX: Ne pas logger ici car WebSocketLogHandler capture déjà logger.error et envoie au frontend
# Le log sera envoyé automatiquement via WebSocketLogHandler
return {'valid': False, 'spread_pct': 999, 'max_allowed': 0.03, 'quality': 'ERROR'}

bids = orderbook.get('bids', []) if orderbook else []
asks = orderbook.get('asks', []) if orderbook else []

best_bid = bids[0][0] if bids and len(bids) > 0 and len(bids[0]) > 0 else 0
best_ask = asks[0][0] if asks and len(asks) > 0 and len(asks[0]) > 0 else 0

if best_bid == 0 or best_ask == 0:
return {'valid': False, 'spread_pct': 999, 'max_allowed': 0.03, 'quality': 'UNKNOWN'}
Expand Down Expand Up @@ -80,7 +89,10 @@ async def check_spread(client, symbol: str, spread_cache: Dict) -> Dict:
return result

except Exception as e:
logger.error(f"❌ Erreur check spread {symbol}: {e}")
error_msg = f"❌ Erreur check spread {symbol}: {e}"
# 🔥 FIX: logger.error est capturé par WebSocketLogHandler et envoyé au frontend automatiquement
# Pas besoin d'envoyer manuellement via websocket_manager.emit (cela créerait un doublon)
logger.error(error_msg)
return {'valid': False, 'spread_pct': 999, 'max_allowed': 0.03, 'quality': 'ERROR'}


Expand Down
25 changes: 23 additions & 2 deletions core/callbacks/position_check_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ async def _emit_position_update(position, current_price: float):

# 🔥 FIX: Émettre mise à jour au frontend avec toutes les infos
import json
from datetime import datetime
# 🔥 NOUVEAU: Ajouter opened_at pour le compte à rebours
opened_at = None
if hasattr(position, 'start_time') and position.start_time:
opened_at = datetime.fromtimestamp(position.start_time).isoformat()
elif hasattr(position, 'timestamp') and position.timestamp:
# Fallback: utiliser timestamp si start_time n'est pas disponible
if isinstance(position.timestamp, (int, float)):
opened_at = datetime.fromtimestamp(position.timestamp).isoformat()
else:
opened_at = position.timestamp

update_data = {
'symbol': position.symbol,
'direction': position.direction,
Expand All @@ -241,6 +253,7 @@ async def _emit_position_update(position, current_price: float):
'pnl': pnl,
'pnl_usdt': pnl_usdt,
'size': position.size,
'opened_at': opened_at, # 🔥 NOUVEAU: Ajouté pour le compte à rebours
'break_even_set': getattr(position, 'break_even_set', False),
'partial_tp_sold': getattr(position, 'partial_tp_sold', False),
'tp_sl_mode': TRADING_CONFIG.get('tp_sl_mode', 'FIXE'),
Expand Down Expand Up @@ -308,6 +321,13 @@ async def _emit_stats_update():
total_pnl_usdt = sum(t.get('net_pnl_usdt', t.get('pnl_usdt', 0)) for t in trades)
total_pnl_pct = sum(t.get('net_pnl_pct', t.get('pnl_pct', 0)) for t in trades)

# 🔥 DEBUG: Log pour vérifier les valeurs
if total > 0:
logger.debug(f"📊 Stats calculées: {total} trades, PnL USDT={total_pnl_usdt:.2f}, PnL %={total_pnl_pct:.2f}")
# Afficher les 3 premiers trades pour debug
for i, t in enumerate(trades[:3]):
logger.debug(f" Trade {i+1}: net_pnl_usdt={t.get('net_pnl_usdt', 'N/A')}, net_pnl_pct={t.get('net_pnl_pct', 'N/A')}")

# 🔥 FIX: Trouver best/worst trade avec net_pnl_usdt
best_trade = max(trades, key=lambda t: t.get('net_pnl_usdt', t.get('pnl_usdt', 0)), default=None)
worst_trade = min(trades, key=lambda t: t.get('net_pnl_usdt', t.get('pnl_usdt', 0)), default=None)
Expand All @@ -320,8 +340,9 @@ async def _emit_stats_update():
'total_trades': total,
'wins': wins,
'losses': losses,
'total_pnl_usdt': round(total_pnl_usdt, 2),
'total_pnl_pct': round(total_pnl_pct, 2),
# 🔥 FIX: Arrondir à 4 décimales pour éviter de perdre les petites valeurs (0.00)
'total_pnl_usdt': round(total_pnl_usdt, 4),
'total_pnl_pct': round(total_pnl_pct, 4),
'best_trade': best_trade,
'worst_trade': worst_trade,
'avg_trade_duration': round(avg_duration, 2)
Expand Down
16 changes: 11 additions & 5 deletions core/callbacks/scanner_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,18 @@ async def _scan_top_pairs():
if _app_state and _app_state.get('top_pairs'):
for pair in _app_state['top_pairs']:
if pair.get('symbol') == symbol:
# 🔥 FIX: Utiliser les bonnes clés depuis le scanner (spread, bookDepth, balanceScore, bidVol, askVol)
spread_value = pair.get('spread', 0)
# Vérifier si spread est NaN ou invalide
if isinstance(spread_value, float) and (spread_value != spread_value or spread_value == float('nan')):
spread_value = 0

scalability_data = {
'spread_pct': pair.get('spread_pct', 0),
'depth': pair.get('depth', 0),
'balance': pair.get('balance', 1.0),
'bid_vol': pair.get('bid_vol'),
'ask_vol': pair.get('ask_vol')
'spread_pct': spread_value,
'depth': pair.get('bookDepth', 0),
'balance': pair.get('balanceScore', 1.0),
'bid_vol': pair.get('bidVol'),
'ask_vol': pair.get('askVol')
}
break

Expand Down
Loading