File size: 54,126 Bytes
a0167b8
c683db9
 
 
 
 
 
 
 
 
 
 
 
63f3afd
7da1e20
c586553
09e3f0b
7da1e20
09e3f0b
7da1e20
67cc9b7
fef8ec7
 
f681884
9ea1c3d
d9703fb
855ee5d
fef8ec7
ebc5d12
fef8ec7
 
 
 
 
ebc5d12
eb8d75c
8910009
 
 
 
 
ebc5d12
eb8d75c
f681884
 
 
 
 
 
fef8ec7
f681884
8d7b6e7
7da1e20
 
2a9b2f8
09ddf1e
 
 
 
 
7da1e20
fef8ec7
09e3f0b
7da1e20
c586553
 
 
 
 
7da1e20
c586553
09ddf1e
7da1e20
09ddf1e
fef8ec7
0c9a77a
 
 
 
 
 
 
7da1e20
 
 
fef8ec7
7da1e20
 
 
 
e09af54
 
 
 
7da1e20
ebc5d12
cc0afe2
7da1e20
ebc5d12
 
62707c9
ebc5d12
 
4d4660b
 
ebc5d12
 
 
 
 
4d4660b
 
 
 
f7cbf17
 
 
 
1f0ed46
f7cbf17
 
 
af84323
 
 
 
 
 
 
 
 
 
f7cbf17
ebc5d12
 
 
 
 
 
 
 
7da1e20
 
bbf43f8
 
63fc3a4
bbf43f8
 
8d7b6e7
 
1c8e8f7
fef8ec7
bbf43f8
 
63fc3a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fef8ec7
63fc3a4
 
 
 
fef8ec7
63fc3a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce70583
d731e20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09ddf1e
ebc5d12
 
 
 
 
 
e09af54
ebc5d12
 
 
2d31516
 
 
 
ebc5d12
2d31516
 
7da1e20
09ddf1e
c586553
09ddf1e
 
 
fef8ec7
7da1e20
c586553
 
 
 
 
 
 
 
 
7da1e20
eb8d75c
 
 
 
 
fef8ec7
637c398
 
 
 
 
fef8ec7
 
 
 
 
 
 
e09af54
 
 
 
 
 
c093da6
 
 
e09af54
 
fef8ec7
 
 
e09af54
 
 
 
 
 
 
 
 
 
 
 
 
09d31a7
67cc9b7
d179db1
7da1e20
33c1409
8602c3d
0c9a77a
 
 
33c1409
09ddf1e
ebc5d12
 
 
 
 
 
 
 
 
 
9ea1c3d
fef8ec7
1c8e8f7
09ddf1e
fef8ec7
33c1409
fef8ec7
 
eb8d75c
c586553
7da1e20
 
 
04fa252
1c8e8f7
7da1e20
 
 
 
eb8d75c
7da1e20
eb8d75c
 
7da1e20
1c8e8f7
 
 
8602c3d
fef8ec7
 
 
 
 
1c8e8f7
 
eb8d75c
1c8e8f7
8d7b6e7
1c8e8f7
 
e9ab125
63fc3a4
 
581a042
c17d9c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581a042
c17d9c4
 
 
 
 
 
581a042
c17d9c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7da1e20
09d31a7
09ddf1e
7da1e20
52f5775
fef8ec7
b107e51
7da1e20
33caa7b
eb8d75c
 
 
b107e51
63fc3a4
fef8ec7
 
63fc3a4
eb8d75c
581a042
610c015
 
581a042
610c015
63fc3a4
7da1e20
63fc3a4
eb8d75c
581a042
 
63fc3a4
 
 
fef8ec7
 
 
 
f7cbf17
 
 
 
 
 
 
 
 
610c015
63fc3a4
33c1409
63fc3a4
c8cf459
fef8ec7
63fc3a4
fef8ec7
63fc3a4
fef8ec7
 
 
 
eb8d75c
5b7fa0f
33caa7b
63fc3a4
 
 
 
 
fef8ec7
5b7fa0f
 
 
 
 
 
 
 
 
 
 
 
eb8d75c
63fc3a4
fef8ec7
 
7a37225
fef8ec7
 
63fc3a4
7a37225
610c015
1c8e8f7
fef8ec7
63fc3a4
110b50c
63fc3a4
 
fef8ec7
 
63fc3a4
 
1f0ed46
63fc3a4
 
d6da76d
21a5372
63fc3a4
21a5372
e1fcbaa
 
 
 
610c015
63fc3a4
9e6a403
110b50c
2334f97
09ddf1e
fef8ec7
63fc3a4
09ddf1e
fef8ec7
f1b57fe
5b7fa0f
63fc3a4
e1fcbaa
 
 
 
 
 
 
 
 
5b7fa0f
 
e1fcbaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5b7fa0f
e1fcbaa
 
 
 
 
 
 
 
 
5b7fa0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63fc3a4
de4f4ea
610c015
 
 
de4f4ea
610c015
 
 
 
 
 
de4f4ea
610c015
de4f4ea
 
 
 
 
0eccbfd
 
 
 
 
a0bfc72
de4f4ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e34f612
de4f4ea
 
610c015
63fc3a4
3a52629
e1fcbaa
3a52629
 
 
0c9a77a
 
 
 
3a52629
 
0c9a77a
 
3a52629
 
 
 
1984e8d
406c49f
63fc3a4
406c49f
99c64e0
 
 
 
 
406c49f
610c015
ed2b570
 
d40ef29
a452727
610c015
d40ef29
610c015
1984e8d
a452727
581a042
a452727
610c015
 
406c49f
99c64e0
406c49f
 
a452727
63fc3a4
a452727
 
1984e8d
a452727
 
 
 
 
 
 
 
 
1984e8d
a452727
 
1984e8d
a452727
1984e8d
a452727
610c015
 
d40ef29
e237498
 
 
1984e8d
e237498
 
 
610c015
 
d40ef29
 
1984e8d
610c015
a452727
 
 
 
610c015
a452727
 
 
 
63fc3a4
610c015
d40ef29
1984e8d
ed2b570
610c015
 
 
 
 
 
63fc3a4
610c015
 
 
1984e8d
 
 
 
 
 
 
 
 
 
a452727
 
1984e8d
610c015
 
1984e8d
 
 
 
 
 
 
d731e20
406c49f
 
d731e20
406c49f
425ff28
 
d731e20
 
 
425ff28
d731e20
425ff28
 
 
 
 
406c49f
610c015
 
 
 
 
 
 
 
63fc3a4
eb0086b
 
2a6e841
3042372
f7cbf17
 
 
 
 
 
 
 
3042372
25d8a90
f7cbf17
25d8a90
fef8ec7
f7cbf17
fef8ec7
eb0086b
 
 
 
ebc5d12
 
f7cbf17
ebc5d12
f7cbf17
 
 
 
4d4660b
32f043d
4d4660b
f7cbf17
 
 
4d4660b
 
 
f7cbf17
4d4660b
f7cbf17
 
 
 
 
 
 
 
 
 
7da1e20
f7cbf17
 
 
 
7da1e20
f7cbf17
 
 
8d7b6e7
ebc5d12
1f0ed46
581a042
5fd7ba5
fef8ec7
 
 
ebc5d12
 
fef8ec7
 
ebc5d12
 
5fd7ba5
b2d04db
 
 
2d31516
 
 
 
b2d04db
2d31516
b2d04db
634ed16
2d31516
b2d04db
ebc5d12
b2d04db
ebc5d12
2d31516
ebc5d12
2d31516
 
ebc5d12
63f3afd
ebc5d12
fef8ec7
09f7d3b
ce70583
 
fef8ec7
ebc5d12
09f7d3b
eb8d75c
ebc5d12
 
ce70583
110b50c
eb8d75c
ebc5d12
 
8308461
 
ebc5d12
eb8d75c
fef8ec7
1af8dac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8602c3d
ebc5d12
c4ae83a
7db3461
1ba8caf
 
 
 
 
 
 
 
fef8ec7
7da1e20
ebc5d12
4316814
 
 
1af8dac
4316814
 
 
 
 
 
581a042
4316814
7da1e20
406c49f
eb8d75c
 
fef8ec7
4316814
fef8ec7
110b50c
1f25aba
eb8d75c
4316814
fef8ec7
eb8d75c
 
8b668a0
f1b57fe
38b7d8f
 
 
 
 
 
 
184fa54
fef8ec7
184fa54
fef8ec7
 
 
f1b57fe
ebc5d12
67821f2
fef8ec7
 
ebc5d12
fef8ec7
38b7d8f
fef8ec7
ebc5d12
184fa54
f1b57fe
 
 
38b7d8f
81bcc3c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
# --- 🏴‍☠️ MONKEY PATCH & IMPORTS --
import huggingface_hub
try:
    from huggingface_hub import HfFolder
except ImportError:
    class MockHfFolder:
        @staticmethod
        def get_token(): return None
        @staticmethod
        def save_token(token): pass
        @staticmethod
        def delete_token(): pass
    huggingface_hub.HfFolder = MockHfFolder

import sys, os, sqlite3, shutil, pandas as pd, asyncio, joblib, json, numpy as np, gc, warnings, psutil
import ccxt.async_support as ccxt_async
from datetime import datetime, timezone
from huggingface_hub import HfApi, hf_hub_download
import gradio as gr
import ccxt
import time
import random
import aiohttp
from types import ModuleType
import yfinance as yf
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="sklearn")

# --- 🦖 IMPORT LIGHTGBM ---
try:
    import lightgbm as lgb
    LGBM_AVAILABLE = True
except ImportError:
    LGBM_AVAILABLE = False
    print("⚠️ LightGBM non installé.")

try:
    import MetaTrader5 as mt5
    MT5_AVAILABLE = True
except ImportError:
    MT5_AVAILABLE = False
    print("🌐 [CLOUD MODE] MetaTrader5 non détecté.")

# --- 🥷 NINJA HACK : MOCK PANDAS_TA ---
if "pandas_ta" not in sys.modules:
    mock_ta = ModuleType("pandas_ta")
    sys.modules["pandas_ta"] = mock_ta
if not hasattr(pd.DataFrame, "ta"):
    class FakeTA:
        def __getattr__(self, name): return lambda *args, **kwargs: None
    pd.DataFrame.ta = property(lambda self: FakeTA())

# --- 🛑 ANTI-CRASH & CPU OPTIMIZATION ---
os.environ["CUDA_VISIBLE_DEVICES"] = "-1" 
import tensorflow as tf
from tensorflow.keras import backend as K

tf.config.threading.set_intra_op_parallelism_threads(2)
tf.config.threading.set_inter_op_parallelism_threads(2)

current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path: sys.path.append(current_dir)

# --- SINGLETON EXCHANGE ---
class ExchangeManager:
    _instance = None
    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = ccxt_async.kucoin({"enableRateLimit": True, "timeout": 30000})
        return cls._instance

exchange_sync = ccxt.kucoin({"enableRateLimit": True, "timeout": 30000})

market_cache, last_fetch_time = {}, {}
# 🛡️ FIX : Cache dynamique selon la timeframe pour éviter l'amnésie
def get_cache_ttl(tf):
    if tf == '1m': return 20   # 20 secondes de cache pour le 1m
    if tf == '5m': return 60   # 1 minute pour le 5m
    if tf == '15m': return 180 # 3 minutes pour le 15m
    return 300                 # 5 minutes pour le 1h et 4h
DREAM_MODE_ACTIVE = True

try:
    from sentiment_engine import get_crypto_sentiment
except:
    async def get_crypto_sentiment(symbol): return 0.5
try:
    from ensemble import combine_scores
except:
    def combine_scores(symbol, timeframe, t, m, l, sent, r): 
        # Formule PRO : 30% Time, 30% ML, 30% LightGBM, 10% Sentiment
        score = (t * 0.30) + (m * 0.30) + (l * 0.30) + (sent * 0.10)
        return score, 0.30, 0.30, 0.30, 0.10

# --- DB, SYNC & HUGGING FACE BACKUP ---
DB_NAME = "alphatrade_v31_dino.db"
HF_TOKEN = os.environ.get("HF_TOKEN")
HF_REPO_ID = "Nexo-S/AlphaTrade-DB"


def restore_db_from_hf():
    if not HF_TOKEN: return
    
    # 1. Tentative de restauration de l'ADN (DB)
    try:
        print("🔄 [SYSTEM] Restauration ADN depuis HF...")
        file_path = hf_hub_download(repo_id=HF_REPO_ID, filename=DB_NAME, repo_type="dataset", token=HF_TOKEN)
        shutil.copy(file_path, DB_NAME)
        print("✅ [RESTORE] ADN récupéré !")
    except Exception as e: 
        print(f"⚠️ Aucun backup ADN trouvé (1er lancement) : {e}")

    # 2. Tentative de restauration du modèle DINO
    # 2. Tentative de restauration des modèles DINO (Multiples)
    global dino_brains
    dino_brains = {}
    
    for crypto in ["BTC", "ETH", "SOL"]:
        filename = f"dino_lgbm_{crypto}.txt"
        try:
            print(f"🔄 [SYSTEM] Restauration du DINO {crypto} depuis HF...")
            file_path = hf_hub_download(repo_id=HF_REPO_ID, filename=filename, repo_type="dataset", local_dir=".", token=HF_TOKEN)
            
            # 🛡️ LE VACCIN EST ICI : On vérifie que le fichier pèse plus de 100 octets (qu'il n'est pas vide)
            if os.path.exists(file_path) and os.path.getsize(file_path) > 100:
                dino_brains[crypto] = lgb.Booster(model_file=file_path)
                print(f"✅ [RESTORE] Modèle Dino {crypto} récupéré et chargé !")
            else:
                print(f"⚠️ [ALERTE] Fichier {filename} vide ou corrompu. Mode aveugle activé pour {crypto}.")
                
        except Exception as e:
            print(f"⚠️ Aucun modèle trouvé pour {crypto}. Pense à Forcer l'Entraînement de {crypto}.")

def backup_db_to_hf():
    if not HF_TOKEN: return
    try:
        api = HfApi()
        api.upload_file(path_or_fileobj=DB_NAME, path_in_repo=DB_NAME, repo_id=HF_REPO_ID, repo_type="dataset", token=HF_TOKEN)
        print("☁️ [BACKUP] ADN sauvegardé sur Hugging Face !")
    except Exception as e: print(f"❌ Erreur Backup HF : {e}")

def init_db():
    try:
        with sqlite3.connect(DB_NAME) as conn:
            # 1. Table des Signaux (Historique)
            conn.execute('''CREATE TABLE IF NOT EXISTS signals (
                id INTEGER PRIMARY KEY AUTOINCREMENT, 
                date TEXT, symbol TEXT, timeframe TEXT, direction TEXT, 
                prob REAL, price REAL, tp REAL, sl REAL, status TEXT, 
                regime INTEGER, prob_time REAL, prob_ml REAL, prob_lstm REAL, prob_sent REAL,
                peak_price REAL, confirmed INTEGER DEFAULT 0)''')
            
            cursor = conn.cursor()
            
            # 2. Table de l'ADN (Logique des Agents)
            # On vérifie si la table existe déjà pour éviter les conflits de structure
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='agent_logic'")
            table_exists = cursor.fetchone()

            if not table_exists:
                conn.execute('''CREATE TABLE agent_logic (
                    symbol TEXT, 
                    timeframe TEXT, 
                    regime INTEGER, -- ⬅️ Ajouté ici
                    tp_mult REAL, 
                    sl_mult REAL,
                    score REAL, 
                    last_pnl REAL, 
                    min_prob REAL, 
                    min_tp_dist REAL,
                    generation INTEGER DEFAULT 1, 
                    best_tp REAL, 
                    best_sl REAL,
                    PRIMARY KEY (symbol, timeframe, regime))''') # ⬅️ CLÉ PRIMAIRE TRIPLE
                
                # 3. Insertion des réglages par défaut
                # On initialise les réglages sur le régime 3 (RANGE/CHAOS) par défaut
                defaults = [
                    # Symbol, TF, Regime, TP, SL, Score, PNL, Prob, Dist, Gen, B_TP, B_SL
                    ('ALL', '15m', 3, 1.5, 1.0, 0, 0, 0.60, 0.003, 1, 0, 0), 
                    ('ALL', '1h', 3, 2.0, 1.5, 0, 0, 0.55, 0.005, 1, 0, 0),
                    ('ALL', '4h', 3, 3.0, 2.0, 0, 0, 0.50, 0.008, 1, 0, 0)
                ]
                # On utilise 12 points d'interrogation pour les 12 colonnes
                conn.executemany("INSERT INTO agent_logic VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", defaults)
                conn.commit()
                print("✅ [DB] Table agent_logic initialisée avec Scénarios.")
            else:
                # Optionnel : Vérifier si la colonne regime existe déjà (pour les mises à jour sans tout supprimer)
                cursor.execute("PRAGMA table_info(agent_logic)")
                columns = [col[1] for col in cursor.fetchall()]
                if 'regime' not in columns:
                    print("⚠️ [DB] Ancienne structure détectée. Suppression pour mise à jour...")
                    conn.execute("DROP TABLE agent_logic")
                    init_db() # On relance pour recréer proprement
                    
    except Exception as e: 
        print(f"❌ Erreur DB: {e}")

import asyncio

# --- 💾 SAUVEGARDE BLINDÉE (ANTI-EMBOUTEILLAGE) ---
async def save_to_db(data_tuple):
    for tentative in range(5): # Le Cerveau va insister 5 fois si la porte est fermée
        try:
            with sqlite3.connect(DB_NAME, timeout=20) as conn:
                conn.execute('''INSERT INTO signals 
                              (date, symbol, timeframe, direction, prob, price, tp, sl, status, regime, prob_time, prob_ml, prob_lstm, prob_sent, peak_price) 
                              VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', data_tuple)
                conn.commit()
                return # Succès, on quitte la fonction
        except sqlite3.OperationalError as e:
            if "locked" in str(e):
                await asyncio.sleep(1) # La base est lue par le Radar, on attend 1 seconde
            else:
                print(f"❌ [ERREUR DB] Sauvegarde échouée : {e}")
                break

# 🚀 INITIALISATION 
restore_db_from_hf()
init_db()

# --- 🛡️ STATE MANAGER ---
active_signals_state = {}
last_signals_sent = {} # ⬅️ LA RUSTINE CRITIQUE EST LÀ

def clean_old_db_signals(symbol, timeframe):
    try:
        with sqlite3.connect(DB_NAME, timeout=10) as conn:
            # 🛡️ LA SÉCURITÉ : On ne touche qu'aux signaux NON-CONFIRMÉS (Les fantômes)
            # Et on retire le "confirmed = 0" du SET (il ne faut pas dé-confirmer un trade)
            conn.execute("UPDATE signals SET status = 'REMPLACÉ ♻️' WHERE symbol = ? AND timeframe = ? AND status = 'EN_COURS' AND confirmed = 0", (symbol, timeframe))
            conn.commit()
    except Exception as e: 
        print(f"⚠️ Erreur nettoyage DB : {e}")

def memory_guard():
    if psutil.virtual_memory().percent > 80:
        K.clear_session()
        gc.collect()

# --- 🛠️ MOTEUR MATHS ---
def get_ema(series, period): return series.ewm(span=period, adjust=False).mean()
def get_rsi(series, period=14):
    delta = series.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    return 100 - (100 / (1 + (gain / (loss + 1e-9))))
def get_atr(df, period=14):
    h_l = df['high'] - df['low']
    h_c = (df['high'] - df['close'].shift()).abs()
    l_c = (df['low'] - df['close'].shift()).abs()
    return pd.concat([h_l, h_c, l_c], axis=1).max(axis=1).rolling(period).mean()
def get_vwap(df):
    v = df['vol']
    tp = (df['high'] + df['low'] + df['close']) / 3
    return (tp * v).cumsum() / (v.cumsum() + 1e-9)

# --- 🧠 CHARGEMENT IA ---
try:
    ml_model = joblib.load("ml_model_v9.pkl")
    time_model = joblib.load("time_model.pkl")
    regime_model = joblib.load("regime_model.pkl")
    regime_scaler = joblib.load("regime_scaler.pkl")
except Exception as e: print(f"⚠️ Erreur IA Classique : {e}")

try:
    dino_brain = lgb.Booster(model_file='dino_lgbm_model.txt') if LGBM_AVAILABLE else None
except:
    dino_brain = None

# --- 🌐 HTTP SESSION MANAGER ---
global_aio_session = None

async def get_aio_session():
    global global_aio_session
    if global_aio_session is None or global_aio_session.closed:
        # On force un connecteur limité pour éviter les fuites de mémoire (Max 100 connexions)
        connector = aiohttp.TCPConnector(limit=100)
        global_aio_session = aiohttp.ClientSession(connector=connector)
    return global_aio_session

async def fetch_kucoin_futures_data(symbol):
    try:
        ku_sym = symbol.replace("/", "").replace("BTC", "XBT") + "M"
        session = await get_aio_session() # On utilise la session globale ouverte
        
        oi_url = f"https://api-futures.kucoin.com/api/v1/open-interest?symbol={ku_sym}"
        trade_url = f"https://api-futures.kucoin.com/api/v1/trade/history?symbol={ku_sym}"
        
        async with session.get(oi_url) as r1, session.get(trade_url) as r2:
            oi_json = await r1.json()
            trades = (await r2.json()).get("data", [])
            cvd = sum([float(t.get("size", 0)) if t.get("side") == "buy" else -float(t.get("size", 0)) for t in trades])
            return {"oi": float(oi_json.get("data", {}).get("value", 0)), "cvd": cvd}
    except Exception as e: 
        print(f"⚠️ Erreur Flux KuCoin ({symbol}) : {e}")
        return {"oi": 0, "cvd": 0}

def prepare_features_sync(symbol, timeframe='1h', limit_bars=600):
    try:
        now = datetime.now().timestamp()
        cache_key = f"{symbol}_{timeframe}"
        
        # 🛡️ FIX : On utilise le TTL dynamique ici
        ttl = get_cache_ttl(timeframe)
        if cache_key in market_cache and now - last_fetch_time.get(cache_key, 0) < ttl:
            df = market_cache[cache_key].copy()
        else:
            fetch_symbol = symbol if "/USDT" in symbol else symbol.replace("/USD", "/USDT")
            if "/" not in fetch_symbol: fetch_symbol += "/USDT"
            
            df = pd.DataFrame()
            for attempt in range(3):
                try:
                    bars = exchange_sync.fetch_ohlcv(fetch_symbol, timeframe, limit=limit_bars)
                    df = pd.DataFrame(bars, columns=['ts', 'open', 'high', 'low', 'close', 'vol'])
                    if not df.empty: break
                except: time.sleep(2)
            
            if df.empty: return pd.DataFrame()
            market_cache[cache_key], last_fetch_time[cache_key] = df.copy(), now

        if len(df) < 50: return pd.DataFrame()

        df["RSI"], df["RSI_9"] = get_rsi(df["close"]), get_rsi(df["close"], 9)
        df["EMA50"], df["EMA200"] = get_ema(df["close"], 50), get_ema(df["close"], 200)
        df["VWAP"] = get_vwap(df)
        df["ATR"] = get_atr(df)
        df["ATR_pct"] = (df["ATR"] / df["close"]) * 100
        df["EMA200_slope"] = (df["EMA200"] / df["EMA200"].shift(10)) - 1
        df["Drawdown"] = (df["close"] / df["close"].rolling(14).max()) - 1
        
        df["High_24h"], df["Low_24h"] = df["high"].rolling(24).max(), df["low"].rolling(24).min()
        df["Dist_High_24h"] = (df["High_24h"] - df["close"]) / df["close"]
        df["Dist_Low_24h"] = (df["close"] - df["Low_24h"]) / df["close"]
        df["EMA_dist"] = (df["close"] - df["EMA50"]) / df["EMA50"]
        df["EMA_slope"] = (df["EMA50"] / df["EMA50"].shift(5)) - 1
        df["Price_vs_VWAP"] = (df["close"] - df["VWAP"]) / df["VWAP"]
        df["ATR_ratio"] = df["ATR"] / df["close"]
        df["VOL_ratio"] = df["vol"] / (df["vol"].rolling(24).mean() + 1e-9)
        df["Vol_Spike"] = df["vol"] / (df["vol"].rolling(5).mean() + 1e-9)
        
        diff = df["High_24h"] - df["Low_24h"]
        df["Fib_618"] = df["Low_24h"] + (diff * 0.618)
        df["Dist_Fib_618"] = (df["close"] - df["Fib_618"]) / df["close"]

        df["Market_Trend"] = df["EMA200_slope"]
        p_low, p_high = df["low"].rolling(24).min().shift(1), df["high"].rolling(24).max().shift(1)
        df["Sweep_Low"] = ((df["low"] < p_low) & (df["close"] > p_low)).astype(int)
        df["Sweep_High"] = ((df["high"] > p_high) & (df["close"] < p_high)).astype(int)
        
        df['return_1h'], df['return_3h'], df['return_12h'] = df['close'].pct_change(1), df['close'].pct_change(3), df['close'].pct_change(12)
        df['RSI_lag1'], df['RSI_lag2'] = df["RSI"].shift(1), df["RSI"].shift(2)
        df['VOL_RATIO'] = df['vol'] / (df['vol'].rolling(20).mean() + 1e-9)
        df['vol_lag1'], df['RSI_Macro'] = df['vol'].shift(1), df["RSI"]

        return df.dropna().copy()
    except Exception as e: print(f"❌ Error Stats: {e}"); return pd.DataFrame()

import numpy as np

def detect_chart_scenario(df, df_15m=None, df_1h=None, df_4h=None):
    try:
        # 🛠️ Fix colonnes KuCoin
        if 'vol' in df.columns and 'volume' not in df.columns:
            df = df.rename(columns={'vol': 'volume'})

        df = df.copy().dropna()
        if len(df) < 100: return 3
        
        last = df.tail(100)
        c, h, l, v = last['close'].values, last['high'].values, last['low'].values, last['volume'].values
        ema200, ema50 = df['EMA200'], df['EMA50']

        # --- 1. VWAP INSTITUTIONNEL (Sécurisé) ---
        typical_price = (df['high'] + df['low'] + df['close']) / 3
        vwap = (typical_price * df['volume']).cumsum() / (df['volume'].cumsum() + 1e-9)
        price_above_vwap = df['close'].iloc[-1] > vwap.iloc[-1]
        price_below_vwap = df['close'].iloc[-1] < vwap.iloc[-1]
        
        # ========================
        # 🔥 VWAP BANDS (Institutional zones)
        # ========================
        vwap_std = df['close'].rolling(20).std()
        vwap_upper = vwap + vwap_std
        vwap_lower = vwap - vwap_std

        price_extended_high = df['close'].iloc[-1] > vwap_upper.iloc[-1]
        price_extended_low = df['close'].iloc[-1] < vwap_lower.iloc[-1]

        # --- 2. MULTI TIMEFRAME MACRO (15m, 1h, 4h) ---
        mtf_bull = mtf_bear = 0
        def analyze_tf(tf_df):
            if tf_df is None or len(tf_df) < 50 or 'EMA200' not in tf_df.columns: return 0
            slope = (tf_df['EMA200'].iloc[-1] / tf_df['EMA200'].iloc[-20]) - 1
            return 1 if slope > 0.0001 else (-1 if slope < -0.0001 else 0)

        for tf_data in [df_15m, df_1h, df_4h]:
            res = analyze_tf(tf_data)
            if res == 1: mtf_bull += 1
            elif res == -1: mtf_bear += 1

        # ========================
        # 🔥 ORDER BLOCK STRENGTH
        # ========================
        last_candles = df.tail(20)
        bullish_ob = False
        bearish_ob = False
        ob_strength = 0

        for i in range(len(last_candles) - 3):
            c1 = last_candles.iloc[i]
            c2 = last_candles.iloc[i + 1]

            body_size = abs(c2['close'] - c2['open'])
            candle_range = c2['high'] - c2['low'] + 1e-9

            strength = body_size / candle_range

            if c1['close'] < c1['open'] and c2['close'] > c2['open']:
                bullish_ob = True
                ob_strength = max(ob_strength, strength)

            if c1['close'] > c1['open'] and c2['close'] < c2['open']:
                bearish_ob = True
                ob_strength = max(ob_strength, strength)

        # --- 4. LIQUIDITY SWEEP ---
        recent_high, prev_high = np.max(h[-10:]), np.max(h[-30:-10])
        recent_low, prev_low = np.min(l[-10:]), np.min(l[-30:-10])
        liquidity_sweep_high = recent_high > prev_high and c[-1] < recent_high
        liquidity_sweep_low = recent_low < prev_low and c[-1] > recent_low
        liquidity_high_zone = np.mean(h[-50:])
        liquidity_low_zone = np.mean(l[-50:])

        near_liquidity_high = abs(c[-1] - liquidity_high_zone) / c[-1] < 0.002
        near_liquidity_low = abs(c[-1] - liquidity_low_zone) / c[-1] < 0.002

        # --- 5. STRUCTURE ET RSI ---
        range_start, range_end = np.mean(h[:20] - l[:20]), np.mean(h[-20:] - l[-20:])
        squeeze = range_end < range_start * 0.65
        
        slope_ema200 = (ema200.iloc[-1] / ema200.iloc[-50]) - 1
        slope_ema50 = (ema50.iloc[-1] / ema50.iloc[-20]) - 1
        trend_strength = abs(slope_ema200) + abs(slope_ema50)
        trend_up, trend_down = (slope_ema200 > 0 and slope_ema50 > 0), (slope_ema200 < 0 and slope_ema50 < 0)

        bos_up = np.max(h[-20:]) > np.max(h[-40:-20])
        bos_down = np.min(l[-20:]) < np.min(l[-40:-20])

        # ========================
        # 🔥 FAKE BREAKOUT FILTER (Déplacé ici pour corriger le bug !)
        # ========================
        fake_breakout_up = bos_up and c[-1] < recent_high
        fake_breakout_down = bos_down and c[-1] > recent_low

        bullish_div = bearish_div = False
        if 'RSI' in df.columns:
            rsi = df['RSI'].values
            bearish_div = np.max(h[-15:]) >= np.max(h[-30:-15]) and np.max(rsi[-15:]) < np.max(rsi[-30:-15])
            bullish_div = np.min(l[-15:]) <= np.min(l[-30:-15]) and np.min(rsi[-15:]) > np.min(rsi[-30:-15])

        vol_recent, vol_past = np.mean(v[-15:]), np.mean(v[-40:-15])
        vol_inc = vol_recent > vol_past * 1.2

        # 🔥 CONTINUATION PRO MAX
        if squeeze and trend_strength > 0.004 and price_above_vwap and mtf_trend_up and not price_extended_high:
            return 4

        # 🔴 REVERSAL SMART MONEY
        if (liquidity_sweep_high or bearish_ob or bearish_div or price_extended_high or near_liquidity_high) and trend_up:
            return 5

        if (liquidity_sweep_low or bullish_ob or bullish_div or price_extended_low or near_liquidity_low) and trend_down:
            return 5

        # 🟢 TREND INSTITUTIONNEL
        if trend_strength > 0.006 and vol_inc and ob_strength > 0.5:
            if price_above_vwap and mtf_trend_up and not fake_breakout_up:
                return 0
            if price_below_vwap and mtf_trend_down and not fake_breakout_down:
                return 1

        # 🟡 BREAKOUT PROPRE
        if vol_inc and not fake_breakout_up and bos_up and price_above_vwap:
            return 0

        if vol_inc and not fake_breakout_down and bos_down and price_below_vwap:
            return 1

        return 3 # RANGE / CHAOS par défaut
    except Exception as e:
        print(f"⚠️ Erreur Detect Scenario: {e}")
        return 3

async def predict_signal(symbol, timeframe="1h"): 
    try:
        memory_guard()
        symbol = str(symbol).strip().upper()
        df = prepare_features_sync(symbol, timeframe)
        if df.empty: return {"status": "error", "message": "Data insuffisante"}
        
        last_row = df.iloc[[-1]]
        prix, atr = float(last_row['close'].iloc[0]), float(last_row['ATR'].iloc[0])
        vwap = float(last_row['VWAP'].iloc[0])           
        vol_spike = float(last_row['Vol_Spike'].iloc[0]) 
        rsi_9 = float(last_row['RSI_9'].iloc[0])         
        
        # 🌊 1. COLLECTE DES DONNÉES (Flux, Sentiment, OI)
        futures_data = await fetch_kucoin_futures_data(symbol)
        oi, cvd = futures_data["oi"], futures_data["cvd"]
        p_sent = await get_crypto_sentiment(symbol)

        # 🏗️ 1.5 CHARGEMENT MULTI-TIMEFRAME MACRO
        df_15m = prepare_features_sync(symbol, "15m", limit_bars=150)
        df_1h = prepare_features_sync(symbol, "1h", limit_bars=150)
        df_4h = prepare_features_sync(symbol, "4h", limit_bars=150)

        # 🧠 2. DÉTECTION DU SCÉNARIO (L'œil + L'IA)
        regime_scaled = regime_scaler.transform(last_row[["ATR_pct", "EMA200_slope", "Drawdown", "RSI_Macro"]])
        regime_ml = int(regime_model.predict(regime_scaled)[0])
        
        # On passe les dataframes MTF à la fonction
        pattern_id = detect_chart_scenario(df, df_15m, df_1h, df_4h)
        final_scenario = pattern_id if pattern_id in [4, 5] else regime_ml

        # 🧠 3. CALCUL DES PROBABILITÉS IA
        ml_cols = ["RSI", "Dist_High_24h", "Dist_Low_24h", "EMA_dist", "EMA_slope", "ATR_ratio", "VOL_ratio"]
        ml_prob = float(ml_model.predict_proba(last_row[ml_cols])[0][1])
        time_cols = ['return_1h', 'return_3h', 'return_12h', 'RSI_lag1', 'RSI_lag2', 'vol_lag1', 'VOL_RATIO']
        time_prob = float(time_model.predict_proba(last_row[time_cols])[0][1])
        
        # 🧠 SÉLECTION DU CERVEAU DINO (Schizophrénie)
        sym_clean = symbol.replace("/", "").replace("USDT", "").replace("USD", "").upper()
        if 'dino_brains' in globals() and sym_clean in dino_brains:
            dino_prob = float(dino_brains[sym_clean].predict(last_row[ml_cols].values)[0])
        elif 'dino_brain' in globals() and dino_brain: # Sécurité : Fallback sur l'ancien modèle si besoin
            dino_prob = float(dino_brain.predict(last_row[ml_cols].values)[0])
        else:
            dino_prob = 0.5 # Si aucun cerveau n'est entraîné, on reste neutre
        # ⚡ 4. ENSEMBLE V30
        final_p, wt, wm, wl, ws = combine_scores(symbol, timeframe, time_prob, ml_prob, dino_prob, p_sent, final_scenario)

        # 🐋 5. BOOSTS (Smart Money & Scalping)
        smc_status = "AUCUN"
        if int(last_row["Sweep_Low"].iloc[0]) == 1 and cvd > 0:
            final_p = min(0.95, final_p + 0.20); smc_status = "LONG SWEEP + CVD 🐋"
        elif int(last_row["Sweep_High"].iloc[0]) == 1 and cvd < 0:
            final_p = max(0.05, final_p - 0.20); smc_status = "SHORT SWEEP + CVD 🐋"
            
        if timeframe in ["1m", "5m"]:
            if final_p > 0.5 and vol_spike > 1.5 and rsi_9 < 70: final_p = min(0.95, final_p + 0.10)
            elif final_p < 0.5 and vol_spike > 1.5 and rsi_9 > 30: final_p = max(0.05, final_p - 0.10)

        # 🛡️ 6. RÉCUPÉRATION ADN (Multi-Scénarios) + BOUCLIER V32
        with sqlite3.connect(DB_NAME) as conn:
            res = conn.execute("SELECT tp_mult, sl_mult, min_prob, min_tp_dist FROM agent_logic WHERE symbol = ? AND timeframe = ? AND regime = ?", (symbol, timeframe, final_scenario)).fetchone()
            if not res:
                res_def = conn.execute("SELECT tp_mult, sl_mult, min_prob, min_tp_dist FROM agent_logic WHERE symbol = 'ALL' AND timeframe = ?", (timeframe,)).fetchone()
                tp_m, sl_m, agent_min_prob, agent_min_tp_dist = res_def if res_def else (1.5, 1.0, 0.55, 0.002)
                conn.execute("INSERT OR IGNORE INTO agent_logic (symbol, timeframe, regime, tp_mult, sl_mult, min_prob, min_tp_dist, generation) VALUES (?, ?, ?, ?, ?, ?, ?, 1)", (symbol, timeframe, final_scenario, tp_m, sl_m, agent_min_prob, agent_min_tp_dist))
                conn.commit()
            else: 
                tp_m, sl_m, agent_min_prob, agent_min_tp_dist = res
                tp_m, sl_m = safe_agent_values(tp_m, sl_m)

            # 🔥 LE PATCH V32 (ANTI-CORRUPTION) EST LÀ :
            if sl_m > 5.0 or tp_m > 5.0:
                print(f"🚨 [V32] CORRUPTION DETECTÉE SUR {symbol} (SLx{round(sl_m,1)}) → NEUTRALISATION")
                tp_m, sl_m = 1.5, 1.0 # On force le reset local
            else:
                # On bride les valeurs par sécurité extrême
                tp_m = float(np.clip(tp_m, 0.5, 3.0))
                sl_m = float(np.clip(sl_m, 0.5, 2.0))

        # 📐 7. CALCUL DES NIVEAUX (TP/SL)
        tp = prix + (atr * tp_m) if final_p > 0.5 else prix - (atr * tp_m)
        sl = prix - (atr * sl_m) if final_p > 0.5 else prix + (atr * sl_m)

        strength = abs(final_p - 0.5) * 2
        conf_val = max(0, min(1, 1 - np.std([time_prob, ml_prob, dino_prob, p_sent])))
        composite_score = max(0, min(100, (strength * 45) + (conf_val * 40) + (15 if final_scenario in [0, 1] else 5)))

        # 🛑 8. SYSTÈME DE VÉTO
        dist_tp_pct = abs(tp - prix) / prix
        ema200_val = float(last_row["EMA200"].iloc[0]) 
        mkt_trend = float(last_row["Market_Trend"].iloc[0])
        print(f"🧠 [IA] {symbol} [{timeframe}] | Scénario: {final_scenario} | Proba: {round(final_p, 4)} | SMC: {smc_status}")
        
        veto, veto_reason = False, ""
        if final_p < agent_min_prob and final_p > (1 - agent_min_prob): veto, veto_reason = True, f"Confiance ({round(final_p, 2)}) < {round(agent_min_prob, 2)}"
        elif dist_tp_pct < agent_min_tp_dist: veto, veto_reason = True, "Gain potentiel trop faible"
        elif final_p < 0.5 and (prix > ema200_val or prix > vwap): veto, veto_reason = True, "Short Interdit (Prix > Bull Lines)"
        elif final_p > 0.5 and (prix < ema200_val or prix < vwap): veto, veto_reason = True, "Long Interdit (Prix < Bull Lines)"
        elif "BTC" not in symbol and "ETH" not in symbol and "SOL" not in symbol:
            if final_p < 0.5 and mkt_trend > 0.002: veto, veto_reason = True, "Trend Global Haussier"
            elif final_p > 0.5 and mkt_trend < -0.002: veto, veto_reason = True, "Trend Global Baissier"

        if veto:
            return {"symbol": symbol, "timeframe": timeframe, "status": "veto", "message": veto_reason, "scenario": final_scenario}

        # 🛑 8.5 ANTI-SPAM (On bloque ici si le signal a déjà été envoyé)
        if not should_send_signal(symbol, timeframe, final_p):
            return {"symbol": symbol, "timeframe": timeframe, "status": "veto", "message": "Anti-Spam Actif (Déjà envoyé)", "scenario": final_scenario}

        # 💾 9. ENREGISTREMENT DB
        db_task = (datetime.now(timezone.utc).isoformat(), symbol, timeframe, 'HAUSSIER' if final_p > 0.5 else 'BAISSIER', final_p, prix, tp, sl, 'EN_COURS', final_scenario, time_prob, ml_prob, dino_prob, p_sent, prix)
        await save_to_db(db_task)
        print(f"💾 [DB] Signal {symbol} [{timeframe}] sauvegardé. En attente du Bras...")

        return {
            "symbol": symbol, "timeframe": timeframe, "status": "success", "final_score": round(final_p, 4), "score": int(composite_score),
            "smart_money": smc_status, "price": prix, "tp": round(tp, 6), "sl": round(sl, 6), "scenario": final_scenario, "confluence": round(conf_val * 100, 1)
        }
    except Exception as e: return {"status": "error", "message": str(e)}
        
# =============================
def mutate_agent(symbol, timeframe, regime, success=True):
    import sqlite3, random, numpy as np, time
    
    for tentative in range(5): # 🔄 Tente 5 fois si la base est verrouillée
        try:
            with sqlite3.connect(DB_NAME, timeout=20) as conn:
                conn.row_factory = sqlite3.Row
                row = conn.execute(
                    "SELECT * FROM agent_logic WHERE symbol = ? AND timeframe = ? AND regime = ?",
                    (symbol, timeframe, regime)
                ).fetchone()

                if not row:
                    res_def = conn.execute(
                        "SELECT * FROM agent_logic WHERE symbol = 'ALL' AND timeframe = ?",
                        (timeframe,)
                    ).fetchone()
                    tp, sl, prob = (res_def['tp_mult'], res_def['sl_mult'], res_def['min_prob']) if res_def else (1.5, 1.0, 0.60)
                else:
                    tp, sl, prob = row['tp_mult'], row['sl_mult'], row['min_prob']

                # 🧠 MUTATION DOUCE
                if success:
                    new_tp = tp * random.uniform(1.01, 1.05)
                    new_sl = sl * random.uniform(0.98, 1.00)
                    new_prob = max(0.55, prob - 0.003)
                else:
                    new_tp = tp * random.uniform(0.95, 0.99)
                    new_sl = sl * random.uniform(1.01, 1.05)
                    new_prob = min(0.85, prob + 0.005)

                # 🛡️ HARD CLAMP (ANTI-EXPLOSION)
                new_tp = float(np.clip(new_tp, 0.5, 3.0))
                new_sl = float(np.clip(new_sl, 0.5, 2.0))

                conn.execute('''
                    INSERT OR REPLACE INTO agent_logic 
                    (symbol, timeframe, regime, tp_mult, sl_mult, min_prob, min_tp_dist, generation)
                    VALUES (?, ?, ?, ?, ?, ?, ?, 
                        COALESCE((SELECT generation FROM agent_logic WHERE symbol=? AND timeframe=? AND regime=?)+1, 1))
                ''', (
                    symbol, timeframe, regime,
                    new_tp, new_sl, new_prob, 0.001,
                    symbol, timeframe, regime
                ))
                conn.commit()
                return # ✅ Succès, on sort de la fonction

        except sqlite3.OperationalError as e:
            if "locked" in str(e):
                time.sleep(1) # La porte est bloquée, on attend 1 seconde
            else:
                print(f"❌ [ERREUR DB] Mutation échouée : {e}")
                break
        except Exception as e:
            print(f"🧬 Erreur Mutation V32: {e}")
            break


# =============================
# 🛡️ 3. PROTECTION LECTURE DB
# =============================
def safe_agent_values(tp_m, sl_m):
    import numpy as np

    # 🔥 Anti corruption
    if sl_m > 10 or tp_m > 10:
        print("🚨 CORRUPTION DETECTÉE → RESET LOCAL")
        return 1.5, 1.0

    tp_m = float(np.clip(tp_m, 0.5, 3.0))
    sl_m = float(np.clip(sl_m, 0.5, 2.0))

    return tp_m, sl_m


# --- 🌙 DREAM LOOP V31 (CORRIGÉE) ---
async def dream_simulation_loop():
    while True:
        if DREAM_MODE_ACTIVE:
            print("🌙 [DREAM MODE] Le bot rêve et s'entraîne à la salle... 🥊")
            for symbol in AUTO_SYMBOLS:
                for tf in AUTO_TIMEFRAMES: 
                    try:
                        df = prepare_features_sync(symbol, timeframe=tf, limit_bars=200)
                        if df.empty or len(df) < 50: continue
                        
                        # 1. On voyage dans le temps (une bougie au hasard)
                        idx = random.randint(10, len(df) - 30)
                        row_actuelle = df.iloc[idx]
                        prix = float(row_actuelle['close'])
                        atr = float(row_actuelle['ATR'])
                        
                        # 2. On récupère le régime de marché (Scénario)
                        # 2. On calcule le VRAI scénario de cette époque !
                        # On coupe le graphique pile au moment du passé qu'on a choisi
                        df_past = df.iloc[:idx+1] 
                        
                        # Le bot analyse la structure du marché de l'époque
                        current_scenario = detect_chart_scenario(df_past, df_past, df_past, df_past, df_past)
                        
                        # 3. On extrait l'ADN actuel (les multiplicateurs)
                        with sqlite3.connect(DB_NAME) as conn:
                            res = conn.execute("SELECT tp_mult, sl_mult FROM agent_logic WHERE symbol = ? AND timeframe = ? AND regime = ?", (symbol, tf, current_scenario)).fetchone()
                            tp_m, sl_m = res if res else (1.5, 1.0)
                        
                        # 4. Simulation de combat (Long ou Short) depuis CETTE époque
                        is_long = random.choice([True, False])
                        tp = prix + (atr * tp_m) if is_long else prix - (atr * tp_m)
                        sl = prix - (atr * sl_m) if is_long else prix + (atr * sl_m)
                        
                        # 5. On regarde les 24 bougies suivantes pour voir qui gagne
                        future_data = df.iloc[idx+1 : idx+25]
                        win = False
                        for _, row in future_data.iterrows():
                            if is_long:
                                if row['high'] >= tp: win = True; break
                                if row['low'] <= sl: win = False; break
                            else:
                                if row['low'] <= tp: win = True; break
                                if row['high'] >= sl: win = False; break
                                
                        # 6. 🧬 ÉVOLUTION : On récompense ou on punit l'ADN !
                        mutate_agent(symbol, tf, current_scenario, success=win)
                        await asyncio.sleep(5) 
                    except Exception as e: 
                        pass # On reste silencieux pendant le sommeil
        await asyncio.sleep(60)

def should_send_signal(symbol, timeframe, proba):
    global last_signals_sent
    key = (symbol, timeframe)
    now = time.time()
    
    # 🛡️ FIX : La prison est adaptée à la Timeframe
    tf_blocks = {"1m": 60, "5m": 300, "15m": 900, "1h": 3600, "4h": 14400}
    block_time = tf_blocks.get(timeframe, 3600) # Par défaut 1h si inconnu
    
    if key in last_signals_sent:
        last_proba, last_time = last_signals_sent[key]
        # Tolérance de 5% d'écart max, et blocage selon le tf_block
        if abs(last_proba - proba) < 0.05 and (now - last_time) < block_time:
            return False
            
    last_signals_sent[key] = (proba, now)
    return True
# --- ⚖️ JUDGE API V31 (VERSION NETTOYÉE POUR SL PHYSIQUE) ---
def run_judge_api(live_prices_json="{}"):
    try:
        import json
        try: 
            raw_prices = json.loads(live_prices_json) if isinstance(live_prices_json, str) else {}
            live_prices = {k.lower(): v for k, v in raw_prices.items()}
        except: 
            live_prices = {}

        with sqlite3.connect(DB_NAME, timeout=10) as conn: 
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            
            cursor.execute("SELECT * FROM signals WHERE status = 'EN_COURS'")
            trades = cursor.fetchall()
            
            closed_trades = []
            watching_info = [] # 🎯 Contiendra uniquement des dicts pour le Bras
            current_time = datetime.now(timezone.utc)
            max_age_minutes = {"15m": 30, "1h": 60, "4h": 240} # Le 4h expire après 4 heures
            
            for t in trades:
                try:
                    sym_db = t['symbol']
                    epic = sym_db.replace("/", "").replace("USDT", "USD").lower()
                    if not epic.endswith("m"): epic += "m"

                    is_live = epic in live_prices

                    if is_live and t['confirmed'] == 0:
                        cursor.execute("UPDATE signals SET confirmed = 1 WHERE id = ?", (t['id'],))
                        print(f"✅ [AUTO-CONFIRM] ID: {t['id']}")
                    
                    if not is_live:
                        date_str = t['date'].replace('Z', '+00:00')
                        signal_time = datetime.fromisoformat(date_str)
                        age_minutes = (current_time - signal_time).total_seconds() / 60.0
                        max_age = max_age_minutes.get(t['timeframe'], 60)
                        
                        if t['confirmed'] == 1:
                            cursor.execute("UPDATE signals SET status = 'FERMÉ PAR MT5 🛑' WHERE id = ?", (t['id'],))
                            closed_trades.append({"symbol": t['symbol'], "id": t['id'], "direction": t['direction']})
                        elif age_minutes > max_age:
                            cursor.execute("UPDATE signals SET status = 'EXPIRÉ ⏰' WHERE id = ?", (t['id'],))
                        continue

                    # --- CALCUL DU TRAILING ---
                    current_price = float(live_prices[epic])
                    sl_dyn, peak = t['sl'], t['peak_price']
                    new_peak = max(peak, current_price) if t['direction'] == 'HAUSSIER' else min(peak, current_price)
                    dist_totale = abs(t['tp'] - t['price'])
                    
                    if t['direction'] == 'HAUSSIER':
                        mouvement = current_price - t['price']
                    else:
                        mouvement = t['price'] - current_price
                        
                    progression = mouvement / dist_totale if (dist_totale > 0 and mouvement > 0) else 0
                    
                    nouveau_sl = sl_dyn
                    dist_sl_initial = abs(t['price'] - sl_dyn)

                    # LOGIQUE AGRESSIVE
                    if t['direction'] == 'HAUSSIER':
                        if progression >= 0.60: nouveau_sl = max(sl_dyn, t['price'] + (dist_totale * 0.50))
                        elif progression >= 0.40: nouveau_sl = max(sl_dyn, t['price'] + (dist_totale * 0.20))
                        elif progression >= 0.20: nouveau_sl = max(sl_dyn, t['price'] + (dist_totale * 0.02))
                        elif progression >= 0.10: nouveau_sl = max(sl_dyn, t['price'] - (dist_sl_initial * 0.50))
                    else:
                        if progression >= 0.60: nouveau_sl = min(sl_dyn, t['price'] - (dist_totale * 0.50))
                        elif progression >= 0.40: nouveau_sl = min(sl_dyn, t['price'] - (dist_totale * 0.20))
                        elif progression >= 0.20: nouveau_sl = min(sl_dyn, t['price'] - (dist_totale * 0.02))
                        elif progression >= 0.10: nouveau_sl = min(sl_dyn, t['price'] + (dist_sl_initial * 0.50))

                    cursor.execute("UPDATE signals SET peak_price = ?, sl = ? WHERE id = ?", (new_peak, nouveau_sl, t['id']))
                    
                    # --- DÉCISION DE CLÔTURE ---
                    outcome, reward = None, 0 
                    if t['direction'] == 'HAUSSIER':
                        if current_price >= t['tp']: outcome, reward = "GAGNÉ ✅", 3
                        elif current_price <= nouveau_sl: outcome, reward = ("GAGNÉ (PARTIEL) 💸", 1) if nouveau_sl > t['price'] else ("PERDU ❌", -5)
                    else:
                        if current_price <= t['tp']: outcome, reward = "GAGNÉ ✅", 3
                        elif current_price >= nouveau_sl: outcome, reward = ("GAGNÉ (PARTIEL) 💸", 1) if nouveau_sl < t['price'] else ("PERDU ❌", -5)

                    if outcome:
                        mutate_agent(t['symbol'], t['timeframe'], t['regime'], success=(reward>0))
                        cursor.execute("UPDATE signals SET status=? WHERE id=?", (outcome, t['id']))
                        closed_trades.append({"symbol": t['symbol'], "id": t['id'], "direction": t['direction']})
                    else:
                        # 🎯 ON N'AJOUTE DANS WATCHING QUE SI LE TRADE EST ENCORE OUVERT
                        watching_info.append({
                            "id": t['id'], 
                            "symbol": t['symbol'], 
                            "sl": round(nouveau_sl, 6), 
                            "prog": round(progression, 4)
                        })

                except Exception as e: 
                    print(f"⚠️ Erreur Trade ID {t['id']}: {e}")
            
            conn.commit()

        # Retour propre : data contient les fermetures, watching contient les mises à jour SL
        return {
            "status": "updates" if closed_trades else "waiting", 
            "data": closed_trades, 
            "watching": watching_info
        }
    except Exception as e: return {"status": "error", "message": str(e)}
# --- 📡 LECTURE RADAR OPTIMISÉE ---
def get_active_signals():
    try:
        with sqlite3.connect(DB_NAME, timeout=20) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            
            # 🎯 On ne prend QUE les signaux NON CONFIRMÉS pour ne pas surcharger le Bras
            cursor.execute("SELECT * FROM signals WHERE status = 'EN_COURS' AND confirmed = 0")
            signaux = [dict(row) for row in cursor.fetchall()]
            
            print(f"📡 [API RADAR] {len(signaux)} nouveaux signaux trouvés pour le Bras.")
            return signaux
            
    except Exception as e:
        print(f"❌ [ERREUR CRITIQUE CLOUD] get_active_signals a planté : {e}")
        return []

# --- 📊 GET BOT SKILLS (UI Dashboard) ---
def get_bot_skills():
    try:
        scenario_map = {0: "BULL RUN", 1: "BEAR RUN", 2: "PULLBACK", 3: "RANGE/CHAOS", 4: "SQUEEZE", 5: "REVERSAL"}
        with sqlite3.connect(DB_NAME) as conn:
            return [[r[0], r[1], scenario_map.get(r[2], "INCONNU"), f"x{round(r[3], 2)}", f"x{round(r[4], 2)}", f"{round(r[5]*100)}%", f"🧬 Gen {r[6]}"] 
                    for r in conn.cursor().execute("SELECT symbol, timeframe, regime, tp_mult, sl_mult, min_prob, generation FROM agent_logic ORDER BY symbol, timeframe, regime").fetchall()]
    except Exception as e: return [[f"Erreur: {str(e)}", "-", "-", "-", "-", "-", "-"]]

# --- 🧠 TRAINING ENGINE ---
def trigger_training(symbol="BTC/USD"):
    try:
        memory_guard()
        
        # 🎯 1. Nettoyage du symbole pour le nom de fichier (ex: BTC/USDT -> BTC)
        sym_clean = symbol.replace("/", "").replace("USDT", "").replace("USD", "").upper()
        if not sym_clean: sym_clean = "DEFAULT"
        
        # On crée un nom de fichier unique par crypto
        model_filename = f"dino_lgbm_{sym_clean}.txt"

        bars = exchange_sync.fetch_ohlcv(symbol, timeframe='1h', limit=1500)
        df = pd.DataFrame(bars, columns=['ts', 'open', 'high', 'low', 'close', 'vol'])
        if len(df) < 500: return f"❌ Historique insuffisant pour {symbol}."
        
        df_final = prepare_features_sync(symbol, '1h', limit_bars=1000)
        if df_final.empty or len(df_final) < 100: return f"❌ Données vides pour {symbol}."

        if LGBM_AVAILABLE:
            ml_cols = ["RSI", "Dist_High_24h", "Dist_Low_24h", "EMA_dist", "EMA_slope", "ATR_ratio", "VOL_ratio"]
            df_final['Target'] = (df_final['close'].shift(-1) > df_final['close']).astype(int)
            df_train = df_final.dropna(subset=ml_cols + ['Target'])
            X, y = df_train[ml_cols], df_train['Target']
            params = {'objective': 'binary', 'metric': 'binary_logloss', 'boosting_type': 'gbdt', 'learning_rate': 0.05, 'num_leaves': 31, 'verbose': -1}
            
            model = lgb.train(params, lgb.Dataset(X, label=y), 100)
            
            # 💾 2. Sauvegarde avec le nom unique
            model.save_model(model_filename)
            
            try:
                api = HfApi()
                api.upload_file(
                    path_or_fileobj=model_filename,
                    path_in_repo=model_filename,
                    repo_id=HF_REPO_ID, 
                    repo_type="dataset",
                    token=HF_TOKEN
                )
                print(f"☁️ [BACKUP] {model_filename} sauvegardé sur le Cloud !")
            except Exception as e:
                print(f"⚠️ Erreur backup modèle {sym_clean} : {e}")
            
            # 🧠 3. On met à jour le dictionnaire global des cerveaux DINO
            global dino_brains
            if 'dino_brains' not in globals():
                dino_brains = {}
            dino_brains[sym_clean] = lgb.Booster(model_file=model_filename)
            print(f"🧠 [IA] Cerveau {sym_clean} chargé en mémoire vive.")

        # Ces deux là restent uniques car ils sont sûrement génériques dans ton code
        global ml_model, time_model
        try:
            ml_model, time_model = joblib.load("ml_model_v9.pkl"), joblib.load("time_model.pkl")
        except: pass # Si les pkl n'existent pas, on ne crashe pas tout
        
        gc.collect()
        return f"✅ IA ré-entraînée et sauvegardée pour {symbol} ({model_filename})."
    except Exception as e: return f"❌ Erreur Training {symbol} : {e}"


# --- 🚀 MOTEURS AUTO-PILOTE ---
AUTO_SYMBOLS = ["BTC/USD", "ETH/USD", "SOL/USD"]
AUTO_TIMEFRAMES = ["15m", "1h", "4h"]

def set_bot_mode(mode):
    global DREAM_MODE_ACTIVE
    target_mode = mode[0] if isinstance(mode, list) else str(mode)
    if "LIVE" in target_mode.upper(): DREAM_MODE_ACTIVE, msg = False, "🛰️ Mode LIVE activé"
    else: DREAM_MODE_ACTIVE, msg = True, "🌙 Mode DREAM activé"
    return {"status": "success", "mode": "DREAM" if DREAM_MODE_ACTIVE else "LIVE", "message": msg}

async def universal_scanner_loop():
    print("👁️ [SCANNER] Le Cerveau Global est en ligne H24...")
    while True:
        for symbol in AUTO_SYMBOLS:
            for tf in AUTO_TIMEFRAMES:
                try:
                    # 🧹 1. ON NETTOIE AVANT DE PRÉDIRE (On vire les vieux fantômes)
                    clean_old_db_signals(symbol, tf)

                    # 🧠 2. ON PRÉDIT ET ON SAUVEGARDE LE NOUVEAU SIGNAL
                    pred = await predict_signal(symbol, timeframe=tf)
                    
                    if pred and pred.get("status") == "success":
                        print(f"✅ [SIGNAL VALIDÉ] {symbol} [{tf}] - Probabilité: {pred['final_score']}")
                        # Ligne clean_old supprimée d'ici pour ne pas tuer le trade !
                        active_signals_state[(symbol, tf)] = pred
                        
                        if not DREAM_MODE_ACTIVE:
                            print(f"🎯 [LIVE] Action requise pour {symbol} [{tf}] : Passage d'ordre.")
                            
                    await asyncio.sleep(5)
                except Exception as e: 
                    print(f"⚠️ Erreur Scanner {symbol} [{tf}]: {e}")
        await asyncio.sleep(60)

# --- ⚖️ TOOLS ---
def keep_alive_ping(): return {"status": "awake", "time": datetime.now(timezone.utc).isoformat()}
def confirm_trade_api(trade_id, real_price):
    try:
        with sqlite3.connect(DB_NAME) as conn:
            conn.row_factory, cursor = sqlite3.Row, conn.cursor()
            cursor.execute("SELECT price, tp, sl FROM signals WHERE id = ?", (int(trade_id),))
            t = cursor.fetchone()
            if not t: return {"status": "error"}
            ecart = float(real_price) - t['price']
            cursor.execute("UPDATE signals SET confirmed = 1, price = ?, tp = ?, sl = ?, peak_price = ? WHERE id = ?", (float(real_price), t['tp'] + ecart, t['sl'] + ecart, float(real_price), int(trade_id)))
            conn.commit()
            print(f"🤝 [BRIDGE] Le Bras a exécuté l'ID {trade_id} à {real_price}$. Synchronisation OK.")
        return {"status": "success"}
    except: return {"status": "error"}

def cancel_trade_api(trade_id):
    try:
        with sqlite3.connect(DB_NAME) as conn: conn.execute("UPDATE signals SET status = 'ANNULÉ 🗑️', confirmed = 0 WHERE id = ?", (int(trade_id),)); conn.commit()
        return {"status": "success"}
    except: return {"status": "error"}
def reset_trade_history():
    """
    Supprime UNIQUEMENT les trades (la table signals)
    Garde intacte la mémoire génétique (la table agents)
    """
    try:
        with sqlite3.connect(DB_NAME, timeout=10) as conn:
            cursor = conn.cursor()
            
            # 1. On supprime tout l'historique des trades (brouillons, validés, expirés...)
            cursor.execute("DELETE FROM signals")
            
            # 2. (Optionnel mais propre) On remet le compteur d'ID à 1
            cursor.execute("DELETE FROM sqlite_sequence WHERE name='signals'")
            
            conn.commit()
            
        return "✅ RESET RÉUSSI : Tous les trades ont été effacés. L'ADN est conservé !"
    except Exception as e:
        return f"❌ ERREUR RESET : {e}"

def training_wrapper(symbol, *args): return trigger_training(str(symbol).strip().upper() if isinstance(symbol, str) else "BTC/USD")


import atexit

@atexit.register
def close_session():
    global global_aio_session
    if global_aio_session and not global_aio_session.closed:
        asyncio.run(global_aio_session.close())

# --- 🎨 INTERFACE GRADIO V30 ---
with gr.Blocks(theme=gr.themes.Monochrome()) as iface:
    gr.Markdown("# 🦖 Alpha V30 Dinosaur Engine (Master Edition)")
    
    with gr.Tab("Admin"): 
        gr.Button("Forcer Entraînement", variant="stop").click(fn=training_wrapper, inputs=gr.Textbox(label="Symbole à recalibrer", value="BTC/USDT"), outputs=gr.Textbox(), api_name="trigger_training")
        gr.Markdown("---")
        gr.Markdown("### ⚠️ Zone de Danger")
        reset_btn = gr.Button("🗑️ Purger tous les trades (Garder l'ADN)", variant="primary")
        reset_out = gr.Textbox(label="Résultat du nettoyage")
        reset_btn.click(fn=reset_trade_history, inputs=[], outputs=reset_out, api_name="reset_trades")
        
    with gr.Tab("🎯 Prédictions"): 
        gr.Button("Predict", variant="primary").click(fn=predict_signal, inputs=[gr.Textbox(label="Symbole", value="BTC/USD"), gr.Dropdown(choices=["15m", "1h", "4h"], value="1h")], outputs=gr.JSON())
        
    with gr.Tab("⚖️ Système"):
        gr.Button("Judge").click(fn=run_judge_api, inputs=gr.Textbox(value="{}", visible=False), outputs=gr.JSON(), api_name="run_judge_api")
        gr.Button("Get Active", visible=False).click(fn=get_active_signals, outputs=gr.JSON(), api_name="get_active_signals")
        gr.Button("Ping", visible=False).click(fn=keep_alive_ping, outputs=gr.JSON(), api_name="keep_alive_ping")
        gr.Button("Set Mode", visible=False).click(fn=set_bot_mode, inputs=gr.Textbox(visible=False), outputs=gr.JSON(), api_name="set_bot_mode")
        
    with gr.Tab("📊 Stats (ADN)"):
        skills_table = gr.Dataframe(headers=["Marché", "Timeframe", "Scénario", "Cible (TP)", "Risque (SL)", "Peur Min.", "Génération"], value=get_bot_skills(), interactive=False)
        with gr.Row():
            gr.Button("🔄 Actualiser").click(get_bot_skills, outputs=skills_table)
            
    gr.Button(visible=False).click(fn=confirm_trade_api, inputs=[gr.Textbox(visible=False), gr.Textbox(visible=False)], outputs=gr.JSON(), api_name="confirm_trade_api")
    gr.Button(visible=False).click(fn=cancel_trade_api, inputs=gr.Textbox(visible=False), outputs=gr.JSON(), api_name="cancel_trade_api")
    iface.load(get_bot_skills, outputs=skills_table)


# --- ☁️ SAUVEGARDE CLOUD SILENCIEUSE ---
async def hf_backup_loop():
    while True:
        await asyncio.sleep(1800) # Attendre 30 minutes (1800 secondes)
        print("☁️ [SYSTEM] Lancement du backup planifié...")
        backup_db_to_hf()

import threading
_auto_pilot_started = False
def run_auto_pilot():
    global _auto_pilot_started
    if _auto_pilot_started: return
    _auto_pilot_started = True
    print("⏳ [SYSTEM] Attente 15s avant propulsion Master V31...")
    time.sleep(15)
    try:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        loop.create_task(universal_scanner_loop())
        loop.create_task(dream_simulation_loop())
        loop.create_task(hf_backup_loop()) # ⬅️ LA SAUVEGARDE EST BIEN ICI
        loop.run_forever()
    except Exception as e: print(f"⚠️ Erreur Auto-Pilote : {e}")

if __name__ == "__main__":
    try: threading.Thread(target=run_auto_pilot, daemon=True).start()
    except Exception as e: print(f"⚠️ Erreur Thread : {e}")
    # 🛑 LE LAUNCH DOIT TOUJOURS ÊTRE LA TOUTE DERNIÈRE LIGNE DU FICHIER !
    iface.launch(server_name="0.0.0.0", server_port=7860, show_api=True)