Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -84,60 +84,45 @@ HF_TOKEN = os.environ.get("HF_TOKEN")
|
|
| 84 |
|
| 85 |
def init_db():
|
| 86 |
reset_needed = False
|
| 87 |
-
|
| 88 |
if os.path.exists(DB_NAME):
|
| 89 |
try:
|
| 90 |
conn = sqlite3.connect(DB_NAME)
|
| 91 |
cursor = conn.execute("PRAGMA table_info(signals)")
|
| 92 |
columns = [col[1] for col in cursor.fetchall()]
|
| 93 |
conn.close()
|
| 94 |
-
|
|
|
|
| 95 |
reset_needed = True
|
| 96 |
except Exception as e:
|
| 97 |
print(f"⚠️ Erreur check DB: {e}")
|
| 98 |
reset_needed = True
|
| 99 |
|
| 100 |
if reset_needed:
|
| 101 |
-
print("⚠️ Structure obsolète
|
| 102 |
-
|
| 103 |
-
if os.path.exists(DB_NAME):
|
| 104 |
-
os.remove(DB_NAME)
|
| 105 |
-
print("✅ Ancienne base supprimée.")
|
| 106 |
-
except Exception as e:
|
| 107 |
-
print(f"❌ Impossible de supprimer la DB: {e}")
|
| 108 |
|
| 109 |
try:
|
| 110 |
with sqlite3.connect(DB_NAME) as conn:
|
|
|
|
| 111 |
conn.execute('''CREATE TABLE IF NOT EXISTS signals (
|
| 112 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 113 |
date TEXT, symbol TEXT, timeframe TEXT, direction TEXT,
|
| 114 |
prob REAL, price REAL, tp REAL, sl REAL, status TEXT,
|
| 115 |
-
regime INTEGER, prob_time REAL, prob_ml REAL, prob_lstm REAL, prob_sent REAL
|
|
|
|
| 116 |
|
| 117 |
-
# 🎮 LA FICHE DE PERSONNAGE (STATS RPG)
|
| 118 |
conn.execute('''CREATE TABLE IF NOT EXISTS agent_logic (
|
| 119 |
-
timeframe TEXT PRIMARY KEY,
|
| 120 |
-
|
| 121 |
-
sl_mult REAL,
|
| 122 |
-
score REAL,
|
| 123 |
-
last_pnl REAL,
|
| 124 |
-
min_prob REAL,
|
| 125 |
-
min_tp_dist REAL)''')
|
| 126 |
|
| 127 |
cursor = conn.cursor()
|
| 128 |
cursor.execute("SELECT COUNT(*) FROM agent_logic")
|
| 129 |
if cursor.fetchone()[0] == 0:
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
('15m', 2.0, 1.2, 0, 0, 0.55, 0.004),
|
| 134 |
-
('1h', 3.0, 1.5, 0, 0, 0.52, 0.005),
|
| 135 |
-
('4h', 4.5, 2.0, 0, 0, 0.50, 0.008),
|
| 136 |
-
('1d', 6.0, 2.5, 0, 0, 0.50, 0.01)
|
| 137 |
-
]
|
| 138 |
conn.executemany("INSERT INTO agent_logic VALUES (?, ?, ?, ?, ?, ?, ?)", defaults)
|
| 139 |
-
|
| 140 |
-
print("✅ Base de données V10 Synchronisée.")
|
| 141 |
except Exception as e:
|
| 142 |
print(f"❌ Erreur critique création DB: {e}")
|
| 143 |
|
|
@@ -184,35 +169,21 @@ try:
|
|
| 184 |
except Exception as e: print(f"⚠️ Erreur IA : {e}")
|
| 185 |
|
| 186 |
# --- 📊 FEATURES ENGINE ---
|
|
|
|
| 187 |
def prepare_features_sync(symbol, timeframe='1h', limit_bars=600):
|
| 188 |
-
"""Version Synchrone avec Bouclier Anti-Blocage Kucoin"""
|
| 189 |
try:
|
| 190 |
now = datetime.now().timestamp()
|
| 191 |
cache_key = f"{symbol}_{timeframe}"
|
| 192 |
-
|
| 193 |
-
if cache_key in market_cache and cache_key in last_fetch_time and now - last_fetch_time[cache_key] < CACHE_DURATION:
|
| 194 |
df = market_cache[cache_key].copy()
|
| 195 |
else:
|
| 196 |
-
bars =
|
| 197 |
-
for attempt in range(3):
|
| 198 |
-
try:
|
| 199 |
-
bars = exchange_sync.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit_bars)
|
| 200 |
-
break
|
| 201 |
-
except Exception as e:
|
| 202 |
-
print(f"⚠️ Kucoin bloque {symbol} (Essai {attempt+1}/3). Attente 2s...")
|
| 203 |
-
time.sleep(2)
|
| 204 |
-
|
| 205 |
-
if not bars:
|
| 206 |
-
print(f"❌ Échec total de la récupération pour {symbol}.")
|
| 207 |
-
return pd.DataFrame()
|
| 208 |
-
|
| 209 |
df = pd.DataFrame(bars, columns=['ts', 'open', 'high', 'low', 'close', 'vol'])
|
| 210 |
-
market_cache[cache_key] = df.copy()
|
| 211 |
-
last_fetch_time[cache_key] = now
|
| 212 |
|
| 213 |
-
if len(df) < 250:
|
| 214 |
-
return pd.DataFrame()
|
| 215 |
|
|
|
|
| 216 |
df["RSI"] = get_rsi(df["close"])
|
| 217 |
df["EMA50"] = get_ema(df["close"], 50)
|
| 218 |
df["EMA200"] = get_ema(df["close"], 200)
|
|
@@ -221,8 +192,8 @@ def prepare_features_sync(symbol, timeframe='1h', limit_bars=600):
|
|
| 221 |
df["EMA200_slope"] = (df["EMA200"] / df["EMA200"].shift(10)) - 1
|
| 222 |
df["Drawdown"] = (df["close"] / df["close"].rolling(14).max()) - 1
|
| 223 |
|
| 224 |
-
|
| 225 |
-
df["Low_24h"] = df["low"].rolling(24).min()
|
| 226 |
df["Dist_High_24h"] = (df["High_24h"] - df["close"]) / df["close"]
|
| 227 |
df["Dist_Low_24h"] = (df["close"] - df["Low_24h"]) / df["close"]
|
| 228 |
df["EMA_dist"] = (df["close"] - df["EMA50"]) / df["EMA50"]
|
|
@@ -230,41 +201,53 @@ def prepare_features_sync(symbol, timeframe='1h', limit_bars=600):
|
|
| 230 |
df["ATR_ratio"] = df["ATR"] / df["close"]
|
| 231 |
df["VOL_ratio"] = df["vol"] / df["vol"].rolling(24).mean()
|
| 232 |
|
| 233 |
-
|
| 234 |
-
df[
|
| 235 |
-
df[
|
| 236 |
-
df[
|
| 237 |
-
df['RSI_lag2'] = df["RSI"].shift(2)
|
| 238 |
-
df['VOL_RATIO'] = df['vol'] / df['vol'].rolling(20).mean()
|
| 239 |
-
df['vol_lag1'] = df['vol'].shift(1)
|
| 240 |
-
df["RSI_Macro"] = df["RSI"]
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
df[
|
| 245 |
-
df["Sweep_High"] = ((df["high"] > prev_high) & (df["close"] < prev_high)).astype(int)
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
|
|
|
|
|
|
| 253 |
|
| 254 |
|
| 255 |
-
# --- 🎯 API PRÉDICTION AVEC GAMEPLAY RPG ---
|
| 256 |
async def predict_signal(symbol, timeframe="1h"):
|
| 257 |
try:
|
| 258 |
memory_guard()
|
| 259 |
symbol = str(symbol).strip().upper()
|
| 260 |
if "/USDT" not in symbol: symbol += "/USDT"
|
| 261 |
|
|
|
|
| 262 |
df = prepare_features_sync(symbol, timeframe)
|
| 263 |
if df.empty: return {"status": "error", "message": f"Data insuffisante pour {timeframe}"}
|
| 264 |
|
| 265 |
last_row = df.iloc[[-1]]
|
| 266 |
prix, atr = float(last_row['close'].iloc[0]), float(last_row['ATR'].iloc[0])
|
| 267 |
|
|
|
|
| 268 |
regime_scaled = regime_scaler.transform(last_row[["ATR_pct", "EMA200_slope", "Drawdown", "RSI_Macro"]])
|
| 269 |
regime_pred = int(regime_model.predict(regime_scaled)[0])
|
| 270 |
|
|
@@ -280,12 +263,17 @@ async def predict_signal(symbol, timeframe="1h"):
|
|
| 280 |
|
| 281 |
p_sent = await get_crypto_sentiment(symbol)
|
| 282 |
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
final_p = (time_prob * wt) + (ml_prob * wm) + (lstm_prob * wl) + (p_sent * ws)
|
| 288 |
|
|
|
|
| 289 |
sweep_low, sweep_high = int(last_row["Sweep_Low"].iloc[0]), int(last_row["Sweep_High"].iloc[0])
|
| 290 |
smc_status = "AUCUN"
|
| 291 |
if sweep_low == 1:
|
|
@@ -295,7 +283,7 @@ async def predict_signal(symbol, timeframe="1h"):
|
|
| 295 |
final_p = max(0.05, final_p - 0.20)
|
| 296 |
smc_status = "SHORT SWEEP 🐋"
|
| 297 |
|
| 298 |
-
#
|
| 299 |
with sqlite3.connect(DB_NAME) as conn:
|
| 300 |
res = conn.execute("SELECT tp_mult, sl_mult, min_prob, min_tp_dist FROM agent_logic WHERE timeframe = ?", (timeframe,)).fetchone()
|
| 301 |
tp_m, sl_m, agent_min_prob, agent_min_tp_dist = res if res else (3.0, 1.5, 0.55, 0.004)
|
|
@@ -303,66 +291,66 @@ async def predict_signal(symbol, timeframe="1h"):
|
|
| 303 |
tp = prix + (atr * tp_m) if final_p > 0.5 else prix - (atr * tp_m)
|
| 304 |
sl = prix - (atr * sl_m) if final_p > 0.5 else prix + (atr * sl_m)
|
| 305 |
|
|
|
|
| 306 |
strength = abs(final_p - 0.5) * 2
|
| 307 |
conf_val = max(0, min(1, 1 - np.std([time_prob, ml_prob, lstm_prob, p_sent])))
|
| 308 |
-
|
| 309 |
score_base = (strength * 45) + (conf_val * 40)
|
| 310 |
regime_bonus = 15 if regime_pred in [0, 1] else 5
|
| 311 |
composite_score = max(0, min(100, score_base + regime_bonus))
|
| 312 |
|
| 313 |
risk_pct = max(0.2, min(2.5, strength * 5.0))
|
| 314 |
-
|
| 315 |
-
risk_usd = CAPITAL_VIRTUEL * (risk_pct / 100)
|
| 316 |
gain_estime_usd = risk_usd * (tp_m / sl_m)
|
| 317 |
|
| 318 |
# ==========================================
|
| 319 |
-
# 🛑 LE VETO DYNAMIQUE (
|
| 320 |
# ==========================================
|
| 321 |
veto = False
|
| 322 |
veto_reason = ""
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
veto_reason = f"Confiance ({round(final_p, 2)}). Requis: > {round(agent_min_prob, 2)}"
|
| 329 |
|
| 330 |
-
#
|
|
|
|
|
|
|
|
|
|
| 331 |
elif timeframe in ["5m", "15m"] and smc_status == "AUCUN":
|
| 332 |
-
veto = True
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
veto = True
|
| 338 |
-
veto_reason = f"TP trop proche ({round(distance_tp_pct*100, 2)}%). Requis: > {round(agent_min_tp_dist*100, 2)}%"
|
| 339 |
-
|
| 340 |
-
# F5: Filtre Macro
|
| 341 |
elif symbol != "BTC/USDT" and regime_pred == 2 and final_p > 0.5:
|
| 342 |
-
veto = True
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
if veto:
|
| 355 |
-
print(f"🛑 [VETO
|
| 356 |
-
return {
|
| 357 |
-
"symbol": symbol, "timeframe": timeframe, "status": "veto",
|
| 358 |
-
"message": veto_reason, "price": prix, "final_score": round(final_p, 4)
|
| 359 |
-
}
|
| 360 |
|
| 361 |
-
#
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
final_p, prix, tp, sl, 'EN_COURS', regime_pred, time_prob, ml_prob, lstm_prob, p_sent)
|
| 366 |
await save_to_db(db_task)
|
| 367 |
|
| 368 |
print(f"✅ [VALIDÉ] {symbol} [{timeframe}] : Ordre envoyé au Juge.")
|
|
@@ -376,7 +364,6 @@ async def predict_signal(symbol, timeframe="1h"):
|
|
| 376 |
"probs": {"xgb": round(time_prob, 3), "rf": round(ml_prob, 3), "lstm": round(lstm_prob, 3), "sent": round(p_sent, 3)},
|
| 377 |
"weights": {"w_xgb": round(wt, 2), "w_rf": round(wm, 2), "w_lstm": round(wl, 2), "w_sent": round(ws, 2)}
|
| 378 |
}
|
| 379 |
-
|
| 380 |
except Exception as e:
|
| 381 |
return {"status": "error", "message": str(e)}
|
| 382 |
|
|
@@ -469,10 +456,13 @@ def run_judge_api():
|
|
| 469 |
conn = sqlite3.connect(DB_NAME)
|
| 470 |
conn.row_factory = sqlite3.Row
|
| 471 |
cursor = conn.cursor()
|
|
|
|
|
|
|
| 472 |
cursor.execute("SELECT * FROM signals WHERE status = 'EN_COURS'")
|
| 473 |
trades = cursor.fetchall()
|
| 474 |
|
| 475 |
-
if not trades:
|
|
|
|
| 476 |
|
| 477 |
closed_trades = []
|
| 478 |
for t in trades:
|
|
@@ -480,53 +470,78 @@ def run_judge_api():
|
|
| 480 |
ticker = exchange_sync.fetch_ticker(t['symbol'])
|
| 481 |
current_price = ticker['last']
|
| 482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
outcome = None
|
| 484 |
reward = 0
|
| 485 |
|
| 486 |
if t['direction'] == 'HAUSSIER':
|
| 487 |
-
if current_price >= t['tp']:
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
else:
|
| 490 |
-
if current_price <= t['tp']:
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
if outcome:
|
| 494 |
-
#
|
| 495 |
-
# 💰 CALCUL DU PNL (PROFIT & LOSS)
|
| 496 |
-
# Capital: 10 000$ | Risque: 1% (100$)
|
| 497 |
-
# ==========================================
|
| 498 |
risque_usd = 100.0
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
# Ratio Risk/Reward
|
| 503 |
-
rr_ratio = distance_tp / distance_sl if distance_sl > 0 else 1
|
| 504 |
|
| 505 |
if "GAGNÉ" in outcome:
|
| 506 |
pnl_dollars = risque_usd * rr_ratio
|
|
|
|
|
|
|
| 507 |
else:
|
| 508 |
pnl_dollars = -risque_usd
|
| 509 |
|
| 510 |
-
# 🎮
|
| 511 |
cursor.execute("SELECT tp_mult, sl_mult, score, min_prob, min_tp_dist FROM agent_logic WHERE timeframe = ?", (t['timeframe'],))
|
| 512 |
row = cursor.fetchone()
|
| 513 |
-
tp_m, sl_m,
|
| 514 |
|
| 515 |
-
if reward > 0: # LEVEL UP
|
| 516 |
tp_m *= 1.05
|
| 517 |
-
min_p = max(0.50, min_p - 0.01)
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
sl_m *= 0.90
|
| 521 |
tp_m *= 0.95
|
| 522 |
-
min_p = min(0.65, min_p + 0.02)
|
| 523 |
-
min_tp_d = min(0.015, min_tp_d * 1.05) # Exige plus de marge de gain
|
| 524 |
|
| 525 |
-
cursor.execute("UPDATE agent_logic SET tp_mult=?, sl_mult=?, score=score+?, min_prob=?
|
| 526 |
-
(
|
| 527 |
cursor.execute("UPDATE signals SET status=? WHERE id=?", (outcome, t['id']))
|
| 528 |
|
| 529 |
-
# 📦 ON PRÉPARE LE COLIS POUR DISCORD
|
| 530 |
closed_trades.append({
|
| 531 |
"symbol": t['symbol'],
|
| 532 |
"timeframe": t['timeframe'],
|
|
@@ -534,26 +549,24 @@ def run_judge_api():
|
|
| 534 |
"outcome": outcome,
|
| 535 |
"entry_price": t['price'],
|
| 536 |
"close_price": current_price,
|
| 537 |
-
"
|
| 538 |
-
"sl": t['sl'],
|
| 539 |
-
"pnl": round(pnl_dollars, 2), # <--- LE NOUVEAU PNL EST ICI
|
| 540 |
"new_peur": round(min_p * 100, 1),
|
| 541 |
"new_tp": round(tp_m, 2),
|
| 542 |
"new_sl": round(sl_m, 2)
|
| 543 |
})
|
|
|
|
| 544 |
except Exception as inner_e:
|
| 545 |
print(f"⚠️ Erreur sur {t['symbol']} : {inner_e}")
|
| 546 |
|
| 547 |
conn.commit()
|
| 548 |
conn.close()
|
| 549 |
|
| 550 |
-
|
| 551 |
if closed_trades:
|
| 552 |
return {"status": "updates", "data": closed_trades}
|
| 553 |
-
return {"status": "waiting", "message": "waiting..."}
|
| 554 |
|
| 555 |
except Exception as e:
|
| 556 |
-
return {"status": "error", "message":
|
| 557 |
|
| 558 |
async def shutdown():
|
| 559 |
ex = ExchangeManager.get_instance()
|
|
|
|
| 84 |
|
| 85 |
def init_db():
|
| 86 |
reset_needed = False
|
|
|
|
| 87 |
if os.path.exists(DB_NAME):
|
| 88 |
try:
|
| 89 |
conn = sqlite3.connect(DB_NAME)
|
| 90 |
cursor = conn.execute("PRAGMA table_info(signals)")
|
| 91 |
columns = [col[1] for col in cursor.fetchall()]
|
| 92 |
conn.close()
|
| 93 |
+
# On vérifie si 'peak_price' existe, sinon on reset pour la nouvelle structure
|
| 94 |
+
if 'date' not in columns or 'peak_price' not in columns:
|
| 95 |
reset_needed = True
|
| 96 |
except Exception as e:
|
| 97 |
print(f"⚠️ Erreur check DB: {e}")
|
| 98 |
reset_needed = True
|
| 99 |
|
| 100 |
if reset_needed:
|
| 101 |
+
print("⚠️ Structure obsolète. Reset de la base de données pour le Trailing Stop...")
|
| 102 |
+
if os.path.exists(DB_NAME): os.remove(DB_NAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
try:
|
| 105 |
with sqlite3.connect(DB_NAME) as conn:
|
| 106 |
+
# Table des signaux avec 'peak_price' pour le Trailing Stop
|
| 107 |
conn.execute('''CREATE TABLE IF NOT EXISTS signals (
|
| 108 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 109 |
date TEXT, symbol TEXT, timeframe TEXT, direction TEXT,
|
| 110 |
prob REAL, price REAL, tp REAL, sl REAL, status TEXT,
|
| 111 |
+
regime INTEGER, prob_time REAL, prob_ml REAL, prob_lstm REAL, prob_sent REAL,
|
| 112 |
+
peak_price REAL)''') # <--- NOUVEAU
|
| 113 |
|
|
|
|
| 114 |
conn.execute('''CREATE TABLE IF NOT EXISTS agent_logic (
|
| 115 |
+
timeframe TEXT PRIMARY KEY, tp_mult REAL, sl_mult REAL,
|
| 116 |
+
score REAL, last_pnl REAL, min_prob REAL, min_tp_dist REAL)''')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
cursor = conn.cursor()
|
| 119 |
cursor.execute("SELECT COUNT(*) FROM agent_logic")
|
| 120 |
if cursor.fetchone()[0] == 0:
|
| 121 |
+
defaults = [('5m', 1.5, 1.0, 0, 0, 0.60, 0.003), ('15m', 2.0, 1.2, 0, 0, 0.55, 0.004),
|
| 122 |
+
('1h', 3.0, 1.5, 0, 0, 0.52, 0.005), ('4h', 4.5, 2.0, 0, 0, 0.50, 0.008),
|
| 123 |
+
('1d', 6.0, 2.5, 0, 0, 0.50, 0.01)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
conn.executemany("INSERT INTO agent_logic VALUES (?, ?, ?, ?, ?, ?, ?)", defaults)
|
| 125 |
+
print("✅ Base de données V11 (Trailing Stop Ready) Synchronisée.")
|
|
|
|
| 126 |
except Exception as e:
|
| 127 |
print(f"❌ Erreur critique création DB: {e}")
|
| 128 |
|
|
|
|
| 169 |
except Exception as e: print(f"⚠️ Erreur IA : {e}")
|
| 170 |
|
| 171 |
# --- 📊 FEATURES ENGINE ---
|
| 172 |
+
# --- 📊 FEATURES ENGINE (Fibonacci + FVG + BTC) ---
|
| 173 |
def prepare_features_sync(symbol, timeframe='1h', limit_bars=600):
|
|
|
|
| 174 |
try:
|
| 175 |
now = datetime.now().timestamp()
|
| 176 |
cache_key = f"{symbol}_{timeframe}"
|
| 177 |
+
if cache_key in market_cache and now - last_fetch_time.get(cache_key, 0) < CACHE_DURATION:
|
|
|
|
| 178 |
df = market_cache[cache_key].copy()
|
| 179 |
else:
|
| 180 |
+
bars = exchange_sync.fetch_ohlcv(symbol, timeframe, limit=limit_bars)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
df = pd.DataFrame(bars, columns=['ts', 'open', 'high', 'low', 'close', 'vol'])
|
| 182 |
+
market_cache[cache_key], last_fetch_time[cache_key] = df.copy(), now
|
|
|
|
| 183 |
|
| 184 |
+
if len(df) < 250: return pd.DataFrame()
|
|
|
|
| 185 |
|
| 186 |
+
# Indicateurs Standard
|
| 187 |
df["RSI"] = get_rsi(df["close"])
|
| 188 |
df["EMA50"] = get_ema(df["close"], 50)
|
| 189 |
df["EMA200"] = get_ema(df["close"], 200)
|
|
|
|
| 192 |
df["EMA200_slope"] = (df["EMA200"] / df["EMA200"].shift(10)) - 1
|
| 193 |
df["Drawdown"] = (df["close"] / df["close"].rolling(14).max()) - 1
|
| 194 |
|
| 195 |
+
# Market Structure
|
| 196 |
+
df["High_24h"], df["Low_24h"] = df["high"].rolling(24).max(), df["low"].rolling(24).min()
|
| 197 |
df["Dist_High_24h"] = (df["High_24h"] - df["close"]) / df["close"]
|
| 198 |
df["Dist_Low_24h"] = (df["close"] - df["Low_24h"]) / df["close"]
|
| 199 |
df["EMA_dist"] = (df["close"] - df["EMA50"]) / df["EMA50"]
|
|
|
|
| 201 |
df["ATR_ratio"] = df["ATR"] / df["close"]
|
| 202 |
df["VOL_ratio"] = df["vol"] / df["vol"].rolling(24).mean()
|
| 203 |
|
| 204 |
+
# Fibonacci (24h Swing)
|
| 205 |
+
diff = df["High_24h"] - df["Low_24h"]
|
| 206 |
+
df["Fib_618"] = df["Low_24h"] + (diff * 0.618)
|
| 207 |
+
df["Dist_Fib_618"] = (df["close"] - df["Fib_618"]) / df["close"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
# FVG (Fair Value Gap)
|
| 210 |
+
df['FVG_bull'] = (df['low'] > df['high'].shift(2)).astype(int)
|
| 211 |
+
df['FVG_bear'] = (df['high'] < df['low'].shift(2)).astype(int)
|
|
|
|
| 212 |
|
| 213 |
+
# BTC Correlation
|
| 214 |
+
if symbol != "BTC/USDT":
|
| 215 |
+
try:
|
| 216 |
+
b_bars = exchange_sync.fetch_ohlcv("BTC/USDT", timeframe, limit=50)
|
| 217 |
+
b_c = pd.Series([b[4] for b in b_bars])
|
| 218 |
+
df["BTC_slope"] = (b_c.ewm(50).mean().iloc[-1] / b_c.ewm(50).mean().iloc[-6]) - 1
|
| 219 |
+
except: df["BTC_slope"] = 0
|
| 220 |
+
else: df["BTC_slope"] = df["EMA200_slope"]
|
| 221 |
+
|
| 222 |
+
# SMC Sweeps
|
| 223 |
+
p_low, p_high = df["low"].rolling(24).min().shift(1), df["high"].rolling(24).max().shift(1)
|
| 224 |
+
df["Sweep_Low"] = ((df["low"] < p_low) & (df["close"] > p_low)).astype(int)
|
| 225 |
+
df["Sweep_High"] = ((df["high"] > p_high) & (df["close"] < p_high)).astype(int)
|
| 226 |
+
|
| 227 |
+
# Lags pour XGBoost
|
| 228 |
+
df['return_1h'], df['return_3h'], df['return_12h'] = df['close'].pct_change(1), df['close'].pct_change(3), df['close'].pct_change(12)
|
| 229 |
+
df['RSI_lag1'], df['RSI_lag2'] = df["RSI"].shift(1), df["RSI"].shift(2)
|
| 230 |
+
df['VOL_RATIO'] = df['vol'] / df['vol'].rolling(20).mean()
|
| 231 |
+
df['vol_lag1'], df['RSI_Macro'] = df['vol'].shift(1), df["RSI"]
|
| 232 |
|
| 233 |
+
return df.dropna().copy()
|
| 234 |
+
except Exception as e: print(f"❌ Error Stats: {e}"); return pd.DataFrame()
|
| 235 |
|
| 236 |
|
|
|
|
| 237 |
async def predict_signal(symbol, timeframe="1h"):
|
| 238 |
try:
|
| 239 |
memory_guard()
|
| 240 |
symbol = str(symbol).strip().upper()
|
| 241 |
if "/USDT" not in symbol: symbol += "/USDT"
|
| 242 |
|
| 243 |
+
# 1. Préparation des données (Contient FVG, Fib, BTC Slope)
|
| 244 |
df = prepare_features_sync(symbol, timeframe)
|
| 245 |
if df.empty: return {"status": "error", "message": f"Data insuffisante pour {timeframe}"}
|
| 246 |
|
| 247 |
last_row = df.iloc[[-1]]
|
| 248 |
prix, atr = float(last_row['close'].iloc[0]), float(last_row['ATR'].iloc[0])
|
| 249 |
|
| 250 |
+
# 2. Analyse des 3 cerveaux + Sentiment
|
| 251 |
regime_scaled = regime_scaler.transform(last_row[["ATR_pct", "EMA200_slope", "Drawdown", "RSI_Macro"]])
|
| 252 |
regime_pred = int(regime_model.predict(regime_scaled)[0])
|
| 253 |
|
|
|
|
| 263 |
|
| 264 |
p_sent = await get_crypto_sentiment(symbol)
|
| 265 |
|
| 266 |
+
# 3. Pondération adaptative selon le Timeframe (Tes réglages d'origine)
|
| 267 |
+
if timeframe in ["5m", "15m"]:
|
| 268 |
+
wm, wt, wl, ws = 0.45, 0.35, 0.15, 0.05
|
| 269 |
+
elif timeframe in ["4h", "1d"]:
|
| 270 |
+
wl, wm, wt, ws = 0.60, 0.15, 0.15, 0.10
|
| 271 |
+
else:
|
| 272 |
+
wt, wm, wl, ws = 0.25, 0.25, 0.25, 0.25
|
| 273 |
|
| 274 |
final_p = (time_prob * wt) + (ml_prob * wm) + (lstm_prob * wl) + (p_sent * ws)
|
| 275 |
|
| 276 |
+
# 4. Bonus Smart Money (SMC)
|
| 277 |
sweep_low, sweep_high = int(last_row["Sweep_Low"].iloc[0]), int(last_row["Sweep_High"].iloc[0])
|
| 278 |
smc_status = "AUCUN"
|
| 279 |
if sweep_low == 1:
|
|
|
|
| 283 |
final_p = max(0.05, final_p - 0.20)
|
| 284 |
smc_status = "SHORT SWEEP 🐋"
|
| 285 |
|
| 286 |
+
# 5. Lecture des Stats RPG et calcul TP/SL
|
| 287 |
with sqlite3.connect(DB_NAME) as conn:
|
| 288 |
res = conn.execute("SELECT tp_mult, sl_mult, min_prob, min_tp_dist FROM agent_logic WHERE timeframe = ?", (timeframe,)).fetchone()
|
| 289 |
tp_m, sl_m, agent_min_prob, agent_min_tp_dist = res if res else (3.0, 1.5, 0.55, 0.004)
|
|
|
|
| 291 |
tp = prix + (atr * tp_m) if final_p > 0.5 else prix - (atr * tp_m)
|
| 292 |
sl = prix - (atr * sl_m) if final_p > 0.5 else prix + (atr * sl_m)
|
| 293 |
|
| 294 |
+
# 6. Scores et Risk Management
|
| 295 |
strength = abs(final_p - 0.5) * 2
|
| 296 |
conf_val = max(0, min(1, 1 - np.std([time_prob, ml_prob, lstm_prob, p_sent])))
|
|
|
|
| 297 |
score_base = (strength * 45) + (conf_val * 40)
|
| 298 |
regime_bonus = 15 if regime_pred in [0, 1] else 5
|
| 299 |
composite_score = max(0, min(100, score_base + regime_bonus))
|
| 300 |
|
| 301 |
risk_pct = max(0.2, min(2.5, strength * 5.0))
|
| 302 |
+
risk_usd = 1000 * (risk_pct / 100) # Capital Virtuel 1000$
|
|
|
|
| 303 |
gain_estime_usd = risk_usd * (tp_m / sl_m)
|
| 304 |
|
| 305 |
# ==========================================
|
| 306 |
+
# 🛑 LE VETO DYNAMIQUE (F1 à F9)
|
| 307 |
# ==========================================
|
| 308 |
veto = False
|
| 309 |
veto_reason = ""
|
| 310 |
+
dist_tp_pct = abs(tp - prix) / prix
|
| 311 |
+
dist_fib = float(last_row["Dist_Fib_618"].iloc[0])
|
| 312 |
+
btc_slope = float(last_row["BTC_slope"].iloc[0])
|
| 313 |
+
ema200_slope_val = float(last_row["EMA200_slope"].iloc[0])
|
| 314 |
+
fvg_bull = int(last_row["FVG_bull"].iloc[0])
|
|
|
|
| 315 |
|
| 316 |
+
# F1/F2: Confiance
|
| 317 |
+
if final_p < agent_min_prob and final_p > (1 - agent_min_prob):
|
| 318 |
+
veto, veto_reason = True, f"Confiance ({round(final_p, 2)}). Requis: > {round(agent_min_prob, 2)}"
|
| 319 |
+
# F3: SMC (5m/15m)
|
| 320 |
elif timeframe in ["5m", "15m"] and smc_status == "AUCUN":
|
| 321 |
+
veto, veto_reason = True, "Aucune manipulation (Sweep) détectée."
|
| 322 |
+
# F4: Frais
|
| 323 |
+
elif dist_tp_pct < agent_min_tp_dist:
|
| 324 |
+
veto, veto_reason = True, f"TP trop proche ({round(dist_tp_pct*100, 2)}%)."
|
| 325 |
+
# F5: Macro
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
elif symbol != "BTC/USDT" and regime_pred == 2 and final_p > 0.5:
|
| 327 |
+
veto, veto_reason = True, "Tendance macro baissière."
|
| 328 |
+
# F6: Anti-Kamikaze
|
| 329 |
+
elif final_p < 0.5 and ema200_slope_val > 0.005:
|
| 330 |
+
veto, veto_reason = True, "Interdit de shorter un pump."
|
| 331 |
+
elif final_p > 0.5 and ema200_slope_val < -0.005:
|
| 332 |
+
veto, veto_reason = True, "Interdit d'acheter un dump."
|
| 333 |
+
# F7: Fibonacci 📏
|
| 334 |
+
elif final_p > 0.6 and dist_fib < -0.02:
|
| 335 |
+
veto, veto_reason = True, f"Support Fib trop loin ({round(dist_fib*100, 2)}%)."
|
| 336 |
+
# F8: Corrélation BTC 👑
|
| 337 |
+
elif symbol != "BTC/USDT":
|
| 338 |
+
if final_p < 0.5 and btc_slope > 0.002:
|
| 339 |
+
veto, veto_reason = True, "BTC Haussier (No Short)."
|
| 340 |
+
elif final_p > 0.5 and btc_slope < -0.002:
|
| 341 |
+
veto, veto_reason = True, "BTC Baissier (No Long)."
|
| 342 |
+
# F9: FVG Check (Facultatif mais enregistré)
|
| 343 |
+
if final_p > 0.7 and fvg_bull == 0:
|
| 344 |
+
print(f"⚠️ [FVG Warning] Pas de gap de liquidité pour l'entrée sur {symbol}")
|
| 345 |
|
| 346 |
if veto:
|
| 347 |
+
print(f"🛑 [VETO] {symbol} [{timeframe}]: {veto_reason}")
|
| 348 |
+
return {"symbol": symbol, "timeframe": timeframe, "status": "veto", "message": veto_reason, "price": prix, "final_score": round(final_p, 4)}
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
+
# 7. SAUVEGARDE DB (On ajoute peak_price à la fin)
|
| 351 |
+
db_task = (datetime.now(timezone.utc).isoformat(), symbol, timeframe,
|
| 352 |
+
'HAUSSIER' if final_p > 0.5 else 'BAISSIER', final_p, prix, tp, sl,
|
| 353 |
+
'EN_COURS', regime_pred, time_prob, ml_prob, lstm_prob, p_sent, prix)
|
|
|
|
| 354 |
await save_to_db(db_task)
|
| 355 |
|
| 356 |
print(f"✅ [VALIDÉ] {symbol} [{timeframe}] : Ordre envoyé au Juge.")
|
|
|
|
| 364 |
"probs": {"xgb": round(time_prob, 3), "rf": round(ml_prob, 3), "lstm": round(lstm_prob, 3), "sent": round(p_sent, 3)},
|
| 365 |
"weights": {"w_xgb": round(wt, 2), "w_rf": round(wm, 2), "w_lstm": round(wl, 2), "w_sent": round(ws, 2)}
|
| 366 |
}
|
|
|
|
| 367 |
except Exception as e:
|
| 368 |
return {"status": "error", "message": str(e)}
|
| 369 |
|
|
|
|
| 456 |
conn = sqlite3.connect(DB_NAME)
|
| 457 |
conn.row_factory = sqlite3.Row
|
| 458 |
cursor = conn.cursor()
|
| 459 |
+
|
| 460 |
+
# On ne juge que les trades qui sont encore ouverts
|
| 461 |
cursor.execute("SELECT * FROM signals WHERE status = 'EN_COURS'")
|
| 462 |
trades = cursor.fetchall()
|
| 463 |
|
| 464 |
+
if not trades:
|
| 465 |
+
return {"status": "waiting", "message": "⚖️ Observation du marché en cours..."}
|
| 466 |
|
| 467 |
closed_trades = []
|
| 468 |
for t in trades:
|
|
|
|
| 470 |
ticker = exchange_sync.fetch_ticker(t['symbol'])
|
| 471 |
current_price = ticker['last']
|
| 472 |
|
| 473 |
+
# --- 🚀 LOGIQUE TRAILING STOP (SÉCURISATION) ---
|
| 474 |
+
sl_dynamique = t['sl']
|
| 475 |
+
peak = t['peak_price']
|
| 476 |
+
|
| 477 |
+
# Mise à jour du point le plus haut (ou bas) atteint
|
| 478 |
+
new_peak = max(peak, current_price) if t['direction'] == 'HAUSSIER' else min(peak, current_price)
|
| 479 |
+
|
| 480 |
+
# Ratio de progression vers le TP (0.5 = 50% du chemin fait)
|
| 481 |
+
chemin_total = abs(t['tp'] - t['price'])
|
| 482 |
+
chemin_parcouru = abs(current_price - t['price'])
|
| 483 |
+
progression = chemin_parcouru / chemin_total if chemin_total > 0 else 0
|
| 484 |
+
|
| 485 |
+
# 🛡️ PROTECTION BREAK-EVEN : Si on a fait 50% du chemin, on met le SL au prix d'entrée
|
| 486 |
+
if progression > 0.5:
|
| 487 |
+
# Pour un LONG, on remonte le SL. Pour un SHORT, on le descend.
|
| 488 |
+
if t['direction'] == 'HAUSSIER' and sl_dynamique < t['price']:
|
| 489 |
+
sl_dynamique = t['price']
|
| 490 |
+
elif t['direction'] == 'BAISSIER' and sl_dynamique > t['price']:
|
| 491 |
+
sl_dynamique = t['price']
|
| 492 |
+
|
| 493 |
+
# On met à jour la base de données avec le nouveau Peak et le nouveau SL
|
| 494 |
+
cursor.execute("UPDATE signals SET peak_price = ?, sl = ? WHERE id = ?", (new_peak, sl_dynamique, t['id']))
|
| 495 |
+
|
| 496 |
+
# --- 🏁 VÉRIFICATION DU RÉSULTAT ---
|
| 497 |
outcome = None
|
| 498 |
reward = 0
|
| 499 |
|
| 500 |
if t['direction'] == 'HAUSSIER':
|
| 501 |
+
if current_price >= t['tp']:
|
| 502 |
+
outcome, reward = "GAGNÉ ✅", 3
|
| 503 |
+
elif current_price <= sl_dynamique:
|
| 504 |
+
# On distingue si c'est une perte ou une protection
|
| 505 |
+
outcome = "SL TOUCHÉ 🛡️" if sl_dynamique == t['price'] else "PERDU ❌"
|
| 506 |
+
reward = 0 if outcome == "SL TOUCHÉ 🛡️" else -5
|
| 507 |
else:
|
| 508 |
+
if current_price <= t['tp']:
|
| 509 |
+
outcome, reward = "GAGNÉ ✅", 3
|
| 510 |
+
elif current_price >= sl_dynamique:
|
| 511 |
+
outcome = "SL TOUCHÉ 🛡️" if sl_dynamique == t['price'] else "PERDU ❌"
|
| 512 |
+
reward = 0 if outcome == "SL TOUCHÉ 🛡️" else -5
|
| 513 |
|
| 514 |
if outcome:
|
| 515 |
+
# --- 💰 CALCUL DU PNL ---
|
|
|
|
|
|
|
|
|
|
| 516 |
risque_usd = 100.0
|
| 517 |
+
dist_sl_initiale = abs(t['price'] - t['sl'])
|
| 518 |
+
dist_tp_initiale = abs(t['tp'] - t['price'])
|
| 519 |
+
rr_ratio = dist_tp_initiale / dist_sl_initiale if dist_sl_initiale > 0 else 1
|
|
|
|
|
|
|
| 520 |
|
| 521 |
if "GAGNÉ" in outcome:
|
| 522 |
pnl_dollars = risque_usd * rr_ratio
|
| 523 |
+
elif "SL TOUCHÉ" in outcome:
|
| 524 |
+
pnl_dollars = 0.0 # On sort à 0, capital sauvé !
|
| 525 |
else:
|
| 526 |
pnl_dollars = -risque_usd
|
| 527 |
|
| 528 |
+
# --- 🎮 ÉVOLUTION RPG ---
|
| 529 |
cursor.execute("SELECT tp_mult, sl_mult, score, min_prob, min_tp_dist FROM agent_logic WHERE timeframe = ?", (t['timeframe'],))
|
| 530 |
row = cursor.fetchone()
|
| 531 |
+
tp_m, sl_m, score_ia, min_p, min_tp_d = row if row else (3.0, 1.5, 0, 0.55, 0.004)
|
| 532 |
|
| 533 |
+
if reward > 0: # LEVEL UP
|
| 534 |
tp_m *= 1.05
|
| 535 |
+
min_p = max(0.50, min_p - 0.01)
|
| 536 |
+
elif reward < 0: # PUNITION
|
| 537 |
+
sl_m = max(1.0, sl_m * 0.95) # On ne descend jamais sous 1.0 ATR
|
|
|
|
| 538 |
tp_m *= 0.95
|
| 539 |
+
min_p = min(0.65, min_p + 0.02)
|
|
|
|
| 540 |
|
| 541 |
+
cursor.execute("UPDATE agent_logic SET tp_mult=?, sl_mult=?, score=score+?, min_prob=? WHERE timeframe=?",
|
| 542 |
+
(tp_m, sl_m, reward, min_p, t['timeframe']))
|
| 543 |
cursor.execute("UPDATE signals SET status=? WHERE id=?", (outcome, t['id']))
|
| 544 |
|
|
|
|
| 545 |
closed_trades.append({
|
| 546 |
"symbol": t['symbol'],
|
| 547 |
"timeframe": t['timeframe'],
|
|
|
|
| 549 |
"outcome": outcome,
|
| 550 |
"entry_price": t['price'],
|
| 551 |
"close_price": current_price,
|
| 552 |
+
"pnl": round(pnl_dollars, 2),
|
|
|
|
|
|
|
| 553 |
"new_peur": round(min_p * 100, 1),
|
| 554 |
"new_tp": round(tp_m, 2),
|
| 555 |
"new_sl": round(sl_m, 2)
|
| 556 |
})
|
| 557 |
+
|
| 558 |
except Exception as inner_e:
|
| 559 |
print(f"⚠️ Erreur sur {t['symbol']} : {inner_e}")
|
| 560 |
|
| 561 |
conn.commit()
|
| 562 |
conn.close()
|
| 563 |
|
|
|
|
| 564 |
if closed_trades:
|
| 565 |
return {"status": "updates", "data": closed_trades}
|
| 566 |
+
return {"status": "waiting", "message": "waiting..."}
|
| 567 |
|
| 568 |
except Exception as e:
|
| 569 |
+
return {"status": "error", "message": str(e)}
|
| 570 |
|
| 571 |
async def shutdown():
|
| 572 |
ex = ExchangeManager.get_instance()
|