import os import sys import hashlib import hmac import time import json import math import urllib.parse import requests import pandas as pd import pandas_ta as ta from datetime import datetime, time as dtime from threading import Thread from flask import Flask, render_template_string, jsonify, request # ========================================== # CONFIGURATION INITIALE ET CLÉS API PIONEX # ========================================== PIONEX_API_KEY = "5fEmm9xYKbbJnrW1vPo1yPupygjKRBTfWHqnEJz6LAkiF5nLVQ8pavXYz9fETK8GqJ" PIONEX_SECRET_KEY = "D8wBdPpL7VPfMEdOd2XkGdyCRV2BoHb7yOpI0QQAaZ6sdYnKZxPmkQtKJARZ8iHc" BASE_URL = "https://api.pionex.com" OLLAMA_URL = "http://localhost:11434/api/generate" OLLAMA_MODEL = "dolphin-llama3" time_offset_ms = 0 bot_config = { "symbol": "SOL_USDT", "amount_usdt": 12.0, # Capital engagé en USDT "take_profit_pct": 0.0100, # Max TP: +1.00% BRUT (+0.80% NET) "min_tp_pct": 0.0060, # Min TP dynamique: +0.60% BRUT (+0.40% NET) "stop_loss_pct": 0.0050, # -0.50% STOP-LOSS de base (Ajusté par ATR) "timeframe": "5M", # Unité de temps des indicateurs "rsi_trigger": 52.0, # Seuil RSI haussier ajusté "is_running": False } bot_status = { "price": 0.0, "rsi": 0.0, "stoch_k": 0.0, "stoch_d": 0.0, "bbl": 0.0, "bbu": 0.0, "ema9": 0.0, "ema21": 0.0, "ema200": 0.0, "atr": 0.0, "trend": "NEUTRE", "decision": "HOLD", "in_position": False, "buy_price": 0.0, "buy_time": "--", "trade_duration": "--", "crypto_qty": 0.0, "amount_usdt": 12.0, "target_price": 0.0, "stop_loss_price": 0.0, "max_price_seen": 0.0, "is_be_active": False, "current_profit_pct": 0.0, "current_profit_usdt": 0.0, "total_pnl_usdt": -0.30, "gauge_percent": 0.0, "pos_count": 0, "total_trades": 11, "winning_trades": 5, "losing_trades": 6, "win_rate": 45.5, "last_update": "En attente...", "logs": [] } app = Flask(__name__) def add_log(msg): timestamp = time.strftime('%H:%M:%S') entry = f"[{timestamp}] {msg}" print(entry) bot_status["logs"].insert(0, entry) if len(bot_status["logs"]) > 70: bot_status["logs"].pop() def sync_pionex_time(): global time_offset_ms try: os.system("sudo timedatectl set-ntp true > /dev/null 2>&1") os.system("sudo hwclock --systohc > /dev/null 2>&1") except Exception: pass try: res = requests.get(f"{BASE_URL}/api/v1/common/timestamp", timeout=3).json() if res.get("result"): server_time = res["data"]["timestamp"] local_time = int(time.time() * 1000) time_offset_ms = server_time - local_time add_log(f"Horloge synchronisée avec Pionex (Décalage: {time_offset_ms} ms)") else: time_offset_ms = 0 except Exception as e: add_log(f"⚠️ Erreur sync temps API : {e}") time_offset_ms = 0 def get_pionex_timestamp(): return int(time.time() * 1000) + time_offset_ms def is_forbidden_trading_time(): """ Pause de sécurité à l'aube (03h30 - 06h00 Guadeloupe UTC-4) """ now = datetime.now().time() return dtime(3, 30) <= now <= dtime(6, 0) def get_pionex_live_price(symbol): try: url = f"{BASE_URL}/api/v1/market/trades" res = requests.get(url, params={"symbol": symbol, "limit": 1}, timeout=2).json() if res.get("result") and res.get("data") and res["data"].get("trades"): return float(res["data"]["trades"][0]["price"]) except Exception: pass try: url_ticker = f"{BASE_URL}/api/v1/market/tickers" res_t = requests.get(url_ticker, params={"symbol": symbol}, timeout=2).json() if res_t.get("result") and res_t.get("data") and res_t["data"].get("tickers"): return float(res_t["data"]["tickers"][0]["close"]) except Exception: pass return None def get_klines(symbol, interval="5M", limit=250): path = "/api/v1/market/klines" url = f"{BASE_URL}{path}" params = {"symbol": symbol, "interval": interval, "limit": limit} try: res = requests.get(url, params=params, timeout=3).json() if res.get("result"): data = res["data"]["klines"] df = pd.DataFrame(data, columns=["time", "open", "high", "low", "close", "volume"]) df["time"] = df["time"].astype(int) df = df.sort_values("time").reset_index(drop=True) df["open"] = df["open"].astype(float) df["close"] = df["close"].astype(float) df["high"] = df["high"].astype(float) df["low"] = df["low"].astype(float) df["volume"] = df["volume"].astype(float) return df else: add_log(f"⚠️ Erreur API Klines : {res.get('message', 'Paramètre invalide')}") except Exception as e: add_log(f"Erreur Réseau API Pionex : {e}") return None def format_crypto_size(symbol, quantity): """ CORRECTION : SOL/USDT nécessite une précision à 2 décimales (step 0.01) sur Pionex """ if "PEPE" in symbol: return str(int(quantity)) elif "DOGE" in symbol or "XRP" in symbol or "SOL" in symbol: return f"{math.floor(quantity * 100) / 100:.2f}" else: return f"{math.floor(quantity * 1000) / 1000:.3f}" def format_price(symbol, price): if "PEPE" in symbol: return f"{price:.8f}" elif "DOGE" in symbol: return f"{price:.5f}" else: return f"{price:.2f}" def place_order(symbol, side, quantity_or_amount, order_type="MARKET", price=None): path = "/api/v1/trade/order" timestamp = get_pionex_timestamp() params = {"timestamp": timestamp} query_string = urllib.parse.urlencode(params) body_data = { "side": side.upper(), "symbol": symbol, "type": order_type.upper() } if order_type.upper() == "LIMIT" and price is not None: body_data["price"] = format_price(symbol, price) body_data["size"] = format_crypto_size(symbol, quantity_or_amount) else: if side.upper() == "BUY": body_data["amount"] = str(round(quantity_or_amount, 2)) else: body_data["size"] = format_crypto_size(symbol, quantity_or_amount) body_str = json.dumps(body_data, sort_keys=True, separators=(',', ':')) message = f"POST{path}?{query_string}{body_str}" signature = hmac.new( PIONEX_SECRET_KEY.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).hexdigest() url = f"{BASE_URL}{path}?{query_string}" headers = { "PIONEX-KEY": PIONEX_API_KEY, "PIONEX-SIGNATURE": signature, "Content-Type": "application/json" } try: response = requests.post(url, data=body_str, headers=headers, timeout=3) return response.json() except Exception as e: add_log(f"Erreur Envoi Ordre ({side}) : {e}") return {"result": False} def calculate_indicators(df): df['RSI'] = ta.rsi(df['close'], length=14) stoch_rsi = ta.stochrsi(df['close'], length=14, rsi_length=14, k=3, d=3) if stoch_rsi is not None and not stoch_rsi.empty: df['STOCHRSIk'] = stoch_rsi.iloc[:, 0] df['STOCHRSId'] = stoch_rsi.iloc[:, 1] else: df['STOCHRSIk'], df['STOCHRSId'] = 50.0, 50.0 df['EMA9'] = ta.ema(df['close'], length=9) df['EMA21'] = ta.ema(df['close'], length=21) df['EMA200'] = ta.ema(df['close'], length=200) df['ATR'] = ta.atr(df['high'], df['low'], df['close'], length=14) bb = ta.bbands(df['close'], length=20, std=2) if bb is not None and not bb.empty: df['BBL'] = bb.iloc[:, 0] df['BBU'] = bb.iloc[:, 2] else: df['BBL'], df['BBU'] = df['close'], df['close'] return df def ask_local_ai_fast(symbol, price, rsi, stoch_k, is_uptrend): prompt = f"""Scalping pour {symbol} : Prix Spot: {price} | RSI(14): {rsi:.1f} | StochRSI: {stoch_k:.1f} | Tendance Haussière Validée: {is_uptrend} Réponds uniquement "BUY" si le rebond est confirmé, sinon "HOLD".""" try: response = requests.post(OLLAMA_URL, json={ "model": OLLAMA_MODEL, "prompt": prompt, "stream": False }, timeout=2.0) if response.status_code == 200: raw_resp = response.json().get("response", "").strip().upper() return "BUY" if "BUY" in raw_resp else "HOLD" return "BUY" if is_uptrend else "HOLD" except Exception: return "BUY" if is_uptrend else "HOLD" def run_trader(): global bot_status sync_pionex_time() in_position = False buy_price = 0.0 trade_start_ts = 0 crypto_qty = 0.0 active_sl_price = 0.0 max_price_seen = 0.0 is_be_active = False while True: loop_start = time.time() try: if not bot_config["is_running"]: bot_status["decision"] = "ARRÊTÉ" bot_status["gauge_percent"] = 0.0 bot_status["last_update"] = time.strftime('%H:%M:%S') time.sleep(1) continue symbol = bot_config["symbol"] amount = bot_config["amount_usdt"] max_tp_pct = bot_config["take_profit_pct"] # 1.00% min_tp_pct = bot_config["min_tp_pct"] # 0.60% tf = bot_config["timeframe"] live_price = get_pionex_live_price(symbol) df = get_klines(symbol, interval=tf, limit=250) if df is not None and len(df) >= 200: df = calculate_indicators(df) last = df.iloc[-1] prev = df.iloc[-2] price = live_price if live_price is not None else float(last['close']) rsi = float(last['RSI']) if pd.notna(last['RSI']) else 50.0 prev_rsi = float(prev['RSI']) if pd.notna(prev['RSI']) else 50.0 stoch_k = float(last['STOCHRSIk']) if pd.notna(last['STOCHRSIk']) else 50.0 stoch_d = float(last['STOCHRSId']) if pd.notna(last['STOCHRSId']) else 50.0 prev_stoch_k = float(prev['STOCHRSIk']) if pd.notna(prev['STOCHRSIk']) else 50.0 prev_stoch_d = float(prev['STOCHRSId']) if pd.notna(prev['STOCHRSId']) else 50.0 bbl = float(last['BBL']) if pd.notna(last['BBL']) else price bbu = float(last['BBU']) if pd.notna(last['BBU']) else price ema9 = float(last['EMA9']) if pd.notna(last['EMA9']) else price ema21 = float(last['EMA21']) if pd.notna(last['EMA21']) else price ema200 = float(last['EMA200']) if pd.notna(last['EMA200']) else price atr = float(last['ATR']) if pd.notna(last['ATR']) else (price * 0.005) calculated_sl_pct = max(0.0040, min(0.0080, (atr * 1.5) / price)) bot_status["price"] = price bot_status["rsi"] = rsi bot_status["stoch_k"] = stoch_k bot_status["stoch_d"] = stoch_d bot_status["bbl"] = bbl bot_status["bbu"] = bbu bot_status["ema9"] = ema9 bot_status["ema21"] = ema21 bot_status["ema200"] = ema200 bot_status["atr"] = atr bot_status["last_update"] = time.strftime('%H:%M:%S') # CONDITION TENDANCE SOUPLE & FLUIDE : Prix au-dessus de l'EMA 200 is_uptrend_macro = price > ema200 is_green_candle = last['close'] > last['open'] is_price_rising = price > float(prev['close']) rsi_rebounding = rsi > prev_rsi stoch_cross = (stoch_k > stoch_d) and (prev_stoch_k <= prev_stoch_d) is_reversal_confirmed = (is_green_candle or is_price_rising) and rsi_rebounding bot_status["trend"] = "HAUSSIÈRE (MACRO) 🟢" if is_uptrend_macro else "NEUTRE / BAISSIÈRE 🔴" if not in_position: bot_status["gauge_percent"] = 0.0 bot_status["current_profit_pct"] = 0.0 bot_status["current_profit_usdt"] = 0.0 bot_status["trade_duration"] = "--" bot_status["is_be_active"] = False # SIGNAL FLUIDE : Tendance haussière + RSI < 52 + Croisement StochRSI + Rebond algo_signal = is_uptrend_macro and (rsi < bot_config["rsi_trigger"]) and stoch_cross and is_reversal_confirmed if is_forbidden_trading_time(): bot_status["decision"] = "PAUSE RISQUE (03H30-06H00)" add_log(f"[{symbol}] ⏸️ Pause créneau risque à l'aube (UTC-4) : Achat ignoré.") else: decision = "HOLD" if algo_signal: ai_decision = ask_local_ai_fast(symbol, price, rsi, stoch_k, is_reversal_confirmed) if ai_decision == "BUY": decision = "BUY" bot_status["decision"] = decision bot_status["in_position"] = False add_log(f"[{symbol}] Prix: ${price:.2f} | RSI: {rsi:.1f} | StochK: {stoch_k:.1f} | Rebond: {is_reversal_confirmed} -> {decision}") if decision == "BUY": order_res = place_order(symbol, "BUY", amount, order_type="MARKET") if order_res.get("result"): in_position = True buy_price = price trade_start_ts = time.time() max_price_seen = buy_price crypto_qty = amount / buy_price is_be_active = False active_sl_price = buy_price * (1 - calculated_sl_pct) target_price = buy_price * (1 + max_tp_pct) bot_status["buy_price"] = buy_price bot_status["buy_time"] = time.strftime('%H:%M:%S') bot_status["crypto_qty"] = crypto_qty bot_status["amount_usdt"] = amount bot_status["target_price"] = target_price bot_status["stop_loss_price"] = active_sl_price bot_status["max_price_seen"] = max_price_seen bot_status["is_be_active"] = False bot_status["pos_count"] = 1 bot_status["in_position"] = True qte_str = format_crypto_size(symbol, crypto_qty) add_log(f"✅ ACHAT Marché à ${buy_price:.2f} (Qté exacte: {qte_str} {symbol.split('_')[0]}). TP: ${target_price:.2f} | SL: ${active_sl_price:.2f}") else: add_log(f"❌ Échec achat : {order_res}") else: bot_status["in_position"] = True if price > max_price_seen: max_price_seen = price dur_sec = int(time.time() - trade_start_ts) mins, secs = divmod(dur_sec, 60) bot_status["trade_duration"] = f"{mins:02d}m {secs:02d}s" profit_brut = ((price - buy_price) / buy_price) * 100.0 profit_net = profit_brut - 0.20 # Frais Achat + Vente (0.10% + 0.10%) profit_usdt = (price - buy_price) * crypto_qty - (amount * 0.002) # VERROUILLAGE BREAK-EVEN Dès +0.35% Brut if profit_brut >= 0.35: break_even_price = buy_price * 1.0020 # Couvre les frais à 100% if active_sl_price < break_even_price: active_sl_price = break_even_price is_be_active = True add_log(f"🛡️ POSITION SÉCURISÉE AU BREAK-EVEN (+0.20% inclus frais). Trade désormais sans risque !") bot_status["target_price"] = buy_price * (1 + max_tp_pct) bot_status["stop_loss_price"] = active_sl_price bot_status["max_price_seen"] = max_price_seen bot_status["is_be_active"] = is_be_active bot_status["current_profit_pct"] = profit_net bot_status["current_profit_usdt"] = profit_usdt total_range = (buy_price * (1 + max_tp_pct)) - active_sl_price current_dist = price - active_sl_price gauge = max(0.0, min(100.0, (current_dist / total_range) * 100.0)) if total_range > 0 else 50.0 bot_status["gauge_percent"] = round(gauge, 1) # DECISION DE VENTE is_max_tp_reached = price >= (buy_price * (1 + max_tp_pct)) is_min_tp_reached = profit_brut >= (min_tp_pct * 100) # >= +0.60% smart_tp_condition = is_min_tp_reached and (price >= bbu or rsi >= 68.0 or price <= (max_price_seen * 0.9985)) is_sl_triggered = price <= active_sl_price if is_max_tp_reached or smart_tp_condition: bot_status["decision"] = "TAKE PROFIT" elif is_sl_triggered: bot_status["decision"] = "STOP LOSS / BE" else: bot_status["decision"] = "HOLD" add_log(f"Position active | Prix Direct: ${price:.2f} | PnL Net: {profit_net:+.2f}% (${profit_usdt:+.2f}) | Prog: {gauge:.1f}%") # VENTE TP if is_max_tp_reached or smart_tp_condition: tp_type = "MAX (+1.00%)" if is_max_tp_reached else f"SMART (+{profit_net:.2f}% NET)" qte_str = format_crypto_size(symbol, crypto_qty) add_log(f"🎯 TAKE PROFIT {tp_type} ATTEINT (${price:.2f}) ! Vente de {qte_str} {symbol.split('_')[0]}...") order_res = place_order(symbol, "SELL", crypto_qty, order_type="MARKET") if order_res.get("result"): bot_status["winning_trades"] += 1 bot_status["total_trades"] += 1 bot_status["total_pnl_usdt"] += profit_usdt bot_status["win_rate"] = (bot_status["winning_trades"] / bot_status["total_trades"]) * 100.0 add_log(f"✅ PROFIT ENCAISSÉ ! Net: {profit_net:+.2f}% (${profit_usdt:+.2f} USDT)") in_position = False buy_price = 0.0 crypto_qty = 0.0 max_price_seen = 0.0 bot_status["in_position"] = False bot_status["pos_count"] = 0 bot_status["gauge_percent"] = 100.0 else: add_log(f"❌ Échec Vente TP : {order_res}") # VENTE SL / BE elif is_sl_triggered: qte_str = format_crypto_size(symbol, crypto_qty) is_be = active_sl_price >= buy_price sl_type = "BREAK-EVEN (FRAIS COUVERTS)" if is_be else "STOP LOSS DÉCLENCHÉ" add_log(f"⚠️ {sl_type} (${price:.2f}) ! Vente de sécurité...") order_res = place_order(symbol, "SELL", crypto_qty, order_type="MARKET") if order_res.get("result"): if not is_be: bot_status["losing_trades"] += 1 else: bot_status["winning_trades"] += 1 bot_status["total_trades"] += 1 bot_status["total_pnl_usdt"] += profit_usdt bot_status["win_rate"] = (bot_status["winning_trades"] / bot_status["total_trades"]) * 100.0 add_log(f"🛑 CLÔTURE EXÉCUTÉE. PnL Net: {profit_net:+.2f}% (${profit_usdt:+.2f} USDT)") in_position = False buy_price = 0.0 crypto_qty = 0.0 max_price_seen = 0.0 bot_status["in_position"] = False bot_status["pos_count"] = 0 bot_status["gauge_percent"] = 0.0 else: add_log(f"❌ Échec Vente SL : {order_res}") except Exception as e: add_log(f"Erreur Boucle Trading : {e}") elapsed = time.time() - loop_start time.sleep(max(0, 1.0 - elapsed)) # ========================================== # INTERFACE WEB HTML / CSS / JS (FLASK) # ========================================== HTML_TEMPLATE = """