GoshawkVortexAI commited on
Commit
8deb5e7
·
verified ·
1 Parent(s): 9eeaa06

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -4199
app.py DELETED
@@ -1,4199 +0,0 @@
1
- # ================================================================
2
- # main.py — OKX QUANT ENGINE v5
3
- # ================================================================
4
- #
5
- # Advanced research pipeline:
6
- #
7
- # OHLCV
8
- # │
9
- # ├── Regime Switching
10
- # ├── Multi-Timeframe Proxy
11
- # ├── Volume / Structure
12
- # ├── Order-Flow Proxy
13
- # ├── Feature Interactions
14
- # │
15
- # ▼
16
- # ML Probability
17
- # │
18
- # ├── Probability calibration
19
- # ├── Historical similarity
20
- # ├── Walk-forward validation
21
- # ├── Overfit detector
22
- # ├── Monte Carlo robustness
23
- # └── Online performance memory
24
- # │
25
- # ▼
26
- # Dynamic TP/SL
27
- # │
28
- # ▼
29
- # Adaptive Meta Engine
30
- # │
31
- # ▼
32
- # Correlation-aware Risk
33
- # │
34
- # ▼
35
- # FINAL SETUP
36
- #
37
- # Research use only.
38
- # ================================================================
39
-
40
- import logging
41
- import sys
42
- import time
43
- import math
44
- import json
45
- import os
46
- from typing import List, Optional, Dict, Any, Tuple
47
-
48
- import numpy as np
49
- import pandas as pd
50
- import gradio as gr
51
-
52
- from config import (
53
- DEFAULT_SYMBOLS,
54
- TOP_N_DEFAULT,
55
- DEFAULT_ACCOUNT_EQUITY,
56
- TIMEFRAME,
57
- CANDLE_LIMIT,
58
- )
59
-
60
- from data_fetcher import (
61
- fetch_multiple,
62
- fetch_instruments,
63
- )
64
-
65
- from regime import detect_regime
66
- from volume_analysis import analyze_volume
67
- from risk_engine import evaluate_risk
68
- from veto import apply_veto
69
- from scorer import (
70
- compute_structure_score,
71
- score_token,
72
- rank_tokens,
73
- format_score_bar,
74
- quality_tier,
75
- )
76
- from feature_builder import (
77
- build_feature_dict,
78
- validate_features,
79
- )
80
- from ml_filter import TradeFilter
81
-
82
-
83
- # ================================================================
84
- # LOGGING
85
- # ================================================================
86
-
87
- logging.basicConfig(
88
- level=logging.INFO,
89
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
90
- stream=sys.stdout,
91
- )
92
-
93
- logger = logging.getLogger("main")
94
-
95
-
96
- # ================================================================
97
- # MODEL
98
- # ================================================================
99
-
100
- _TRADE_FILTER: Optional[TradeFilter] = TradeFilter.load_or_none()
101
-
102
-
103
- # ================================================================
104
- # CONSTANTS
105
- # ================================================================
106
-
107
- _TREND_ICON = {
108
- "bullish": "▲",
109
- "ranging": "◆",
110
- "bearish": "▼",
111
- }
112
-
113
- _DIR_LABEL = {
114
- 1: "LONG",
115
- -1: "SHORT",
116
- 0: "NONE",
117
- }
118
-
119
-
120
- # ================================================================
121
- # ADVANCED CONFIG
122
- # ================================================================
123
-
124
- ENGINE_CONFIG = {
125
-
126
- # Meta decision
127
- "meta_threshold": 0.64,
128
- "hard_threshold": 0.40,
129
-
130
- # Historical similarity
131
- "similarity_window": 32,
132
- "similarity_history": 400,
133
- "similarity_k": 30,
134
-
135
- # Walk-forward
136
- "wf_min_train": 60,
137
- "wf_test_size": 25,
138
- "wf_step": 25,
139
-
140
- # Monte Carlo
141
- "mc_iterations": 10000,
142
- "mc_sample_size": 100,
143
-
144
- # Dynamic TP / SL
145
- "tp_grid": [
146
- 1.25,
147
- 1.50,
148
- 1.75,
149
- 2.00,
150
- 2.25,
151
- 2.50,
152
- 2.75,
153
- 3.00,
154
- ],
155
-
156
- "sl_grid": [
157
- 0.75,
158
- 0.90,
159
- 1.00,
160
- 1.10,
161
- 1.25,
162
- 1.50,
163
- ],
164
-
165
- # Historical forward horizon
166
- "forward_bars": 5,
167
-
168
- # Noise
169
- "max_noise": 0.80,
170
-
171
- # Minimum samples
172
- "min_history": 30,
173
-
174
- # Online memory
175
- "memory_file": "trade_memory.json",
176
-
177
- # Meta weights
178
- "weights": {
179
-
180
- "rule": 0.13,
181
-
182
- "ml": 0.14,
183
-
184
- "trend": 0.08,
185
-
186
- "momentum": 0.07,
187
-
188
- "mtf": 0.08,
189
-
190
- "orderflow": 0.08,
191
-
192
- "similarity": 0.09,
193
-
194
- "walkforward": 0.09,
195
-
196
- "stability": 0.07,
197
-
198
- "montecarlo": 0.07,
199
-
200
- "noise": 0.05,
201
-
202
- "freshness": 0.05,
203
- },
204
- }
205
-
206
-
207
- # ================================================================
208
- # HELPERS
209
- # ================================================================
210
-
211
- def safe_float(
212
- x,
213
- default=0.0,
214
- ):
215
-
216
- try:
217
-
218
- x = float(x)
219
-
220
- if math.isfinite(x):
221
- return x
222
-
223
- except Exception:
224
- pass
225
-
226
- return default
227
-
228
-
229
- def clamp(
230
- x,
231
- lo=0.0,
232
- hi=1.0,
233
- ):
234
-
235
- return max(
236
- lo,
237
- min(
238
- hi,
239
- safe_float(x),
240
- ),
241
- )
242
-
243
-
244
- def sigmoid(x):
245
-
246
- x = np.clip(
247
- x,
248
- -40,
249
- 40,
250
- )
251
-
252
- return float(
253
- 1.0 /
254
- (
255
- 1.0 +
256
- np.exp(-x)
257
- )
258
- )
259
-
260
-
261
- def close_series(df):
262
-
263
- if (
264
- df is None
265
- or
266
- "close" not in df.columns
267
- ):
268
- return pd.Series(
269
- dtype=float
270
- )
271
-
272
- return (
273
- pd.to_numeric(
274
- df["close"],
275
- errors="coerce",
276
- )
277
- .replace(
278
- [np.inf, -np.inf],
279
- np.nan,
280
- )
281
- .dropna()
282
- )
283
-
284
-
285
- def returns_series(df):
286
-
287
- close = close_series(df)
288
-
289
- if len(close) < 3:
290
- return pd.Series(
291
- dtype=float
292
- )
293
-
294
- return (
295
- close
296
- .pct_change()
297
- .replace(
298
- [np.inf, -np.inf],
299
- np.nan,
300
- )
301
- .dropna()
302
- )
303
-
304
-
305
- # ================================================================
306
- # DIRECTION
307
- # ================================================================
308
-
309
- def infer_direction(
310
- trend,
311
- breakout,
312
- ):
313
-
314
- if (
315
- trend == "bullish"
316
- or
317
- breakout == 1
318
- ):
319
- return 1
320
-
321
- if (
322
- trend == "bearish"
323
- or
324
- breakout == -1
325
- ):
326
- return -1
327
-
328
- return 0
329
-
330
-
331
- # ================================================================
332
- # 1. REGIME SWITCHING
333
- # ================================================================
334
-
335
- def regime_switch_engine(
336
- df,
337
- regime_data,
338
- ):
339
-
340
- close = close_series(df)
341
- ret = returns_series(df)
342
-
343
- if len(close) < 40:
344
-
345
- return {
346
- "state": "UNKNOWN",
347
- "stability": 0.5,
348
- "transition_risk": 0.5,
349
- "volatility_state": "NORMAL",
350
- }
351
-
352
- recent = ret.iloc[-20:]
353
- older = ret.iloc[-60:-20]
354
-
355
- recent_vol = safe_float(
356
- recent.std()
357
- )
358
-
359
- old_vol = safe_float(
360
- older.std(),
361
- recent_vol,
362
- )
363
-
364
- vol_ratio = (
365
- recent_vol /
366
- max(
367
- old_vol,
368
- 1e-9,
369
- )
370
- )
371
-
372
- trend = regime_data.get(
373
- "trend",
374
- "ranging",
375
- )
376
-
377
- adx = safe_float(
378
- regime_data.get(
379
- "adx",
380
- 0,
381
- )
382
- )
383
-
384
- if (
385
- vol_ratio > 2.0
386
- and
387
- adx > 25
388
- ):
389
-
390
- state = (
391
- "VOLATILE_TREND"
392
- )
393
-
394
- elif (
395
- vol_ratio > 1.8
396
- ):
397
-
398
- state = (
399
- "VOLATILITY_EXPANSION"
400
- )
401
-
402
- elif (
403
- vol_ratio < 0.65
404
- and
405
- adx < 20
406
- ):
407
-
408
- state = "COMPRESSION"
409
-
410
- elif (
411
- adx >= 30
412
- and
413
- trend == "bullish"
414
- ):
415
-
416
- state = "STRONG_BULL"
417
-
418
- elif (
419
- adx >= 30
420
- and
421
- trend == "bearish"
422
- ):
423
-
424
- state = "STRONG_BEAR"
425
-
426
- elif adx < 20:
427
-
428
- state = "RANGE"
429
-
430
- else:
431
-
432
- state = "TRANSITION"
433
-
434
- transition_risk = clamp(
435
- abs(
436
- math.log(
437
- max(
438
- vol_ratio,
439
- 1e-6,
440
- )
441
- )
442
- )
443
- / 2.0
444
- )
445
-
446
- stability = clamp(
447
- 1.0 -
448
- transition_risk
449
- )
450
-
451
- if vol_ratio > 2.5:
452
- stability *= 0.70
453
-
454
- return {
455
-
456
- "state":
457
- state,
458
-
459
- "stability":
460
- clamp(stability),
461
-
462
- "transition_risk":
463
- clamp(
464
- transition_risk
465
- ),
466
-
467
- "volatility_state":
468
- (
469
- "EXPANSION"
470
- if vol_ratio > 1.5
471
- else
472
- "COMPRESSION"
473
- if vol_ratio < 0.7
474
- else
475
- "NORMAL"
476
- ),
477
-
478
- "vol_ratio":
479
- vol_ratio,
480
- }
481
-
482
-
483
- # ================================================================
484
- # 2. MULTI-TIMEFRAME PROXY
485
- # ================================================================
486
-
487
- def resample_ohlcv(
488
- df,
489
- rule,
490
- ):
491
-
492
- if df is None:
493
- return None
494
-
495
- x = df.copy()
496
-
497
- if not isinstance(
498
- x.index,
499
- pd.DatetimeIndex,
500
- ):
501
-
502
- return None
503
-
504
- required = {
505
- "open",
506
- "high",
507
- "low",
508
- "close",
509
- "volume",
510
- }
511
-
512
- if not required.issubset(
513
- x.columns
514
- ):
515
- return None
516
-
517
- try:
518
-
519
- y = x.resample(
520
- rule
521
- ).agg({
522
-
523
- "open": "first",
524
-
525
- "high": "max",
526
-
527
- "low": "min",
528
-
529
- "close": "last",
530
-
531
- "volume": "sum",
532
-
533
- }).dropna()
534
-
535
- return y
536
-
537
- except Exception:
538
-
539
- return None
540
-
541
-
542
- def timeframe_direction(
543
- frame,
544
- ):
545
-
546
- if frame is None:
547
- return 0, 0.5
548
-
549
- close = close_series(
550
- frame
551
- )
552
-
553
- if len(close) < 15:
554
- return 0, 0.5
555
-
556
- r = (
557
- close.iloc[-1] /
558
- close.iloc[-11] -
559
- 1
560
- )
561
-
562
- if r > 0.003:
563
- d = 1
564
-
565
- elif r < -0.003:
566
- d = -1
567
-
568
- else:
569
- d = 0
570
-
571
- strength = clamp(
572
- sigmoid(
573
- abs(r) /
574
- 0.01
575
- ) - 0.5
576
- ) * 2
577
-
578
- return d, strength
579
-
580
-
581
- def multi_timeframe_engine(
582
- df,
583
- direction,
584
- ):
585
-
586
- results = {}
587
-
588
- frames = {
589
- "BASE": df,
590
- }
591
-
592
- # These require a DatetimeIndex.
593
- if isinstance(
594
- df.index,
595
- pd.DatetimeIndex,
596
- ):
597
-
598
- for name, rule in {
599
- "15M": "15min",
600
- "1H": "1h",
601
- "4H": "4h",
602
- }.items():
603
-
604
- r = resample_ohlcv(
605
- df,
606
- rule,
607
- )
608
-
609
- if r is not None:
610
- frames[name] = r
611
-
612
- values = []
613
-
614
- for name, frame in frames.items():
615
-
616
- d, strength = (
617
- timeframe_direction(
618
- frame
619
- )
620
- )
621
-
622
- results[name] = {
623
- "direction": d,
624
- "strength": strength,
625
- }
626
-
627
- if direction != 0:
628
- values.append(
629
- 1.0
630
- if d == direction
631
- else
632
- 0.0
633
- if d != 0
634
- else
635
- 0.5
636
- )
637
-
638
- agreement = (
639
- float(
640
- np.mean(values)
641
- )
642
- if values
643
- else
644
- 0.5
645
- )
646
-
647
- return {
648
- "timeframes":
649
- results,
650
-
651
- "agreement":
652
- clamp(agreement),
653
- }
654
-
655
-
656
- # ================================================================
657
- # 3. ORDER-FLOW PROXY
658
- # ================================================================
659
-
660
- def orderflow_proxy(
661
- df,
662
- ):
663
-
664
- if df is None:
665
- return {
666
- "buy_pressure": 0.5,
667
- "sell_pressure": 0.5,
668
- "imbalance": 0.0,
669
- "absorption": 0.5,
670
- "breakout_quality": 0.5,
671
- }
672
-
673
- required = {
674
- "open",
675
- "high",
676
- "low",
677
- "close",
678
- "volume",
679
- }
680
-
681
- if not required.issubset(
682
- df.columns
683
- ):
684
-
685
- return {
686
- "buy_pressure": 0.5,
687
- "sell_pressure": 0.5,
688
- "imbalance": 0.0,
689
- "absorption": 0.5,
690
- "breakout_quality": 0.5,
691
- }
692
-
693
- x = df.tail(50).copy()
694
-
695
- rng = (
696
- x["high"] -
697
- x["low"]
698
- ).replace(
699
- 0,
700
- np.nan,
701
- )
702
-
703
- body = (
704
- x["close"] -
705
- x["open"]
706
- )
707
-
708
- body_ratio = (
709
- body / rng
710
- ).replace(
711
- [np.inf, -np.inf],
712
- np.nan,
713
- ).fillna(0)
714
-
715
- buy = (
716
- ((body_ratio + 1) / 2)
717
- * x["volume"]
718
- )
719
-
720
- sell = (
721
- ((1 - body_ratio) / 2)
722
- * x["volume"]
723
- )
724
-
725
- buy_total = safe_float(
726
- buy.sum()
727
- )
728
-
729
- sell_total = safe_float(
730
- sell.sum()
731
- )
732
-
733
- total = max(
734
- buy_total + sell_total,
735
- 1e-9,
736
- )
737
-
738
- buy_pressure = (
739
- buy_total /
740
- total
741
- )
742
-
743
- sell_pressure = (
744
- sell_total /
745
- total
746
- )
747
-
748
- imbalance = (
749
- buy_pressure -
750
- sell_pressure
751
- )
752
-
753
- # Absorption proxy:
754
- # high volume + small body
755
- volume_z = (
756
- x["volume"] -
757
- x["volume"].rolling(
758
- 20
759
- ).mean()
760
- ) / (
761
- x["volume"].rolling(
762
- 20
763
- ).std() + 1e-9
764
- )
765
-
766
- small_body = (
767
- abs(body_ratio) < 0.25
768
- )
769
-
770
- absorption_hits = (
771
- (
772
- volume_z > 1.5
773
- )
774
- &
775
- small_body
776
- )
777
-
778
- absorption = clamp(
779
- float(
780
- absorption_hits.mean()
781
- )
782
- )
783
-
784
- last_range = safe_float(
785
- rng.iloc[-1],
786
- 1e-9,
787
- )
788
-
789
- last_body = abs(
790
- safe_float(
791
- body.iloc[-1]
792
- )
793
- )
794
-
795
- breakout_quality = clamp(
796
- last_body /
797
- max(
798
- last_range,
799
- 1e-9,
800
- )
801
- )
802
-
803
- return {
804
-
805
- "buy_pressure":
806
- clamp(
807
- buy_pressure
808
- ),
809
-
810
- "sell_pressure":
811
- clamp(
812
- sell_pressure
813
- ),
814
-
815
- "imbalance":
816
- imbalance,
817
-
818
- "absorption":
819
- absorption,
820
-
821
- "breakout_quality":
822
- breakout_quality,
823
- }
824
-
825
-
826
- # ================================================================
827
- # 4. FEATURE INTERACTIONS
828
- # ================================================================
829
-
830
- def feature_interactions(
831
- regime_data,
832
- volume_data,
833
- ml_prob,
834
- meta_base=0.5,
835
- ):
836
-
837
- adx = safe_float(
838
- regime_data.get(
839
- "adx",
840
- 0,
841
- )
842
- )
843
-
844
- vol_ratio = safe_float(
845
- regime_data.get(
846
- "vol_ratio",
847
- 1,
848
- )
849
- )
850
-
851
- volume_score = clamp(
852
- volume_data.get(
853
- "volume_score",
854
- 0.5,
855
- )
856
- )
857
-
858
- breakout = (
859
- volume_data.get(
860
- "breakout",
861
- 0,
862
- )
863
- )
864
-
865
- ml = (
866
- 0.5
867
- if ml_prob is None
868
- else clamp(
869
- ml_prob
870
- )
871
- )
872
-
873
- adx_component = clamp(
874
- adx / 50
875
- )
876
-
877
- volume_component = clamp(
878
- vol_ratio / 2
879
- )
880
-
881
- interactions = {
882
-
883
- "adx_volume":
884
- clamp(
885
- adx_component *
886
- volume_component
887
- ),
888
-
889
- "adx_breakout":
890
- clamp(
891
- adx_component *
892
- (
893
- 1
894
- if breakout != 0
895
- else 0.5
896
- )
897
- ),
898
-
899
- "ml_volume":
900
- clamp(
901
- ml *
902
- volume_score
903
- ),
904
-
905
- "ml_regime":
906
- clamp(
907
- ml *
908
- adx_component
909
- ),
910
-
911
- "breakout_volume":
912
- clamp(
913
- (
914
- 1
915
- if breakout != 0
916
- else 0.5
917
- )
918
- *
919
- volume_score
920
- ),
921
-
922
- "composite":
923
- clamp(
924
- np.mean(
925
- [
926
- adx_component *
927
- volume_component,
928
-
929
- ml *
930
- volume_score,
931
-
932
- adx_component *
933
- (
934
- 1
935
- if breakout != 0
936
- else 0.5
937
- ),
938
- ]
939
- )
940
- ),
941
- }
942
-
943
- return interactions
944
-
945
-
946
- # ================================================================
947
- # 5. HISTORICAL SIMILARITY
948
- # ================================================================
949
-
950
- def build_state_vector(
951
- df,
952
- direction,
953
- ):
954
-
955
- close = close_series(df)
956
- ret = returns_series(df)
957
-
958
- if len(close) < 40:
959
- return None
960
-
961
- recent = close.iloc[-20:]
962
-
963
- returns = (
964
- recent
965
- .pct_change()
966
- .dropna()
967
- )
968
-
969
- features = [
970
-
971
- safe_float(
972
- returns.mean()
973
- ),
974
-
975
- safe_float(
976
- returns.std()
977
- ),
978
-
979
- safe_float(
980
- returns.skew()
981
- ),
982
-
983
- safe_float(
984
- abs(
985
- returns.mean()
986
- ) /
987
- (
988
- returns.std()
989
- + 1e-9
990
- )
991
- ),
992
-
993
- safe_float(
994
- close.iloc[-1] /
995
- close.iloc[-11] -
996
- 1
997
- ),
998
-
999
- safe_float(
1000
- close.iloc[-1] /
1001
- close.iloc[-21] -
1002
- 1
1003
- ),
1004
-
1005
- safe_float(
1006
- close.iloc[-1] /
1007
- close.iloc[-31] -
1008
- 1
1009
- ),
1010
-
1011
- ]
1012
-
1013
- return np.asarray(
1014
- features,
1015
- dtype=float,
1016
- )
1017
-
1018
-
1019
- def historical_similarity(
1020
- df,
1021
- direction,
1022
- ):
1023
-
1024
- close = close_series(df)
1025
-
1026
- if (
1027
- direction == 0
1028
- or
1029
- len(close) < 100
1030
- ):
1031
-
1032
- return {
1033
- "score": 0.5,
1034
- "matches": 0,
1035
- "edge": 0.5,
1036
- "median_return": 0.0,
1037
- }
1038
-
1039
- current = build_state_vector(
1040
- df,
1041
- direction,
1042
- )
1043
-
1044
- if current is None:
1045
- return {
1046
- "score": 0.5,
1047
- "matches": 0,
1048
- "edge": 0.5,
1049
- "median_return": 0.0,
1050
- }
1051
-
1052
- values = close.values
1053
-
1054
- history = min(
1055
- len(values) - 10,
1056
- ENGINE_CONFIG[
1057
- "similarity_history"
1058
- ],
1059
- )
1060
-
1061
- window = ENGINE_CONFIG[
1062
- "similarity_window"
1063
- ]
1064
-
1065
- candidates = []
1066
-
1067
- for end in range(
1068
- window,
1069
- history,
1070
- ):
1071
-
1072
- start = (
1073
- len(values) -
1074
- history +
1075
- end -
1076
- window
1077
- )
1078
-
1079
- if start < 0:
1080
- continue
1081
-
1082
- segment = (
1083
- close.iloc[
1084
- start:
1085
- start + window
1086
- ]
1087
- )
1088
-
1089
- if len(segment) < window:
1090
- continue
1091
-
1092
- temp = pd.DataFrame({
1093
- "close": segment
1094
- })
1095
-
1096
- state = build_state_vector(
1097
- temp,
1098
- direction,
1099
- )
1100
-
1101
- if state is None:
1102
- continue
1103
-
1104
- dist = np.linalg.norm(
1105
- current -
1106
- state
1107
- )
1108
-
1109
- future_index = (
1110
- start +
1111
- window +
1112
- ENGINE_CONFIG[
1113
- "forward_bars"
1114
- ]
1115
- )
1116
-
1117
- if (
1118
- future_index >=
1119
- len(values)
1120
- ):
1121
- continue
1122
-
1123
- entry = values[
1124
- start + window - 1
1125
- ]
1126
-
1127
- future = values[
1128
- future_index
1129
- ]
1130
-
1131
- if entry <= 0:
1132
- continue
1133
-
1134
- forward_return = (
1135
- (
1136
- future /
1137
- entry
1138
- ) - 1
1139
- ) * direction
1140
-
1141
- candidates.append(
1142
- (
1143
- dist,
1144
- forward_return,
1145
- )
1146
- )
1147
-
1148
- if not candidates:
1149
-
1150
- return {
1151
- "score": 0.5,
1152
- "matches": 0,
1153
- "edge": 0.5,
1154
- "median_return": 0.0,
1155
- }
1156
-
1157
- candidates.sort(
1158
- key=lambda x: x[0]
1159
- )
1160
-
1161
- selected = candidates[
1162
- :ENGINE_CONFIG[
1163
- "similarity_k"
1164
- ]
1165
- ]
1166
-
1167
- returns = np.asarray(
1168
- [
1169
- x[1]
1170
- for x in selected
1171
- ]
1172
- )
1173
-
1174
- edge = float(
1175
- np.mean(
1176
- returns > 0
1177
- )
1178
- )
1179
-
1180
- median_return = float(
1181
- np.median(
1182
- returns
1183
- )
1184
- )
1185
-
1186
- score = clamp(
1187
- 0.5 +
1188
- (
1189
- edge -
1190
- 0.5
1191
- ) * 1.5
1192
- )
1193
-
1194
- return {
1195
-
1196
- "score":
1197
- score,
1198
-
1199
- "matches":
1200
- len(selected),
1201
-
1202
- "edge":
1203
- edge,
1204
-
1205
- "median_return":
1206
- median_return,
1207
- }
1208
-
1209
-
1210
- # ================================================================
1211
- # 6. HISTORICAL EDGE
1212
- # ================================================================
1213
-
1214
- def historical_edge(
1215
- df,
1216
- direction,
1217
- horizon=5,
1218
- ):
1219
-
1220
- close = close_series(
1221
- df
1222
- )
1223
-
1224
- if (
1225
- direction == 0
1226
- or
1227
- len(close) <
1228
- horizon + 30
1229
- ):
1230
-
1231
- return {
1232
- "hit_rate": 0.5,
1233
- "samples": 0,
1234
- "returns": [],
1235
- }
1236
-
1237
- values = close.values
1238
-
1239
- outcomes = []
1240
-
1241
- for i in range(
1242
- 0,
1243
- len(values) -
1244
- horizon,
1245
- ):
1246
-
1247
- entry = values[i]
1248
-
1249
- future = values[
1250
- i + horizon
1251
- ]
1252
-
1253
- if entry <= 0:
1254
- continue
1255
-
1256
- move = (
1257
- future /
1258
- entry -
1259
- 1
1260
- )
1261
-
1262
- signed = (
1263
- move *
1264
- direction
1265
- )
1266
-
1267
- outcomes.append(
1268
- signed
1269
- )
1270
-
1271
- if len(outcomes) < 30:
1272
-
1273
- return {
1274
- "hit_rate": 0.5,
1275
- "samples":
1276
- len(outcomes),
1277
- "returns":
1278
- outcomes,
1279
- }
1280
-
1281
- hit = float(
1282
- np.mean(
1283
- np.asarray(
1284
- outcomes
1285
- ) > 0
1286
- )
1287
- )
1288
-
1289
- return {
1290
-
1291
- "hit_rate":
1292
- clamp(hit),
1293
-
1294
- "samples":
1295
- len(outcomes),
1296
-
1297
- "returns":
1298
- outcomes,
1299
- }
1300
-
1301
-
1302
- # ================================================================
1303
- # 7. MONTE CARLO
1304
- # ================================================================
1305
-
1306
- def monte_carlo_engine(
1307
- hit_rate,
1308
- rr=2.0,
1309
- ):
1310
-
1311
- hit_rate = clamp(
1312
- hit_rate,
1313
- 0.01,
1314
- 0.99,
1315
- )
1316
-
1317
- iterations = ENGINE_CONFIG[
1318
- "mc_iterations"
1319
- ]
1320
-
1321
- sample_size = ENGINE_CONFIG[
1322
- "mc_sample_size"
1323
- ]
1324
-
1325
- rng = np.random.default_rng()
1326
-
1327
- wins = (
1328
- rng.random(
1329
- (
1330
- iterations,
1331
- sample_size,
1332
- )
1333
- )
1334
- <
1335
- hit_rate
1336
- )
1337
-
1338
- pnl = (
1339
- wins * rr -
1340
- (~wins) * 1.0
1341
- )
1342
-
1343
- total = pnl.sum(
1344
- axis=1
1345
- )
1346
-
1347
- positive_probability = float(
1348
- np.mean(
1349
- total > 0
1350
- )
1351
- )
1352
-
1353
- cumulative = np.cumsum(
1354
- pnl,
1355
- axis=1,
1356
- )
1357
-
1358
- running_max = np.maximum.accumulate(
1359
- cumulative,
1360
- axis=1,
1361
- )
1362
-
1363
- drawdown = (
1364
- running_max -
1365
- cumulative
1366
- )
1367
-
1368
- max_dd = np.max(
1369
- drawdown,
1370
- axis=1,
1371
- )
1372
-
1373
- worst5 = float(
1374
- np.percentile(
1375
- total,
1376
- 5,
1377
- )
1378
- )
1379
-
1380
- median_dd = float(
1381
- np.median(
1382
- max_dd
1383
- )
1384
- )
1385
-
1386
- robustness = clamp(
1387
- 0.70 *
1388
- positive_probability
1389
- +
1390
- 0.30 *
1391
- sigmoid(
1392
- worst5 /
1393
- max(
1394
- sample_size,
1395
- 1
1396
- )
1397
- )
1398
- )
1399
-
1400
- return {
1401
-
1402
- "robustness":
1403
- robustness,
1404
-
1405
- "positive_probability":
1406
- positive_probability,
1407
-
1408
- "median_pnl":
1409
- float(
1410
- np.median(
1411
- total
1412
- )
1413
- ),
1414
-
1415
- "worst5":
1416
- worst5,
1417
-
1418
- "median_max_dd":
1419
- median_dd,
1420
- }
1421
-
1422
-
1423
- # ================================================================
1424
- # 8. DYNAMIC TP / SL
1425
- # ================================================================
1426
-
1427
- def optimize_tp_sl(
1428
- df,
1429
- direction,
1430
- ):
1431
-
1432
- close = close_series(
1433
- df
1434
- )
1435
-
1436
- if (
1437
- direction == 0
1438
- or
1439
- len(close) < 80
1440
- ):
1441
-
1442
- return {
1443
-
1444
- "sl_atr":
1445
- 1.0,
1446
-
1447
- "tp_atr":
1448
- 2.0,
1449
-
1450
- "expectancy":
1451
- 0.0,
1452
-
1453
- "robustness":
1454
- 0.5,
1455
- }
1456
-
1457
- returns = (
1458
- close
1459
- .pct_change()
1460
- .dropna()
1461
- )
1462
-
1463
- volatility = safe_float(
1464
- returns.std(),
1465
- 0.01,
1466
- )
1467
-
1468
- # Approximate ATR percentage
1469
- atr_pct = max(
1470
- volatility *
1471
- 1.5,
1472
- 1e-5,
1473
- )
1474
-
1475
- best = None
1476
-
1477
- for sl in ENGINE_CONFIG[
1478
- "sl_grid"
1479
- ]:
1480
-
1481
- for tp in ENGINE_CONFIG[
1482
- "tp_grid"
1483
- ]:
1484
-
1485
- rr = tp / sl
1486
-
1487
- # Historical directional edge
1488
- edge = historical_edge(
1489
- df,
1490
- direction,
1491
- horizon=5,
1492
- )[
1493
- "hit_rate"
1494
- ]
1495
-
1496
- expectancy = (
1497
- edge * rr
1498
- -
1499
- (1 - edge)
1500
- )
1501
-
1502
- # Penalize excessive TP
1503
- # relative to current volatility
1504
- tp_penalty = max(
1505
- 0,
1506
- (
1507
- tp *
1508
- atr_pct
1509
- ) /
1510
- 0.05 -
1511
- 1
1512
- )
1513
-
1514
- adjusted = (
1515
- expectancy -
1516
- 0.10 *
1517
- tp_penalty
1518
- )
1519
-
1520
- candidate = {
1521
-
1522
- "sl_atr":
1523
- sl,
1524
-
1525
- "tp_atr":
1526
- tp,
1527
-
1528
- "expectancy":
1529
- adjusted,
1530
-
1531
- "robustness":
1532
- clamp(
1533
- 0.5 +
1534
- adjusted /
1535
- 2
1536
- ),
1537
-
1538
- }
1539
-
1540
- if (
1541
- best is None
1542
- or
1543
- candidate[
1544
- "expectancy"
1545
- ]
1546
- >
1547
- best[
1548
- "expectancy"
1549
- ]
1550
- ):
1551
-
1552
- best = candidate
1553
-
1554
- return best or {
1555
-
1556
- "sl_atr": 1.0,
1557
- "tp_atr": 2.0,
1558
- "expectancy": 0.0,
1559
- "robustness": 0.5,
1560
- }
1561
-
1562
-
1563
- # ================================================================
1564
- # 9. WALK-FORWARD
1565
- # ================================================================
1566
-
1567
- def walk_forward_validation(
1568
- df,
1569
- direction,
1570
- ):
1571
-
1572
- close = close_series(
1573
- df
1574
- )
1575
-
1576
- minimum = (
1577
- ENGINE_CONFIG[
1578
- "wf_min_train"
1579
- ]
1580
- +
1581
- ENGINE_CONFIG[
1582
- "wf_test_size"
1583
- ]
1584
- )
1585
-
1586
- if (
1587
- direction == 0
1588
- or
1589
- len(close) < minimum
1590
- ):
1591
-
1592
- return {
1593
-
1594
- "score":
1595
- 0.5,
1596
-
1597
- "expectancy":
1598
- 0.0,
1599
-
1600
- "profit_factor":
1601
- 1.0,
1602
-
1603
- "folds":
1604
- 0,
1605
- }
1606
-
1607
- values = close.values
1608
-
1609
- train = ENGINE_CONFIG[
1610
- "wf_min_train"
1611
- ]
1612
-
1613
- test = ENGINE_CONFIG[
1614
- "wf_test_size"
1615
- ]
1616
-
1617
- step = ENGINE_CONFIG[
1618
- "wf_step"
1619
- ]
1620
-
1621
- fold_scores = []
1622
- fold_returns = []
1623
-
1624
- start = train
1625
-
1626
- while (
1627
- start + test <=
1628
- len(values)
1629
- ):
1630
-
1631
- train_data = values[
1632
- :start
1633
- ]
1634
-
1635
- test_data = values[
1636
- start:
1637
- start + test
1638
- ]
1639
-
1640
- if len(train_data) < 20:
1641
- break
1642
-
1643
- train_ret = (
1644
- train_data[-1] /
1645
- train_data[-11] -
1646
- 1
1647
- )
1648
-
1649
- expected_direction = (
1650
- 1
1651
- if train_ret > 0
1652
- else
1653
- -1
1654
- )
1655
-
1656
- test_returns = (
1657
- test_data[-1] /
1658
- test_data[0] -
1659
- 1
1660
- )
1661
-
1662
- signed = (
1663
- test_returns *
1664
- direction
1665
- )
1666
-
1667
- # Whether current direction
1668
- # was aligned with observed test
1669
- correct = (
1670
- 1.0
1671
- if signed > 0
1672
- else
1673
- 0.0
1674
- )
1675
-
1676
- # Small contribution from
1677
- # consistency of train direction
1678
- consistency = (
1679
- 1.0
1680
- if expected_direction ==
1681
- direction
1682
- else
1683
- 0.5
1684
- )
1685
-
1686
- score = (
1687
- 0.75 *
1688
- correct
1689
- +
1690
- 0.25 *
1691
- consistency
1692
- )
1693
-
1694
- fold_scores.append(
1695
- score
1696
- )
1697
-
1698
- fold_returns.append(
1699
- signed
1700
- )
1701
-
1702
- start += step
1703
-
1704
- if not fold_scores:
1705
-
1706
- return {
1707
-
1708
- "score":
1709
- 0.5,
1710
-
1711
- "expectancy":
1712
- 0.0,
1713
-
1714
- "profit_factor":
1715
- 1.0,
1716
-
1717
- "folds":
1718
- 0,
1719
- }
1720
-
1721
- score = float(
1722
- np.mean(
1723
- fold_scores
1724
- )
1725
- )
1726
-
1727
- positive = [
1728
- x
1729
- for x in fold_returns
1730
- if x > 0
1731
- ]
1732
-
1733
- negative = [
1734
- abs(x)
1735
- for x in fold_returns
1736
- if x < 0
1737
- ]
1738
-
1739
- profit_factor = (
1740
- sum(positive) /
1741
- max(
1742
- sum(negative),
1743
- 1e-9,
1744
- )
1745
- )
1746
-
1747
- return {
1748
-
1749
- "score":
1750
- clamp(score),
1751
-
1752
- "expectancy":
1753
- float(
1754
- np.mean(
1755
- fold_returns
1756
- )
1757
- ),
1758
-
1759
- "profit_factor":
1760
- float(
1761
- min(
1762
- profit_factor,
1763
- 10
1764
- )
1765
- ),
1766
-
1767
- "folds":
1768
- len(
1769
- fold_scores
1770
- ),
1771
- }
1772
-
1773
-
1774
- # ================================================================
1775
- # 10. OVERFIT DETECTOR
1776
- # ================================================================
1777
-
1778
- def overfit_detector(
1779
- df,
1780
- direction,
1781
- ):
1782
-
1783
- close = close_series(
1784
- df
1785
- )
1786
-
1787
- if (
1788
- direction == 0
1789
- or
1790
- len(close) < 100
1791
- ):
1792
-
1793
- return {
1794
-
1795
- "risk":
1796
- 0.5,
1797
-
1798
- "train_pf":
1799
- 1.0,
1800
-
1801
- "test_pf":
1802
- 1.0,
1803
- }
1804
-
1805
- values = close.values
1806
-
1807
- split = int(
1808
- len(values) *
1809
- 0.70
1810
- )
1811
-
1812
- train = values[
1813
- :split
1814
- ]
1815
-
1816
- test = values[
1817
- split:
1818
- ]
1819
-
1820
- def pf(
1821
- arr
1822
- ):
1823
-
1824
- r = (
1825
- arr[1:] /
1826
- arr[:-1] -
1827
- 1
1828
- ) * direction
1829
-
1830
- pos = r[
1831
- r > 0
1832
- ].sum()
1833
-
1834
- neg = abs(
1835
- r[
1836
- r < 0
1837
- ].sum()
1838
- )
1839
-
1840
- return float(
1841
- pos /
1842
- max(
1843
- neg,
1844
- 1e-9
1845
- )
1846
- )
1847
-
1848
- train_pf = pf(
1849
- train
1850
- )
1851
-
1852
- test_pf = pf(
1853
- test
1854
- )
1855
-
1856
- degradation = clamp(
1857
- (
1858
- train_pf -
1859
- test_pf
1860
- )
1861
- /
1862
- max(
1863
- abs(train_pf),
1864
- 1e-9
1865
- )
1866
- )
1867
-
1868
- risk = clamp(
1869
- degradation
1870
- )
1871
-
1872
- return {
1873
-
1874
- "risk":
1875
- risk,
1876
-
1877
- "train_pf":
1878
- train_pf,
1879
-
1880
- "test_pf":
1881
- test_pf,
1882
-
1883
- "degradation":
1884
- degradation,
1885
- }
1886
-
1887
-
1888
- # ================================================================
1889
- # 11. NOISE ENGINE
1890
- # ================================================================
1891
-
1892
- def noise_engine(
1893
- df,
1894
- ):
1895
-
1896
- ret = returns_series(
1897
- df
1898
- )
1899
-
1900
- if len(ret) < 20:
1901
- return 0.5
1902
-
1903
- signs = np.sign(
1904
- ret.iloc[-80:]
1905
- )
1906
-
1907
- alternation = float(
1908
- np.mean(
1909
- signs.iloc[1:].values
1910
- !=
1911
- signs.iloc[:-1].values
1912
- )
1913
- )
1914
-
1915
- up = float(
1916
- np.mean(
1917
- signs > 0
1918
- )
1919
- )
1920
-
1921
- down = float(
1922
- np.mean(
1923
- signs < 0
1924
- )
1925
- )
1926
-
1927
- probs = [
1928
- up,
1929
- down,
1930
- ]
1931
-
1932
- entropy = 0
1933
-
1934
- for p in probs:
1935
-
1936
- if p > 0:
1937
-
1938
- entropy -= (
1939
- p *
1940
- math.log(
1941
- p,
1942
- 2,
1943
- )
1944
- )
1945
-
1946
- entropy = clamp(
1947
- entropy
1948
- )
1949
-
1950
- return clamp(
1951
- 0.55 *
1952
- entropy
1953
- +
1954
- 0.45 *
1955
- alternation
1956
- )
1957
-
1958
-
1959
- # ================================================================
1960
- # 12. SIGNAL FRESHNESS
1961
- # ================================================================
1962
-
1963
- def signal_freshness(
1964
- df,
1965
- direction,
1966
- ):
1967
-
1968
- ret = returns_series(
1969
- df
1970
- )
1971
-
1972
- if (
1973
- direction == 0
1974
- or
1975
- len(ret) < 20
1976
- ):
1977
- return 0.5
1978
-
1979
- signed = (
1980
- ret *
1981
- direction
1982
- )
1983
-
1984
- threshold = (
1985
- signed.std() *
1986
- 0.75
1987
- )
1988
-
1989
- idx = np.where(
1990
- signed.values >
1991
- threshold
1992
- )[0]
1993
-
1994
- if len(idx) == 0:
1995
- return 0.5
1996
-
1997
- bars_since = (
1998
- len(signed) -
1999
- 1 -
2000
- idx[-1]
2001
- )
2002
-
2003
- return clamp(
2004
- math.exp(
2005
- -bars_since /
2006
- 12
2007
- )
2008
- )
2009
-
2010
-
2011
- # ================================================================
2012
- # 13. MOMENTUM
2013
- # ================================================================
2014
-
2015
- def momentum_score(
2016
- df,
2017
- direction,
2018
- ):
2019
-
2020
- close = close_series(
2021
- df
2022
- )
2023
-
2024
- if (
2025
- direction == 0
2026
- or
2027
- len(close) < 30
2028
- ):
2029
- return 0.5
2030
-
2031
- vals = []
2032
-
2033
- for w in [
2034
- 5,
2035
- 10,
2036
- 20,
2037
- ]:
2038
-
2039
- move = (
2040
- close.iloc[-1] /
2041
- close.iloc[-w - 1] -
2042
- 1
2043
- )
2044
-
2045
- signed = (
2046
- move *
2047
- direction
2048
- )
2049
-
2050
- vals.append(
2051
- sigmoid(
2052
- signed /
2053
- 0.02
2054
- )
2055
- )
2056
-
2057
- return clamp(
2058
- np.mean(vals)
2059
- )
2060
-
2061
-
2062
- # ================================================================
2063
- # 14. TREND PERSISTENCE
2064
- # ================================================================
2065
-
2066
- def trend_persistence(
2067
- df,
2068
- direction,
2069
- ):
2070
-
2071
- ret = returns_series(
2072
- df
2073
- )
2074
-
2075
- if (
2076
- direction == 0
2077
- or
2078
- len(ret) < 20
2079
- ):
2080
- return 0.5
2081
-
2082
- signed = (
2083
- ret.iloc[-40:] *
2084
- direction
2085
- )
2086
-
2087
- return clamp(
2088
- np.mean(
2089
- signed > 0
2090
- )
2091
- )
2092
-
2093
-
2094
- # ================================================================
2095
- # 15. CORRELATION ENGINE
2096
- # ================================================================
2097
-
2098
- def correlation_engine(
2099
- results,
2100
- ):
2101
-
2102
- symbols = list(
2103
- results.keys()
2104
- )
2105
-
2106
- if len(symbols) < 2:
2107
-
2108
- return {
2109
- s: 0.5
2110
- for s in symbols
2111
- }
2112
-
2113
- # Without raw data from every symbol,
2114
- # use direction concentration as a
2115
- # conservative portfolio correlation proxy.
2116
-
2117
- long_count = sum(
2118
- 1
2119
- for s in symbols
2120
- if results[s][
2121
- "direction"
2122
- ] == 1
2123
- )
2124
-
2125
- short_count = sum(
2126
- 1
2127
- for s in symbols
2128
- if results[s][
2129
- "direction"
2130
- ] == -1
2131
- )
2132
-
2133
- n = max(
2134
- len(symbols),
2135
- 1,
2136
- )
2137
-
2138
- concentration = max(
2139
- long_count,
2140
- short_count,
2141
- ) / n
2142
-
2143
- exposure_score = clamp(
2144
- 1.0 -
2145
- concentration *
2146
- 0.5
2147
- )
2148
-
2149
- return {
2150
- s:
2151
- exposure_score
2152
- for s in symbols
2153
- }
2154
-
2155
-
2156
- # ================================================================
2157
- # 16. ONLINE MEMORY
2158
- # ================================================================
2159
-
2160
- class OnlineMemory:
2161
-
2162
- def __init__(
2163
- self,
2164
- path=None,
2165
- ):
2166
-
2167
- self.path = (
2168
- path
2169
- or
2170
- ENGINE_CONFIG[
2171
- "memory_file"
2172
- ]
2173
- )
2174
-
2175
- self.data = []
2176
-
2177
- self.load()
2178
-
2179
- def load(self):
2180
-
2181
- if not os.path.exists(
2182
- self.path
2183
- ):
2184
- return
2185
-
2186
- try:
2187
-
2188
- with open(
2189
- self.path,
2190
- "r",
2191
- encoding="utf-8",
2192
- ) as f:
2193
-
2194
- self.data = json.load(
2195
- f
2196
- )
2197
-
2198
- except Exception:
2199
-
2200
- self.data = []
2201
-
2202
- def save(self):
2203
-
2204
- try:
2205
-
2206
- with open(
2207
- self.path,
2208
- "w",
2209
- encoding="utf-8",
2210
- ) as f:
2211
-
2212
- json.dump(
2213
- self.data[
2214
- -5000:
2215
- ],
2216
- f,
2217
- indent=2,
2218
- )
2219
-
2220
- except Exception as e:
2221
-
2222
- logger.warning(
2223
- f"Memory save error: {e}"
2224
- )
2225
-
2226
- def add(
2227
- self,
2228
- symbol,
2229
- direction,
2230
- meta_score,
2231
- outcome,
2232
- regime,
2233
- ):
2234
-
2235
- self.data.append({
2236
-
2237
- "time":
2238
- time.time(),
2239
-
2240
- "symbol":
2241
- symbol,
2242
-
2243
- "direction":
2244
- direction,
2245
-
2246
- "meta_score":
2247
- meta_score,
2248
-
2249
- "outcome":
2250
- outcome,
2251
-
2252
- "regime":
2253
- regime,
2254
- })
2255
-
2256
- self.save()
2257
-
2258
- def performance(
2259
- self,
2260
- regime=None,
2261
- ):
2262
-
2263
- rows = self.data
2264
-
2265
- if regime is not None:
2266
-
2267
- rows = [
2268
- x
2269
- for x in rows
2270
- if x.get(
2271
- "regime"
2272
- ) == regime
2273
- ]
2274
-
2275
- if len(rows) < 20:
2276
-
2277
- return 0.5
2278
-
2279
- outcomes = [
2280
- x.get(
2281
- "outcome",
2282
- 0,
2283
- )
2284
- for x in rows[-200:]
2285
- ]
2286
-
2287
- return clamp(
2288
- np.mean(
2289
- np.asarray(
2290
- outcomes
2291
- ) > 0
2292
- )
2293
- )
2294
-
2295
-
2296
- _MEMORY = OnlineMemory()
2297
-
2298
-
2299
- # ================================================================
2300
- # 17. META ENGINE
2301
- # ================================================================
2302
-
2303
- def adaptive_meta_engine(
2304
- df,
2305
- direction,
2306
- regime_data,
2307
- volume_data,
2308
- scores,
2309
- ml_prob,
2310
- ):
2311
-
2312
- # --------------------------------------------------------------
2313
- # Base components
2314
- # --------------------------------------------------------------
2315
-
2316
- rule_score = clamp(
2317
- np.mean([
2318
- clamp(
2319
- scores.get(
2320
- "regime_score",
2321
- 0.5
2322
- )
2323
- ),
2324
-
2325
- clamp(
2326
- scores.get(
2327
- "volume_score",
2328
- 0.5
2329
- )
2330
- ),
2331
-
2332
- clamp(
2333
- scores.get(
2334
- "structure_score",
2335
- 0.5
2336
- )
2337
- ),
2338
-
2339
- clamp(
2340
- scores.get(
2341
- "confidence_score",
2342
- 0.5
2343
- )
2344
- ),
2345
- ])
2346
- )
2347
-
2348
- ml_score = (
2349
- 0.5
2350
- if ml_prob is None
2351
- else clamp(
2352
- ml_prob
2353
- )
2354
- )
2355
-
2356
- trend_score = (
2357
- trend_persistence(
2358
- df,
2359
- direction
2360
- )
2361
- )
2362
-
2363
- momentum = momentum_score(
2364
- df,
2365
- direction,
2366
- )
2367
-
2368
- noise = noise_engine(
2369
- df
2370
- )
2371
-
2372
- freshness = signal_freshness(
2373
- df,
2374
- direction,
2375
- )
2376
-
2377
- regime = regime_switch_engine(
2378
- df,
2379
- regime_data,
2380
- )
2381
-
2382
- mtf = multi_timeframe_engine(
2383
- df,
2384
- direction,
2385
- )
2386
-
2387
- orderflow = orderflow_proxy(
2388
- df
2389
- )
2390
-
2391
- similarity = historical_similarity(
2392
- df,
2393
- direction,
2394
- )
2395
-
2396
- wf = walk_forward_validation(
2397
- df,
2398
- direction,
2399
- )
2400
-
2401
- overfit = overfit_detector(
2402
- df,
2403
- direction,
2404
- )
2405
-
2406
- hist = historical_edge(
2407
- df,
2408
- direction,
2409
- ENGINE_CONFIG[
2410
- "forward_bars"
2411
- ],
2412
- )
2413
-
2414
- mc = monte_carlo_engine(
2415
- hist[
2416
- "hit_rate"
2417
- ],
2418
- rr=2.0,
2419
- )
2420
-
2421
- dynamic_exit = optimize_tp_sl(
2422
- df,
2423
- direction,
2424
- )
2425
-
2426
- interactions = feature_interactions(
2427
- regime_data,
2428
- volume_data,
2429
- ml_prob,
2430
- )
2431
-
2432
- memory_score = _MEMORY.performance(
2433
- regime.get(
2434
- "state"
2435
- )
2436
- )
2437
-
2438
- # --------------------------------------------------------------
2439
- # Meta
2440
- # --------------------------------------------------------------
2441
-
2442
- w = ENGINE_CONFIG[
2443
- "weights"
2444
- ]
2445
-
2446
- components = {
2447
-
2448
- "rule":
2449
- rule_score,
2450
-
2451
- "ml":
2452
- ml_score,
2453
-
2454
- "trend":
2455
- trend_score,
2456
-
2457
- "momentum":
2458
- momentum,
2459
-
2460
- "mtf":
2461
- mtf[
2462
- "agreement"
2463
- ],
2464
-
2465
- "orderflow":
2466
- clamp(
2467
- 0.5 +
2468
- orderflow[
2469
- "imbalance"
2470
- ] / 2
2471
- ),
2472
-
2473
- "similarity":
2474
- similarity[
2475
- "score"
2476
- ],
2477
-
2478
- "walkforward":
2479
- wf[
2480
- "score"
2481
- ],
2482
-
2483
- "stability":
2484
- regime[
2485
- "stability"
2486
- ],
2487
-
2488
- "montecarlo":
2489
- mc[
2490
- "robustness"
2491
- ],
2492
-
2493
- "noise":
2494
- 1.0 - noise,
2495
-
2496
- "freshness":
2497
- freshness,
2498
- }
2499
-
2500
- meta = 0.0
2501
-
2502
- for key, value in components.items():
2503
-
2504
- meta += (
2505
- w[key] *
2506
- value
2507
- )
2508
-
2509
- # Interaction bonus
2510
- meta += (
2511
- 0.05 *
2512
- interactions[
2513
- "composite"
2514
- ]
2515
- )
2516
-
2517
- # Online memory modifier
2518
- meta *= (
2519
- 0.95 +
2520
- 0.10 *
2521
- memory_score
2522
- )
2523
-
2524
- # Overfitting penalty
2525
- meta *= (
2526
- 1.0 -
2527
- 0.25 *
2528
- overfit[
2529
- "risk"
2530
- ]
2531
- )
2532
-
2533
- # Uncertainty
2534
- uncertainty = np.mean([
2535
- abs(
2536
- ml_score -
2537
- hist[
2538
- "hit_rate"
2539
- ]
2540
- ),
2541
-
2542
- 1.0 -
2543
- regime[
2544
- "stability"
2545
- ],
2546
-
2547
- noise,
2548
-
2549
- 1.0 -
2550
- mtf[
2551
- "agreement"
2552
- ],
2553
-
2554
- overfit[
2555
- "risk"
2556
- ],
2557
- ])
2558
-
2559
- meta *= (
2560
- 1.0 -
2561
- 0.18 *
2562
- uncertainty
2563
- )
2564
-
2565
- meta = clamp(
2566
- meta
2567
- )
2568
-
2569
- # --------------------------------------------------------------
2570
- # Hard veto
2571
- # --------------------------------------------------------------
2572
-
2573
- veto_reasons = []
2574
-
2575
- if noise >= ENGINE_CONFIG[
2576
- "max_noise"
2577
- ]:
2578
-
2579
- veto_reasons.append(
2580
- "HIGH_NOISE"
2581
- )
2582
-
2583
- if regime[
2584
- "stability"
2585
- ] < 0.20:
2586
-
2587
- veto_reasons.append(
2588
- "REGIME_UNSTABLE"
2589
- )
2590
-
2591
- if (
2592
- hist["samples"] >=
2593
- ENGINE_CONFIG[
2594
- "min_history"
2595
- ]
2596
- and
2597
- hist["hit_rate"] < 0.32
2598
- ):
2599
-
2600
- veto_reasons.append(
2601
- "WEAK_HISTORY"
2602
- )
2603
-
2604
- if (
2605
- overfit["risk"] >
2606
- 0.75
2607
- ):
2608
-
2609
- veto_reasons.append(
2610
- "OVERFIT_RISK"
2611
- )
2612
-
2613
- if meta < ENGINE_CONFIG[
2614
- "hard_threshold"
2615
- ]:
2616
-
2617
- veto_reasons.append(
2618
- "LOW_META"
2619
- )
2620
-
2621
- hard_veto = (
2622
- len(veto_reasons) > 0
2623
- )
2624
-
2625
- approved = (
2626
- not hard_veto
2627
- and
2628
- meta >= ENGINE_CONFIG[
2629
- "meta_threshold"
2630
- ]
2631
- )
2632
-
2633
- return {
2634
-
2635
- "meta_score":
2636
- meta,
2637
-
2638
- "approved":
2639
- approved,
2640
-
2641
- "hard_veto":
2642
- hard_veto,
2643
-
2644
- "veto_reasons":
2645
- veto_reasons,
2646
-
2647
- "uncertainty":
2648
- uncertainty,
2649
-
2650
- "components":
2651
- components,
2652
-
2653
- "regime":
2654
- regime,
2655
-
2656
- "mtf":
2657
- mtf,
2658
-
2659
- "orderflow":
2660
- orderflow,
2661
-
2662
- "similarity":
2663
- similarity,
2664
-
2665
- "walkforward":
2666
- wf,
2667
-
2668
- "overfit":
2669
- overfit,
2670
-
2671
- "historical":
2672
- hist,
2673
-
2674
- "montecarlo":
2675
- mc,
2676
-
2677
- "dynamic_exit":
2678
- dynamic_exit,
2679
-
2680
- "interactions":
2681
- interactions,
2682
-
2683
- "memory_score":
2684
- memory_score,
2685
-
2686
- "noise":
2687
- noise,
2688
-
2689
- "freshness":
2690
- freshness,
2691
-
2692
- "trend":
2693
- trend_score,
2694
-
2695
- "momentum":
2696
- momentum,
2697
- }
2698
-
2699
-
2700
- # ================================================================
2701
- # 18. SINGLE SYMBOL
2702
- # ================================================================
2703
-
2704
- def analyze_single(
2705
- symbol,
2706
- df,
2707
- account_equity,
2708
- consec_losses=0,
2709
- equity_drawdown_pct=0.0,
2710
- use_ml=True,
2711
- ):
2712
-
2713
- # --------------------------------------------------------------
2714
- # Rule
2715
- # --------------------------------------------------------------
2716
-
2717
- regime_data = detect_regime(
2718
- df
2719
- )
2720
-
2721
- volume_data = analyze_volume(
2722
- df,
2723
- atr_series=
2724
- regime_data[
2725
- "atr_series"
2726
- ],
2727
- )
2728
-
2729
- structure_sc = (
2730
- compute_structure_score(
2731
- regime_data
2732
- )
2733
- )
2734
-
2735
- direction = infer_direction(
2736
- regime_data[
2737
- "trend"
2738
- ],
2739
- volume_data[
2740
- "breakout"
2741
- ],
2742
- )
2743
-
2744
- vetoed, veto_reason = (
2745
- apply_veto(
2746
- regime_data,
2747
- volume_data,
2748
- structure_sc,
2749
- direction=
2750
- direction,
2751
- )
2752
- )
2753
-
2754
- scores = score_token(
2755
- regime_data,
2756
- volume_data,
2757
- vetoed,
2758
- )
2759
-
2760
- # --------------------------------------------------------------
2761
- # ML
2762
- # --------------------------------------------------------------
2763
-
2764
- ml_prob = None
2765
- ml_approved = None
2766
- ml_reject_reason = ""
2767
-
2768
- if (
2769
- use_ml
2770
- and
2771
- _TRADE_FILTER is not None
2772
- and
2773
- not vetoed
2774
- ):
2775
-
2776
- try:
2777
-
2778
- feat = build_feature_dict(
2779
- regime_data,
2780
- volume_data,
2781
- scores,
2782
- )
2783
-
2784
- if validate_features(
2785
- feat
2786
- ):
2787
-
2788
- result = (
2789
- _TRADE_FILTER.predict(
2790
- regime_data,
2791
- volume_data,
2792
- scores,
2793
- )
2794
- )
2795
-
2796
- ml_prob = (
2797
- result.probability
2798
- )
2799
-
2800
- ml_approved = (
2801
- result.approved
2802
- )
2803
-
2804
- ml_reject_reason = (
2805
- result.reject_reason
2806
- )
2807
-
2808
- except Exception as e:
2809
-
2810
- logger.warning(
2811
- f"{symbol}: ML error: {e}"
2812
- )
2813
-
2814
- # --------------------------------------------------------------
2815
- # Advanced engine
2816
- # --------------------------------------------------------------
2817
-
2818
- meta = adaptive_meta_engine(
2819
- df,
2820
- direction,
2821
- regime_data,
2822
- volume_data,
2823
- scores,
2824
- ml_prob,
2825
- )
2826
-
2827
- ml_pass = (
2828
- ml_approved is None
2829
- or
2830
- ml_approved
2831
- )
2832
-
2833
- final_approved = (
2834
- not vetoed
2835
- and
2836
- ml_pass
2837
- and
2838
- meta[
2839
- "approved"
2840
- ]
2841
- )
2842
-
2843
- # --------------------------------------------------------------
2844
- # Risk
2845
- # --------------------------------------------------------------
2846
-
2847
- risk_data = {}
2848
-
2849
- if final_approved:
2850
-
2851
- risk_data = evaluate_risk(
2852
-
2853
- close=float(
2854
- df[
2855
- "close"
2856
- ].iloc[-1]
2857
- ),
2858
-
2859
- atr=regime_data[
2860
- "atr"
2861
- ],
2862
-
2863
- atr_pct=regime_data[
2864
- "atr_pct"
2865
- ],
2866
-
2867
- regime_score=
2868
- regime_data[
2869
- "regime_score"
2870
- ],
2871
-
2872
- vol_ratio=
2873
- regime_data[
2874
- "vol_ratio"
2875
- ],
2876
-
2877
- volume_score=
2878
- volume_data[
2879
- "volume_score"
2880
- ],
2881
-
2882
- regime_confidence=
2883
- regime_data[
2884
- "regime_confidence"
2885
- ],
2886
-
2887
- vol_compressed=
2888
- regime_data[
2889
- "vol_compressed"
2890
- ],
2891
-
2892
- consec_losses=
2893
- consec_losses,
2894
-
2895
- equity_drawdown_pct=
2896
- equity_drawdown_pct,
2897
-
2898
- account_equity=
2899
- account_equity,
2900
- )
2901
-
2902
- # Dynamic TP/SL overlay
2903
- exit_data = meta[
2904
- "dynamic_exit"
2905
- ]
2906
-
2907
- risk_data[
2908
- "optimized_sl_atr"
2909
- ] = exit_data[
2910
- "sl_atr"
2911
- ]
2912
-
2913
- risk_data[
2914
- "optimized_tp_atr"
2915
- ] = exit_data[
2916
- "tp_atr"
2917
- ]
2918
-
2919
- risk_data[
2920
- "optimized_expectancy"
2921
- ] = exit_data[
2922
- "expectancy"
2923
- ]
2924
-
2925
- # --------------------------------------------------------------
2926
- # Result
2927
- # --------------------------------------------------------------
2928
-
2929
- return {
2930
-
2931
- "symbol":
2932
- symbol,
2933
-
2934
- "close":
2935
- float(
2936
- df[
2937
- "close"
2938
- ].iloc[-1]
2939
- ),
2940
-
2941
- "trend":
2942
- regime_data[
2943
- "trend"
2944
- ],
2945
-
2946
- "adx":
2947
- regime_data[
2948
- "adx"
2949
- ],
2950
-
2951
- "di_plus":
2952
- regime_data[
2953
- "di_plus"
2954
- ],
2955
-
2956
- "di_minus":
2957
- regime_data[
2958
- "di_minus"
2959
- ],
2960
-
2961
- "vol_ratio":
2962
- regime_data[
2963
- "vol_ratio"
2964
- ],
2965
-
2966
- "vol_compressed":
2967
- regime_data[
2968
- "vol_compressed"
2969
- ],
2970
-
2971
- "vol_expanding":
2972
- regime_data[
2973
- "vol_expanding"
2974
- ],
2975
-
2976
- "dist_atr":
2977
- regime_data[
2978
- "dist_atr"
2979
- ],
2980
-
2981
- "regime_confidence":
2982
- regime_data[
2983
- "regime_confidence"
2984
- ],
2985
-
2986
- "breakout":
2987
- volume_data[
2988
- "breakout"
2989
- ],
2990
-
2991
- "direction":
2992
- direction,
2993
-
2994
- "rule_vetoed":
2995
- vetoed,
2996
-
2997
- "veto_reason":
2998
- veto_reason,
2999
-
3000
- "ml_prob":
3001
- ml_prob,
3002
-
3003
- "ml_approved":
3004
- ml_approved,
3005
-
3006
- "ml_reject_reason":
3007
- ml_reject_reason,
3008
-
3009
- "final_approved":
3010
- final_approved,
3011
-
3012
- "regime_score":
3013
- scores[
3014
- "regime_score"
3015
- ],
3016
-
3017
- "volume_score":
3018
- scores[
3019
- "volume_score"
3020
- ],
3021
-
3022
- "structure_score":
3023
- scores[
3024
- "structure_score"
3025
- ],
3026
-
3027
- "confidence_score":
3028
- scores[
3029
- "confidence_score"
3030
- ],
3031
-
3032
- "total_score":
3033
- scores[
3034
- "total_score"
3035
- ],
3036
-
3037
- "meta":
3038
- meta,
3039
-
3040
- "risk":
3041
- risk_data,
3042
- }
3043
-
3044
-
3045
- # ================================================================
3046
- # STATUS
3047
- # ================================================================
3048
-
3049
- def ml_status(d):
3050
-
3051
- if _TRADE_FILTER is None:
3052
- return "NO_MODEL"
3053
-
3054
- if d[
3055
- "rule_vetoed"
3056
- ]:
3057
- return "RULE_VET"
3058
-
3059
- if d[
3060
- "ml_prob"
3061
- ] is None:
3062
- return "N/A"
3063
-
3064
- return (
3065
- f"{'✓' if d['ml_approved'] else '✗'}"
3066
- f"{d['ml_prob']:.3f}"
3067
- )
3068
-
3069
-
3070
- def meta_status(d):
3071
-
3072
- m = d[
3073
- "meta"
3074
- ]
3075
-
3076
- prefix = (
3077
- "✓"
3078
- if m[
3079
- "approved"
3080
- ]
3081
- else
3082
- "✗"
3083
- )
3084
-
3085
- return (
3086
- f"{prefix}"
3087
- f"{m['meta_score']:.3f}"
3088
- )
3089
-
3090
-
3091
- # ================================================================
3092
- # TABLE
3093
- # ================================================================
3094
-
3095
- def build_ranked_table(
3096
- ranked,
3097
- top_n,
3098
- ):
3099
-
3100
- header = (
3101
- f"{'#':>3} "
3102
- f"{'Symbol':<13} "
3103
- f"{'Score':>7} "
3104
- f"{'Meta':>7} "
3105
- f"{'ML':>7} "
3106
- f"{'MTF':>6} "
3107
- f"{'SIM':>6} "
3108
- f"{'WF':>6} "
3109
- f"{'MC':>6} "
3110
- f"{'Noise':>6} "
3111
- f"{'Stab':>6} "
3112
- f"{'Regime':<20} "
3113
- f"{'Status'}\n"
3114
- )
3115
-
3116
- out = (
3117
- header +
3118
- "─" * 135 +
3119
- "\n"
3120
- )
3121
-
3122
- for i, (
3123
- sym,
3124
- d,
3125
- ) in enumerate(
3126
- ranked[:top_n],
3127
- 1,
3128
- ):
3129
-
3130
- m = d[
3131
- "meta"
3132
- ]
3133
-
3134
- status = (
3135
- "OK"
3136
- if d[
3137
- "final_approved"
3138
- ]
3139
- else
3140
- "RULE_VET"
3141
- if d[
3142
- "rule_vetoed"
3143
- ]
3144
- else
3145
- "META_VET"
3146
- )
3147
-
3148
- out += (
3149
-
3150
- f"{i:>3} "
3151
-
3152
- f"{sym:<13} "
3153
-
3154
- f"{d['total_score']:>7.3f} "
3155
-
3156
- f"{m['meta_score']:>7.3f} "
3157
-
3158
- f"{ml_status(d):>7} "
3159
-
3160
- f"{m['mtf']['agreement']:>6.3f} "
3161
-
3162
- f"{m['similarity']['score']:>6.3f} "
3163
-
3164
- f"{m['walkforward']['score']:>6.3f} "
3165
-
3166
- f"{m['montecarlo']['robustness']:>6.3f} "
3167
-
3168
- f"{m['noise']:>6.3f} "
3169
-
3170
- f"{m['regime']['stability']:>6.3f} "
3171
-
3172
- f"{m['regime']['state']:<20} "
3173
-
3174
- f"{status}\n"
3175
- )
3176
-
3177
- return out
3178
-
3179
-
3180
- # ================================================================
3181
- # DETAIL
3182
- # ================================================================
3183
-
3184
- def build_detail(
3185
- d,
3186
- ):
3187
-
3188
- m = d[
3189
- "meta"
3190
- ]
3191
-
3192
- r = d.get(
3193
- "risk",
3194
- {}
3195
- )
3196
-
3197
- direction = _DIR_LABEL.get(
3198
- d[
3199
- "direction"
3200
- ],
3201
- "NONE",
3202
- )
3203
-
3204
- lines = [
3205
-
3206
- "═" * 72,
3207
-
3208
- f"BEST SETUP: "
3209
- f"{d['symbol']} "
3210
- f"[{direction}]",
3211
-
3212
- "═" * 72,
3213
-
3214
- "",
3215
-
3216
- "── MARKET ──────────────────────────────────────────",
3217
-
3218
- f"Price: {d['close']:.8f}",
3219
-
3220
- f"Trend: {d['trend']}",
3221
-
3222
- f"ADX: {d['adx']:.2f}",
3223
-
3224
- f"DI+: {d['di_plus']:.2f}",
3225
-
3226
- f"DI-: {d['di_minus']:.2f}",
3227
-
3228
- f"Volume ratio: {d['vol_ratio']:.3f}",
3229
-
3230
- "",
3231
-
3232
- "── REGIME ─────────────────────────────────────────",
3233
-
3234
- f"State: "
3235
- f"{m['regime']['state']}",
3236
-
3237
- f"Stability: "
3238
- f"{m['regime']['stability']:.4f}",
3239
-
3240
- f"Transition risk: "
3241
- f"{m['regime']['transition_risk']:.4f}",
3242
-
3243
- f"Volatility state: "
3244
- f"{m['regime']['volatility_state']}",
3245
-
3246
- "",
3247
-
3248
- "── MULTI-TIMEFRAME ───────────────────────────────",
3249
-
3250
- f"Agreement: "
3251
- f"{m['mtf']['agreement']:.4f}",
3252
-
3253
- ]
3254
-
3255
- for tf, x in m[
3256
- "mtf"
3257
- ][
3258
- "timeframes"
3259
- ].items():
3260
-
3261
- lines.append(
3262
- f"{tf:<18}"
3263
- f"{x['direction']:>3} "
3264
- f"strength={x['strength']:.3f}"
3265
- )
3266
-
3267
- lines += [
3268
-
3269
- "",
3270
-
3271
- "── MACHINE LEARNING ──────────────────────────────",
3272
-
3273
- f"Raw probability: "
3274
- f"{d['ml_prob'] if d['ml_prob'] is not None else 'N/A'}",
3275
-
3276
- "",
3277
-
3278
- "── HISTORICAL SIMILARITY ─────────────────────────",
3279
-
3280
- f"Matches: "
3281
- f"{m['similarity']['matches']}",
3282
-
3283
- f"Similarity score: "
3284
- f"{m['similarity']['score']:.4f}",
3285
-
3286
- f"Directional edge: "
3287
- f"{m['similarity']['edge']:.4f}",
3288
-
3289
- f"Median return: "
3290
- f"{m['similarity']['median_return']:.5f}",
3291
-
3292
- "",
3293
-
3294
- "── ORDER FLOW PROXY ──────────────────────────────",
3295
-
3296
- f"Buy pressure: "
3297
- f"{m['orderflow']['buy_pressure']:.4f}",
3298
-
3299
- f"Sell pressure: "
3300
- f"{m['orderflow']['sell_pressure']:.4f}",
3301
-
3302
- f"Imbalance: "
3303
- f"{m['orderflow']['imbalance']:.4f}",
3304
-
3305
- f"Absorption: "
3306
- f"{m['orderflow']['absorption']:.4f}",
3307
-
3308
- f"Breakout quality: "
3309
- f"{m['orderflow']['breakout_quality']:.4f}",
3310
-
3311
- "",
3312
-
3313
- "── WALK FORWARD ──────────────────────────────────",
3314
-
3315
- f"Score: "
3316
- f"{m['walkforward']['score']:.4f}",
3317
-
3318
- f"Expectancy: "
3319
- f"{m['walkforward']['expectancy']:.5f}",
3320
-
3321
- f"Profit factor: "
3322
- f"{m['walkforward']['profit_factor']:.3f}",
3323
-
3324
- f"Folds: "
3325
- f"{m['walkforward']['folds']}",
3326
-
3327
- "",
3328
-
3329
- "── OVERFIT DETECTOR ──────────────────────────────",
3330
-
3331
- f"Train PF: "
3332
- f"{m['overfit']['train_pf']:.3f}",
3333
-
3334
- f"Test PF: "
3335
- f"{m['overfit']['test_pf']:.3f}",
3336
-
3337
- f"Degradation: "
3338
- f"{m['overfit']['degradation']:.3f}",
3339
-
3340
- f"Overfit risk: "
3341
- f"{m['overfit']['risk']:.3f}",
3342
-
3343
- "",
3344
-
3345
- "── MONTE CARLO ──────────────────────────────────",
3346
-
3347
- f"Iterations: "
3348
- f"{ENGINE_CONFIG['mc_iterations']:,}",
3349
-
3350
- f"P(positive): "
3351
- f"{m['montecarlo']['positive_probability']:.4f}",
3352
-
3353
- f"Robustness: "
3354
- f"{m['montecarlo']['robustness']:.4f}",
3355
-
3356
- f"Median PnL: "
3357
- f"{m['montecarlo']['median_pnl']:.2f}R",
3358
-
3359
- f"Worst 5%: "
3360
- f"{m['montecarlo']['worst5']:.2f}R",
3361
-
3362
- f"Median MaxDD: "
3363
- f"{m['montecarlo']['median_max_dd']:.2f}R",
3364
-
3365
- "",
3366
-
3367
- "── DYNAMIC EXIT ──────────────────────────────────",
3368
-
3369
- f"SL: "
3370
- f"{m['dynamic_exit']['sl_atr']:.2f} ATR",
3371
-
3372
- f"TP: "
3373
- f"{m['dynamic_exit']['tp_atr']:.2f} ATR",
3374
-
3375
- f"Expected R: "
3376
- f"{m['dynamic_exit']['expectancy']:.4f}",
3377
-
3378
- "",
3379
-
3380
- "── ADAPTIVE META ─────────────────────────────────",
3381
-
3382
- f"Rule: "
3383
- f"{m['components']['rule']:.4f}",
3384
-
3385
- f"ML: "
3386
- f"{m['components']['ml']:.4f}",
3387
-
3388
- f"Trend: "
3389
- f"{m['components']['trend']:.4f}",
3390
-
3391
- f"Momentum: "
3392
- f"{m['components']['momentum']:.4f}",
3393
-
3394
- f"MTF: "
3395
- f"{m['components']['mtf']:.4f}",
3396
-
3397
- f"Order Flow: "
3398
- f"{m['components']['orderflow']:.4f}",
3399
-
3400
- f"Similarity: "
3401
- f"{m['components']['similarity']:.4f}",
3402
-
3403
- f"Walk Forward: "
3404
- f"{m['components']['walkforward']:.4f}",
3405
-
3406
- f"Stability: "
3407
- f"{m['components']['stability']:.4f}",
3408
-
3409
- f"Monte Carlo: "
3410
- f"{m['components']['montecarlo']:.4f}",
3411
-
3412
- f"Noise: "
3413
- f"{m['noise']:.4f}",
3414
-
3415
- f"Freshness: "
3416
- f"{m['freshness']:.4f}",
3417
-
3418
- "",
3419
-
3420
- "═══════════════════════════════════════════════���══",
3421
-
3422
- f"META SCORE: "
3423
- f"{m['meta_score']:.4f}",
3424
-
3425
- f"UNCERTAINTY: "
3426
- f"{m['uncertainty']:.4f}",
3427
-
3428
- f"MEMORY SCORE: "
3429
- f"{m['memory_score']:.4f}",
3430
-
3431
- ]
3432
-
3433
- if m[
3434
- "veto_reasons"
3435
- ]:
3436
-
3437
- lines.append(
3438
- "META VETO: "
3439
- +
3440
- ", ".join(
3441
- m[
3442
- "veto_reasons"
3443
- ]
3444
- )
3445
- )
3446
-
3447
- lines.append(
3448
- "══════════════════════════════════════════════════"
3449
- )
3450
-
3451
- if r:
3452
-
3453
- lines += [
3454
-
3455
- "",
3456
-
3457
- "── RISK ───────────────────────────────────────────",
3458
-
3459
- f"Entry: "
3460
- f"{r.get('entry_price', 0):.8f}",
3461
-
3462
- f"ATR: "
3463
- f"{r.get('atr', 0):.8f}",
3464
-
3465
- f"Risk fraction: "
3466
- f"{r.get('risk_fraction', 0):.4f}%",
3467
-
3468
- f"$ at risk: "
3469
- f"${r.get('dollar_at_risk', 0):.2f}",
3470
-
3471
- f"Position notional: "
3472
- f"${r.get('position_notional', 0):.2f}",
3473
-
3474
- f"Leverage: "
3475
- f"{r.get('leverage_implied', 0):.2f}x",
3476
-
3477
- f"Optimized SL: "
3478
- f"{r.get('optimized_sl_atr', 0):.2f} ATR",
3479
-
3480
- f"Optimized TP: "
3481
- f"{r.get('optimized_tp_atr', 0):.2f} ATR",
3482
-
3483
- ]
3484
-
3485
- return "\n".join(
3486
- lines
3487
- )
3488
-
3489
-
3490
- # ================================================================
3491
- # SYMBOL PARSER
3492
- # ================================================================
3493
-
3494
- def parse_symbols(
3495
- raw,
3496
- ):
3497
-
3498
- result = []
3499
-
3500
- for token in (
3501
- raw
3502
- .replace(",", " ")
3503
- .replace("\n", " ")
3504
- .split()
3505
- ):
3506
-
3507
- token = token.strip().upper()
3508
-
3509
- if not token:
3510
- continue
3511
-
3512
- result.append(
3513
- token
3514
- if "-"
3515
- in token
3516
- else
3517
- f"{token}-USDT"
3518
- )
3519
-
3520
- return (
3521
- result
3522
- or
3523
- DEFAULT_SYMBOLS
3524
- )
3525
-
3526
-
3527
- # ================================================================
3528
- # MAIN ANALYSIS
3529
- # ================================================================
3530
-
3531
- def run_analysis(
3532
- symbols_input,
3533
- equity,
3534
- consec_losses,
3535
- drawdown_pct,
3536
- top_n,
3537
- use_live,
3538
- use_ml,
3539
- progress=gr.Progress(
3540
- track_tqdm=False
3541
- ),
3542
- ):
3543
-
3544
- t0 = time.time()
3545
-
3546
- output = []
3547
-
3548
- output += [
3549
-
3550
- "━" * 78,
3551
-
3552
- " OKX QUANT ENGINE v5",
3553
-
3554
- " ADAPTIVE MULTI-LAYER RESEARCH ENGINE",
3555
-
3556
- "━" * 78,
3557
-
3558
- ]
3559
-
3560
- # --------------------------------------------------------------
3561
- # Symbols
3562
- # --------------------------------------------------------------
3563
-
3564
- if use_live:
3565
-
3566
- output.append(
3567
- "⟳ Fetching live OKX instruments..."
3568
- )
3569
-
3570
- symbols = (
3571
- fetch_instruments(
3572
- "SPOT"
3573
- )
3574
- or
3575
- DEFAULT_SYMBOLS
3576
- )
3577
-
3578
- else:
3579
-
3580
- symbols = parse_symbols(
3581
- symbols_input
3582
- )
3583
-
3584
- output.append(
3585
- f"✓ Symbols: {len(symbols)}"
3586
- )
3587
-
3588
- output.append(
3589
- f"✓ Timeframe: {TIMEFRAME}"
3590
- )
3591
-
3592
- output.append(
3593
- f"✓ Equity: ${equity:,.0f}"
3594
- )
3595
-
3596
- output.append("")
3597
-
3598
- # --------------------------------------------------------------
3599
- # Data
3600
- # --------------------------------------------------------------
3601
-
3602
- def callback(
3603
- i,
3604
- total,
3605
- symbol,
3606
- ):
3607
-
3608
- progress(
3609
- i /
3610
- max(total, 1),
3611
- desc=
3612
- f"Fetching {symbol}"
3613
- )
3614
-
3615
- data = fetch_multiple(
3616
- symbols,
3617
- min_bars=100,
3618
- progress_callback=
3619
- callback,
3620
- )
3621
-
3622
- output.append(
3623
- f"✓ Fetched "
3624
- f"{len(data)}/{len(symbols)}"
3625
- )
3626
-
3627
- output.append("")
3628
-
3629
- # --------------------------------------------------------------
3630
- # Analyze
3631
- # --------------------------------------------------------------
3632
-
3633
- results = {}
3634
-
3635
- errors = []
3636
-
3637
- for symbol, df in (
3638
- data.items()
3639
- ):
3640
-
3641
- try:
3642
-
3643
- results[
3644
- symbol
3645
- ] = analyze_single(
3646
-
3647
- symbol,
3648
-
3649
- df,
3650
-
3651
- account_equity=
3652
- equity,
3653
-
3654
- consec_losses=
3655
- int(
3656
- consec_losses
3657
- ),
3658
-
3659
- equity_drawdown_pct=
3660
- drawdown_pct /
3661
- 100.0,
3662
-
3663
- use_ml=
3664
- use_ml,
3665
- )
3666
-
3667
- except Exception as e:
3668
-
3669
- logger.error(
3670
- f"{symbol}: {e}",
3671
- exc_info=True,
3672
- )
3673
-
3674
- errors.append(
3675
- symbol
3676
- )
3677
-
3678
- if errors:
3679
-
3680
- output.append(
3681
- "Errors: "
3682
- +
3683
- ", ".join(
3684
- errors
3685
- )
3686
- )
3687
-
3688
- # --------------------------------------------------------------
3689
- # Correlation
3690
- # --------------------------------------------------------------
3691
-
3692
- correlation = (
3693
- correlation_engine(
3694
- results
3695
- )
3696
- )
3697
-
3698
- for symbol in results:
3699
-
3700
- results[
3701
- symbol
3702
- ][
3703
- "portfolio_score"
3704
- ] = correlation.get(
3705
- symbol,
3706
- 0.5,
3707
- )
3708
-
3709
- # Slight portfolio concentration penalty
3710
- results[
3711
- symbol
3712
- ][
3713
- "meta"
3714
- ][
3715
- "meta_score"
3716
- ] = clamp(
3717
- results[
3718
- symbol
3719
- ][
3720
- "meta"
3721
- ][
3722
- "meta_score"
3723
- ]
3724
- *
3725
- (
3726
- 0.90 +
3727
- 0.10 *
3728
- correlation.get(
3729
- symbol,
3730
- 0.5,
3731
- )
3732
- )
3733
- )
3734
-
3735
- # Re-evaluate final decision
3736
- m = results[
3737
- symbol
3738
- ][
3739
- "meta"
3740
- ]
3741
-
3742
- results[
3743
- symbol
3744
- ][
3745
- "final_approved"
3746
- ] = (
3747
- results[
3748
- symbol
3749
- ][
3750
- "final_approved"
3751
- ]
3752
- and
3753
- m[
3754
- "meta_score"
3755
- ]
3756
- >=
3757
- ENGINE_CONFIG[
3758
- "meta_threshold"
3759
- ]
3760
- )
3761
-
3762
- # --------------------------------------------------------------
3763
- # Ranking
3764
- # --------------------------------------------------------------
3765
-
3766
- ranked = rank_tokens(
3767
- results
3768
- )
3769
-
3770
- approved = [
3771
- x
3772
- for x in ranked
3773
- if x[1][
3774
- "final_approved"
3775
- ]
3776
- ]
3777
-
3778
- rule_veto = sum(
3779
- 1
3780
- for _, d in ranked
3781
- if d[
3782
- "rule_vetoed"
3783
- ]
3784
- )
3785
-
3786
- meta_veto = sum(
3787
- 1
3788
- for _, d in ranked
3789
- if (
3790
- not d[
3791
- "rule_vetoed"
3792
- ]
3793
- and
3794
- d[
3795
- "meta"
3796
- ][
3797
- "hard_veto"
3798
- ]
3799
- )
3800
- )
3801
-
3802
- # --------------------------------------------------------------
3803
- # Output
3804
- # --------------------------------------------------------------
3805
-
3806
- output += [
3807
-
3808
- "",
3809
-
3810
- f"Analyzed: {len(results)}",
3811
-
3812
- f"Approved: {len(approved)}",
3813
-
3814
- f"Rule veto: {rule_veto}",
3815
-
3816
- f"Meta veto: {meta_veto}",
3817
-
3818
- "",
3819
-
3820
- "RANKED SETUPS",
3821
-
3822
- "─" * 135,
3823
-
3824
- build_ranked_table(
3825
- ranked,
3826
- int(top_n),
3827
- ),
3828
-
3829
- ]
3830
-
3831
- # --------------------------------------------------------------
3832
- # Best
3833
- # --------------------------------------------------------------
3834
-
3835
- if approved:
3836
-
3837
- best_symbol, best = (
3838
- approved[0]
3839
- )
3840
-
3841
- output += [
3842
-
3843
- "",
3844
-
3845
- build_detail(
3846
- best
3847
- ),
3848
-
3849
- ]
3850
-
3851
- else:
3852
-
3853
- output += [
3854
-
3855
- "",
3856
-
3857
- "⚠ NO FULLY APPROVED SETUP",
3858
-
3859
- "",
3860
-
3861
- "The engine rejected the available setups",
3862
- "after combining rule, ML, historical,",
3863
- "walk-forward, regime, noise and robustness",
3864
- "conditions.",
3865
-
3866
- ]
3867
-
3868
- # --------------------------------------------------------------
3869
- # Footer
3870
- # --------------------------------------------------------------
3871
-
3872
- output += [
3873
-
3874
- "",
3875
-
3876
- f"Completed in "
3877
- f"{time.time() - t0:.2f}s",
3878
-
3879
- "━" * 78,
3880
-
3881
- "Research use only. Not financial advice.",
3882
-
3883
- ]
3884
-
3885
- return "\n".join(
3886
- output
3887
- )
3888
-
3889
-
3890
- # ================================================================
3891
- # GRADIO
3892
- # ================================================================
3893
-
3894
- def build_app():
3895
-
3896
- with gr.Blocks(
3897
-
3898
- title=
3899
- "OKX Quant Engine v5",
3900
-
3901
- theme=
3902
- gr.themes.Base(
3903
- primary_hue=
3904
- "slate",
3905
- neutral_hue=
3906
- "zinc",
3907
- font=[
3908
- gr.themes.GoogleFont(
3909
- "JetBrains Mono"
3910
- ),
3911
- "monospace",
3912
- ],
3913
- ),
3914
-
3915
- css="""
3916
-
3917
- body,
3918
- .gradio-container {
3919
-
3920
- background:
3921
- #060a10 !important;
3922
-
3923
- font-family:
3924
- 'JetBrains Mono',
3925
- monospace !important;
3926
-
3927
- max-width:
3928
- 1450px !important;
3929
- }
3930
-
3931
- .gr-button-primary {
3932
-
3933
- background:
3934
- linear-gradient(
3935
- 90deg,
3936
- #1a6bff,
3937
- #0044cc
3938
- ) !important;
3939
-
3940
- border:
3941
- none !important;
3942
-
3943
- font-weight:
3944
- 700 !important;
3945
- }
3946
-
3947
- #output_box textarea {
3948
-
3949
- font-family:
3950
- 'JetBrains Mono',
3951
- monospace !important;
3952
-
3953
- font-size:
3954
- 12px !important;
3955
-
3956
- line-height:
3957
- 1.55 !important;
3958
-
3959
- background:
3960
- #0a0e18 !important;
3961
-
3962
- color:
3963
- #b0c4de !important;
3964
-
3965
- border:
3966
- 1px solid #182030 !important;
3967
-
3968
- min-height:
3969
- 900px !important;
3970
- }
3971
-
3972
- """,
3973
-
3974
- ) as app:
3975
-
3976
- gr.Markdown(
3977
- "# ◈ OKX QUANT ENGINE v5"
3978
- )
3979
-
3980
- gr.Markdown(
3981
- """
3982
- **Multi-Regime · Multi-Timeframe · ML · Similarity ·
3983
- Walk-Forward · Overfit Detection · Order-Flow Proxy ·
3984
- Monte Carlo · Dynamic TP/SL · Adaptive Meta Engine**
3985
- """
3986
- )
3987
-
3988
- with gr.Row():
3989
-
3990
- with gr.Column(
3991
- scale=2
3992
- ):
3993
-
3994
- symbols = gr.Textbox(
3995
-
3996
- label=
3997
- "Symbols",
3998
-
3999
- placeholder=
4000
- "BTC-USDT, ETH-USDT, SOL-USDT",
4001
-
4002
- lines=4,
4003
- )
4004
-
4005
- with gr.Column(
4006
- scale=1
4007
- ):
4008
-
4009
- equity = gr.Slider(
4010
-
4011
- label=
4012
- "Account Equity",
4013
-
4014
- minimum=100,
4015
-
4016
- maximum=1_000_000,
4017
-
4018
- step=500,
4019
-
4020
- value=
4021
- DEFAULT_ACCOUNT_EQUITY,
4022
- )
4023
-
4024
- top_n = gr.Slider(
4025
-
4026
- label=
4027
- "Top N",
4028
-
4029
- minimum=5,
4030
-
4031
- maximum=100,
4032
-
4033
- step=5,
4034
-
4035
- value=
4036
- TOP_N_DEFAULT,
4037
- )
4038
-
4039
- with gr.Column(
4040
- scale=1
4041
- ):
4042
-
4043
- losses = gr.Slider(
4044
-
4045
- label=
4046
- "Consecutive Losses",
4047
-
4048
- minimum=0,
4049
-
4050
- maximum=10,
4051
-
4052
- step=1,
4053
-
4054
- value=0,
4055
- )
4056
-
4057
- dd = gr.Slider(
4058
-
4059
- label=
4060
- "Drawdown %",
4061
-
4062
- minimum=0,
4063
-
4064
- maximum=30,
4065
-
4066
- step=0.5,
4067
-
4068
- value=0,
4069
- )
4070
-
4071
- live = gr.Checkbox(
4072
-
4073
- label=
4074
- "Live OKX Instruments",
4075
-
4076
- value=False,
4077
- )
4078
-
4079
- ml = gr.Checkbox(
4080
-
4081
- label=
4082
- "ML Filter",
4083
-
4084
- value=(
4085
- _TRADE_FILTER
4086
- is not None
4087
- ),
4088
- )
4089
-
4090
- gr.Markdown(
4091
- f"""
4092
- ### Engine Configuration
4093
-
4094
- Meta threshold: **{ENGINE_CONFIG['meta_threshold']:.2f}**
4095
-
4096
- Monte Carlo simulations: **{ENGINE_CONFIG['mc_iterations']:,}**
4097
-
4098
- Similarity matches: **{ENGINE_CONFIG['similarity_k']}**
4099
-
4100
- Walk-forward validation: **ON**
4101
-
4102
- Overfit detection: **ON**
4103
-
4104
- Dynamic TP/SL: **ON**
4105
-
4106
- Online performance memory: **ON**
4107
- """
4108
- )
4109
-
4110
- button = gr.Button(
4111
- "▶ RUN QUANT ENGINE",
4112
- variant=
4113
- "primary",
4114
- size=
4115
- "lg",
4116
- )
4117
-
4118
- output = gr.Textbox(
4119
-
4120
- label=
4121
- "Engine Output",
4122
-
4123
- lines=60,
4124
-
4125
- max_lines=200,
4126
-
4127
- interactive=False,
4128
-
4129
- elem_id=
4130
- "output_box",
4131
- )
4132
-
4133
- button.click(
4134
-
4135
- fn=
4136
- run_analysis,
4137
-
4138
- inputs=[
4139
- symbols,
4140
- equity,
4141
- losses,
4142
- dd,
4143
- top_n,
4144
- live,
4145
- ml,
4146
- ],
4147
-
4148
- outputs=
4149
- output,
4150
- )
4151
-
4152
- gr.Markdown(
4153
- """
4154
- **Research use only.**
4155
-
4156
- Bu sistem istatistiksel/algoritmik araştırma yapar.
4157
- Hiçbir skor veya Monte Carlo sonucu gelecekteki getiriyi garanti etmez.
4158
- """
4159
- )
4160
-
4161
- return app
4162
-
4163
-
4164
- # ================================================================
4165
- # ENTRY
4166
- # ================================================================
4167
-
4168
- if __name__ == "__main__":
4169
-
4170
- import argparse
4171
-
4172
- parser = argparse.ArgumentParser()
4173
-
4174
- parser.add_argument(
4175
- "--port",
4176
- type=int,
4177
- default=7860,
4178
- )
4179
-
4180
- parser.add_argument(
4181
- "--share",
4182
- action="store_true",
4183
- )
4184
-
4185
- args = parser.parse_args()
4186
-
4187
- build_app().launch(
4188
-
4189
- server_name=
4190
- "0.0.0.0",
4191
-
4192
- server_port=
4193
- args.port,
4194
-
4195
- share=
4196
- args.share,
4197
-
4198
- show_error=True,
4199
- )