GoshawkVortexAI commited on
Commit
8f3ad83
·
verified ·
1 Parent(s): cfb37af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2457 -294
app.py CHANGED
@@ -1,72 +1,1108 @@
1
  """
2
- main.py — Gradio interface with integrated ML probability filter.
 
3
 
4
  Pipeline:
5
- OHLCV Data
6
- │
7
- ▼
8
- Rule Engine (regime + volume + scoring + veto)
9
- │
10
- ├─► Vetoed → skip (no ML call, save compute)
11
- │
12
- └─► Approved by rules
13
- │
14
- ▼
15
- ML Filter (LightGBM / HGBM probability)
16
- │
17
- ├─► prob < threshold → FILTERED (shown as ML_REJECT)
18
- │
19
- └─► prob >= threshold → Risk Engine → Final setup
20
- │
21
- ▼
22
- Ranked output with ML prob overlay
 
 
 
 
 
 
 
 
 
 
 
 
23
  """
24
 
25
- import logging
26
- import sys
27
- import time
28
- from typing import List, Optional, Dict, Any
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- import gradio as gr
31
 
32
- from config import (
33
- DEFAULT_SYMBOLS,
34
- TOP_N_DEFAULT,
35
- DEFAULT_ACCOUNT_EQUITY,
36
- TIMEFRAME,
37
- CANDLE_LIMIT,
38
- )
39
- from data_fetcher import fetch_multiple, fetch_instruments
40
- from regime import detect_regime
41
- from volume_analysis import analyze_volume
42
- from risk_engine import evaluate_risk
43
- from veto import apply_veto, veto_summary
44
- from scorer import compute_structure_score, score_token, rank_tokens, format_score_bar, quality_tier
45
- from feature_builder import build_feature_dict, validate_features
46
- from ml_filter import TradeFilter
47
 
48
- logging.basicConfig(
49
- level=logging.INFO,
50
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
51
- stream=sys.stdout,
52
- )
53
- logger = logging.getLogger("main")
54
 
55
- # Load ML filter once at startup (None if not yet trained)
56
- _TRADE_FILTER: Optional[TradeFilter] = TradeFilter.load_or_none()
57
 
58
- _TREND_ICON = {"bullish": "▲", "ranging": "◆", "bearish": "▼"}
59
- _BREAK_LABEL = {1: "↑UP", -1: "↓DN", 0: " — "}
60
- _DIR_LABEL = {1: "LONG", -1: "SHORT", 0: "NONE"}
61
 
 
 
62
 
63
- def infer_direction(trend: str, breakout: int) -> int:
64
- if trend == "bullish" or breakout == 1:
65
- return 1
66
- if trend == "bearish" or breakout == -1:
67
- return -1
68
- return 0
 
 
 
 
 
 
 
 
 
 
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  def analyze_single(
72
  symbol: str,
@@ -76,209 +1112,767 @@ def analyze_single(
76
  equity_drawdown_pct: float = 0.0,
77
  use_ml: bool = True,
78
  ) -> Dict[str, Any]:
79
- # ── RULE ENGINE ───────────────────────────────────────────────────────────
80
- regime_data = detect_regime(df)
81
- volume_data = analyze_volume(df, atr_series=regime_data["atr_series"])
82
- structure_sc = compute_structure_score(regime_data)
83
- direction = infer_direction(regime_data["trend"], volume_data["breakout"])
84
- vetoed, veto_reason = apply_veto(regime_data, volume_data, structure_sc, direction=direction)
85
- scores = score_token(regime_data, volume_data, vetoed)
86
-
87
- # ── ML FILTER ─────────────────────────────────���───────────────────────────
88
- ml_prob = None
89
- ml_approved = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  ml_reject_reason = ""
91
 
92
- if use_ml and _TRADE_FILTER is not None and not vetoed:
 
 
 
 
 
 
 
93
  try:
94
- feat = build_feature_dict(regime_data, volume_data, scores)
 
 
 
 
 
 
95
  if validate_features(feat):
96
- result = _TRADE_FILTER.predict(regime_data, volume_data, scores)
97
- ml_prob = result.probability
 
 
 
 
 
 
 
 
98
  ml_approved = result.approved
99
- ml_reject_reason = result.reject_reason
 
 
 
100
  else:
101
- ml_approved = None # pass through if features invalid
 
 
102
  except Exception as e:
103
- logger.warning(f"{symbol}: ML filter error: {e}")
 
 
 
 
104
  ml_approved = None
105
 
106
- # ── RISK ENGINE ───────────────────────────────────────────────────────────
107
- # Only compute full risk if not vetoed by rules AND not rejected by ML
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  final_approved = (
109
- not vetoed and
110
- (ml_approved is None or ml_approved)
111
- )
112
-
113
- risk_data = evaluate_risk(
114
- close=float(df["close"].iloc[-1]),
115
- atr=regime_data["atr"],
116
- atr_pct=regime_data["atr_pct"],
117
- regime_score=regime_data["regime_score"],
118
- vol_ratio=regime_data["vol_ratio"],
119
- volume_score=volume_data["volume_score"],
120
- regime_confidence=regime_data["regime_confidence"],
121
- vol_compressed=regime_data["vol_compressed"],
122
- consec_losses=consec_losses,
123
- equity_drawdown_pct=equity_drawdown_pct,
124
- account_equity=account_equity,
125
- ) if final_approved else {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  return {
128
- "symbol": symbol,
129
- "close": float(df["close"].iloc[-1]),
130
- "trend": regime_data["trend"],
131
- "adx": regime_data["adx"],
132
- "di_plus": regime_data["di_plus"],
133
- "di_minus": regime_data["di_minus"],
134
- "vol_ratio": regime_data["vol_ratio"],
135
- "vol_compressed": regime_data["vol_compressed"],
136
- "vol_expanding_from_base": regime_data["vol_expanding_from_base"],
137
- "vol_expanding": regime_data["vol_expanding"],
138
- "dist_atr": regime_data["dist_atr"],
139
- "price_extended": regime_data["price_extended_long"] or regime_data["price_extended_short"],
140
- "regime_confidence": regime_data["regime_confidence"],
141
- "spike": volume_data["spike"],
142
- "climax": volume_data["climax"],
143
- "absorption": volume_data["absorption"],
144
- "failed_breakout": volume_data["failed_breakout"],
145
- "recent_failed": volume_data["recent_failed_count"],
146
- "breakout": volume_data["breakout"],
147
- "obv_slope": volume_data["obv_slope_norm"],
148
- "delta_sign": volume_data["delta_sign"],
149
- "direction": direction,
150
- "rule_vetoed": vetoed,
151
- "veto_reason": veto_reason,
152
- "ml_prob": ml_prob,
153
- "ml_approved": ml_approved,
154
- "ml_reject_reason": ml_reject_reason,
155
- "final_approved": final_approved,
156
- "regime_score": scores["regime_score"],
157
- "volume_score": scores["volume_score"],
158
- "structure_score": scores["structure_score"],
159
- "confidence_score": scores["confidence_score"],
160
- "total_score": scores["total_score"],
161
- "risk": risk_data,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
 
164
 
 
 
 
 
165
  def _ml_status(d: Dict) -> str:
 
166
  if _TRADE_FILTER is None:
167
  return "NO_MODEL"
 
168
  if d["rule_vetoed"]:
169
  return "RULE_VET"
 
170
  if d["ml_prob"] is None:
171
- return "ML_ERR "
172
- prob_str = f"{d['ml_prob']:.3f}"
173
- return f"✓{prob_str}" if d["ml_approved"] else f"✗{prob_str}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
- def build_ranked_table(ranked: list, top_n: int) -> str:
177
  hdr = (
178
- f"{'#':>3} {'Symbol':<14} {'Score':>7} {'Tier':>4} "
179
- f"{'Regime':>6} {'Vol':>6} {'S':>5} {'C':>5} "
180
- f"{'Trend':>7} {'ADX':>5} {'VR':>5} "
181
- f"{'ML':>8} {'Status'}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  )
183
- sep = "─" * 105 + "\n"
184
- rows = hdr + sep
185
 
186
- for rank, (sym, d) in enumerate(ranked[:top_n], 1):
187
- icon = _TREND_ICON.get(d["trend"], "?")
188
- tier = quality_tier(d["total_score"])
189
- ml_str = _ml_status(d)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  if d["rule_vetoed"]:
192
- status = "RULE_VET"
 
 
 
 
193
  elif not d["final_approved"]:
194
- status = "ML_FILT "
 
 
 
 
 
195
  else:
196
- status = "OK "
 
197
 
198
  rows += (
199
- f"{rank:>3} {sym:<14} {d['total_score']:>7.4f} {tier:>4} "
200
- f"{d['regime_score']:>6.3f} {d['volume_score']:>6.3f} "
201
- f"{d['structure_score']:>5.3f} {d['confidence_score']:>5.3f} "
202
- f"{icon} {d['trend']:<5} {d['adx']:>5.1f} {d['vol_ratio']:>5.2f} "
203
- f"{ml_str:>8} {status}\n"
 
 
 
 
 
 
 
 
 
 
204
  )
 
205
  return rows
206
 
207
 
208
- def build_best_detail(data: Dict[str, Any]) -> str:
209
- r = data.get("risk", {})
210
- sym = data["symbol"]
211
- icon = _TREND_ICON.get(data["trend"], "?")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  vol_state = []
214
- if data["vol_compressed"]: vol_state.append("COMPRESSED")
215
- if data["vol_expanding_from_base"]: vol_state.append("EXPANDING FROM BASE ✓")
216
- if data["vol_expanding"] and not data["vol_expanding_from_base"]:
217
- vol_state.append("EXPANDING (no base)")
218
- vol_state_str = " | ".join(vol_state) or "NORMAL"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  ml_section = ""
 
221
  if _TRADE_FILTER is not None:
222
- prob_str = f"{data['ml_prob']:.4f}" if data["ml_prob"] is not None else "N/A"
223
- thresh_str = f"{_TRADE_FILTER.threshold:.4f}"
224
- decision = "APPROVED ✓" if data["ml_approved"] else "FILTERED ✗"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  ml_section = (
226
- f"\n ── ML PROBABILITY FILTER ─────────────────────────\n"
 
227
  f" P(win): {prob_str}\n"
228
  f" Threshold: {thresh_str}\n"
229
  f" ML Decision: {decision}\n"
230
  )
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  risk_section = ""
 
233
  if r:
 
234
  risk_section = (
235
- f"\n ── RISK PARAMETERS ────────────────────────────────\n"
 
236
  f" Entry: {r.get('entry_price', 0):.8f}\n"
237
- f" ATR: {r.get('atr', 0):.8f} ({r.get('atr_pct', 0):.3f}%)\n"
 
238
  f" Stop Mult: {r.get('stop_mult', 0):.1f}x ATR\n"
239
- f" LONG → Stop: {r.get('stop_long', 0):.8f} Target: {r.get('target_long', 0):.8f}\n"
240
- f" SHORT → Stop: {r.get('stop_short', 0):.8f} Target: {r.get('target_short', 0):.8f}\n"
 
 
241
  f" R:R Ratio: 1 : {r.get('rr_ratio', 2):.1f}\n"
242
  f" Risk Fraction: {r.get('risk_fraction', 0):.4f}%\n"
243
  f" $ At Risk: ${r.get('dollar_at_risk', 0):.2f}\n"
244
  f" Position Size: ${r.get('position_notional', 0):.2f} notional\n"
245
  f" Leverage (est): {r.get('leverage_implied', 0):.2f}x\n"
246
  f" Consec. Losses: {r.get('consec_losses', 0)}\n"
247
- f" Sizing Halted: {'YES ⛔' if r.get('sizing_halted') else 'no'}\n"
 
248
  )
249
 
250
- lines = [
251
- "═" * 64,
252
- f" BEST APPROVED SETUP: {sym} [{_DIR_LABEL.get(data['direction'], '?')}]",
253
- "═" * 64,
254
- f" Trend: {icon} {data['trend'].upper()}",
255
- f" ADX: {data['adx']:.1f} (DI+ {data['di_plus']:.1f} / DI- {data['di_minus']:.1f})",
256
- f" Vol State: {vol_state_str}",
257
- f" Dist from Mean: {data['dist_atr']:.2f} ATR",
258
- f" Regime Confidence:{data['regime_confidence']:.3f}",
259
- "",
260
- " ── SCORES ──────────────────────────────────────────",
261
- f" Regime: {format_score_bar(data['regime_score'])}",
262
- f" Volume: {format_score_bar(data['volume_score'])}",
263
- f" Structure: {format_score_bar(data['structure_score'])}",
264
- f" Confidence: {format_score_bar(data['confidence_score'])}",
265
- f" TOTAL: {format_score_bar(data['total_score'])}",
266
- ]
267
- lines.append(ml_section)
268
- lines.append(risk_section)
269
- lines.append("═" * 64)
270
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
 
 
 
 
 
 
 
272
 
273
- def parse_symbols(raw: str) -> List[str]:
274
  out = []
275
- for tok in raw.replace(",", " ").replace("\n", " ").split():
 
 
 
 
 
 
 
276
  tok = tok.strip().upper()
277
- if tok:
278
- out.append(tok if "-" in tok else f"{tok}-USDT")
279
- return out or DEFAULT_SYMBOLS
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
 
 
 
 
 
282
  def run_analysis(
283
  symbols_input: str,
284
  equity: float,
@@ -287,195 +1881,764 @@ def run_analysis(
287
  top_n: int,
288
  use_live: bool,
289
  use_ml: bool,
290
- progress=gr.Progress(track_tqdm=False),
 
 
291
  ) -> str:
 
292
  t0 = time.time()
 
293
  lines = []
294
 
295
- ml_status_str = "ACTIVE" if (_TRADE_FILTER is not None and use_ml) else (
296
- "DISABLED" if not use_ml else "NOT TRAINED (run train.py)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  )
298
 
299
  lines += [
300
- "━" * 68,
301
- " OKX QUANTITATIVE ANALYSIS ENGINE v3",
 
 
 
 
 
302
  f" ML Filter: {ml_status_str}",
303
- "━" * 68,
 
 
304
  ]
305
 
306
- if _TRADE_FILTER is not None and use_ml:
307
- lines.append(f" ML threshold: {_TRADE_FILTER.threshold:.4f} | Stats: {_TRADE_FILTER.stats()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
  if use_live:
310
- lines.append("⟳ Fetching live OKX instrument list...")
311
- symbols = fetch_instruments("SPOT") or DEFAULT_SYMBOLS
312
- lines.append(f"✓ {len(symbols)} live USDT instruments")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  else:
314
- symbols = parse_symbols(symbols_input)
315
- lines.append(f"✓ {len(symbols)} symbol(s)")
 
 
 
 
 
 
 
 
 
 
316
 
317
  lines.append(
318
- f" Equity: ${equity:,.0f} | Losses: {int(consec_losses)}"
319
- f" | DD: {drawdown_pct:.1f}% | TF: {TIMEFRAME}"
 
 
 
 
320
  )
 
321
  lines.append("")
322
 
323
- total = len(symbols)
 
 
 
 
 
 
324
 
325
- def prog_cb(i, t, sym):
326
- progress(i / t, desc=f"Fetching {sym} ({i}/{t})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
- ohlcv_map = fetch_multiple(symbols, min_bars=50, progress_callback=prog_cb)
329
- lines.append(f"✓ Fetched {len(ohlcv_map)}/{total}")
330
  lines.append("")
331
 
332
- all_results: Dict[str, Any] = {}
 
 
 
 
 
 
 
 
333
  errors = []
334
 
335
- for sym, df in ohlcv_map.items():
 
 
 
336
  try:
337
- all_results[sym] = analyze_single(
338
- sym, df,
339
- account_equity=equity,
340
- consec_losses=int(consec_losses),
341
- equity_drawdown_pct=drawdown_pct / 100.0,
342
- use_ml=use_ml,
 
 
 
 
 
 
 
 
 
 
 
343
  )
 
344
  except Exception as exc:
345
- logger.error(f"{sym}: {exc}", exc_info=True)
346
- errors.append(sym)
 
 
 
 
 
 
 
 
 
 
 
347
 
348
  if errors:
349
- lines.append(f"⚠ Errors: {', '.join(errors)}")
350
 
351
- ranked = rank_tokens(all_results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- rule_vetoed_n = sum(1 for _, d in ranked if d["rule_vetoed"])
354
- ml_filtered_n = sum(1 for _, d in ranked if not d["rule_vetoed"] and not d["final_approved"])
355
- approved_n = sum(1 for _, d in ranked if d["final_approved"])
356
 
357
  lines += [
358
- f" {len(all_results)} analyzed | {approved_n} approved | "
359
- f"{rule_vetoed_n} rule-vetoed | {ml_filtered_n} ML-filtered",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  "",
 
361
  " RANKED SETUPS",
362
- "─" * 105,
363
- build_ranked_table(ranked, int(top_n)),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  ]
365
 
366
- final_approved = [(s, d) for s, d in ranked if d["final_approved"]]
367
  if final_approved:
368
- best_sym, best_data = final_approved[0]
369
- lines += ["", build_best_detail(best_data)]
 
 
 
 
 
 
 
 
 
 
370
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  lines += [
372
  "",
373
- " ⚠ No fully approved setups.",
374
- " Possible causes: market regime unfavorable, ML model not trained,",
375
- " or all signals vetoed by rule engine.",
376
  ]
377
 
378
- if _TRADE_FILTER is not None and use_ml:
379
- lines += ["", f" ML session stats: {_TRADE_FILTER.stats()}"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- lines += ["", f" ✓ Complete in {time.time() - t0:.1f}s", "━" * 68]
382
- return "\n".join(lines)
383
 
 
 
 
384
 
385
  def build_app() -> gr.Blocks:
 
386
  with gr.Blocks(
387
- title="OKX Quant Engine v3",
 
 
 
388
  theme=gr.themes.Base(
 
389
  primary_hue="slate",
 
390
  neutral_hue="zinc",
391
- font=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
 
 
 
 
 
 
392
  ),
 
393
  css="""
394
- body, .gradio-container {
 
 
395
  background: #060a10 !important;
396
- font-family: 'JetBrains Mono', monospace !important;
397
- max-width: 1280px !important;
 
 
398
  }
 
399
  .gr-button-primary {
400
- background: linear-gradient(90deg, #1a6bff, #0044cc) !important;
 
 
 
 
 
 
401
  border: none !important;
 
402
  font-weight: 700 !important;
403
- letter-spacing: 0.06em !important;
 
 
404
  }
 
405
  #output_box textarea {
406
- font-family: 'JetBrains Mono', monospace !important;
407
- font-size: 12px !important;
408
- line-height: 1.55 !important;
409
- background: #0a0e18 !important;
410
- color: #b0c4de !important;
411
- border: 1px solid #182030 !important;
412
- min-height: 740px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  }
414
- label { color: #4a6080 !important; font-size: 11px !important; text-transform: uppercase !important; letter-spacing: 0.09em !important; }
415
- h1, h2 { color: #c0d4f0 !important; font-family: 'JetBrains Mono', monospace !important; }
416
- p { color: #384858 !important; font-size: 12px !important; }
417
- .gr-panel { background: #0c1020 !important; border: 1px solid #182030 !important; }
418
- """,
419
  ) as app:
420
- gr.Markdown("# ◈ OKX QUANT ENGINE v3")
 
 
 
 
 
 
 
 
421
  gr.Markdown(
422
- "ADX · absorption detection · volatility compression · "
423
- "fake breakout filter · **LightGBM probability layer** · adaptive risk"
 
 
 
 
 
424
  )
425
 
 
 
 
 
426
  with gr.Row():
427
- with gr.Column(scale=2):
 
 
 
 
428
  symbols_box = gr.Textbox(
429
- label="Symbols (comma / newline — blank = defaults)",
430
- placeholder="BTC-USDT, ETH-USDT, SOL-USDT ...",
431
- lines=4, value="",
 
 
 
 
 
 
 
 
432
  )
433
- with gr.Column(scale=1):
 
 
 
 
434
  equity_slider = gr.Slider(
435
- label="Account Equity ($)",
436
- minimum=100, maximum=1_000_000, step=500,
437
- value=DEFAULT_ACCOUNT_EQUITY,
 
 
 
 
 
 
 
 
 
438
  )
 
439
  top_n_slider = gr.Slider(
440
- label="Top N to Display",
441
- minimum=5, maximum=100, step=5, value=TOP_N_DEFAULT,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  )
443
- with gr.Column(scale=1):
444
- consec_loss = gr.Slider(label="Consecutive Losses", minimum=0, maximum=10, step=1, value=0)
445
- drawdown = gr.Slider(label="Drawdown from Peak (%)", minimum=0.0, maximum=30.0, step=0.5, value=0.0)
446
- live_check = gr.Checkbox(label="Fetch live OKX instruments (100+)", value=False)
447
- ml_check = gr.Checkbox(
448
- label=f"Enable ML Filter (model: {'LOADED' if _TRADE_FILTER else 'NOT TRAINED'})",
449
- value=_TRADE_FILTER is not None,
 
 
 
 
 
 
450
  )
451
 
452
- run_btn = gr.Button("▶ RUN ANALYSIS", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  output_box = gr.Textbox(
454
- label="Analysis Output",
455
- lines=50, max_lines=150,
 
 
 
 
 
 
456
  interactive=False,
457
- elem_id="output_box",
 
 
458
  )
459
 
 
 
 
 
460
  run_btn.click(
 
461
  fn=run_analysis,
462
- inputs=[symbols_box, equity_slider, consec_loss, drawdown, top_n_slider, live_check, ml_check],
463
- outputs=output_box,
 
 
 
 
 
 
 
 
 
 
 
464
  )
465
 
 
 
 
 
466
  gr.Markdown(
467
- "**Research use only. Not financial advice.** "
468
- "Train the ML filter: `python train.py --use-defaults` | "
469
- "Re-optimize threshold: `python threshold_optimizer.py`"
 
 
 
 
 
 
 
 
 
470
  )
471
 
472
  return app
473
 
474
 
 
 
 
 
475
  if __name__ == "__main__":
 
476
  import argparse
 
477
  parser = argparse.ArgumentParser()
478
- parser.add_argument("--port", type=int, default=7860)
479
- parser.add_argument("--share", action="store_true")
480
- a = parser.parse_args()
481
- build_app().launch(server_name="0.0.0.0", server_port=a.port, share=a.share, show_error=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ main.py — OKX Quant Engine v4
3
+ Adaptive Meta-Decision Engine
4
 
5
  Pipeline:
6
+
7
+ OHLCV
8
+ │
9
+ ▼
10
+ RULE ENGINE
11
+ │
12
+ ├── VETO
13
+ │
14
+ ▼
15
+ ML PROBABILITY FILTER
16
+ │
17
+ ▼
18
+ ADAPTIVE META ENGINE
19
+ │
20
+ ├── Trend persistence
21
+ ├── Momentum alignment
22
+ ├── Noise / entropy
23
+ ├── Regime stability
24
+ ├── Signal freshness
25
+ ├── Historical directional hit-rate
26
+ ├── Monte Carlo robustness
27
+ └── Uncertainty penalty
28
+ │
29
+ ▼
30
+ RISK ENGINE
31
+ │
32
+ ▼
33
+ RANKED SETUPS
34
+
35
+ Research use only.
36
  """
37
 
38
+ import logging
39
+ import sys
40
+ import time
41
+ import math
42
+ from typing import List, Optional, Dict, Any, Tuple
43
+
44
+ import numpy as np
45
+ import pandas as pd
46
+ import gradio as gr
47
+
48
+ from config import (
49
+ DEFAULT_SYMBOLS,
50
+ TOP_N_DEFAULT,
51
+ DEFAULT_ACCOUNT_EQUITY,
52
+ TIMEFRAME,
53
+ CANDLE_LIMIT,
54
+ )
55
+
56
+ from data_fetcher import (
57
+ fetch_multiple,
58
+ fetch_instruments,
59
+ )
60
+
61
+ from regime import detect_regime
62
+ from volume_analysis import analyze_volume
63
+ from risk_engine import evaluate_risk
64
+ from veto import apply_veto
65
+ from scorer import (
66
+ compute_structure_score,
67
+ score_token,
68
+ rank_tokens,
69
+ format_score_bar,
70
+ quality_tier,
71
+ )
72
+ from feature_builder import (
73
+ build_feature_dict,
74
+ validate_features,
75
+ )
76
+ from ml_filter import TradeFilter
77
+
78
+
79
+ # ============================================================================
80
+ # LOGGING
81
+ # ============================================================================
82
+
83
+ logging.basicConfig(
84
+ level=logging.INFO,
85
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
86
+ stream=sys.stdout,
87
+ )
88
+
89
+ logger = logging.getLogger("main")
90
+
91
+
92
+ # ============================================================================
93
+ # GLOBAL ML MODEL
94
+ # ============================================================================
95
+
96
+ _TRADE_FILTER: Optional[TradeFilter] = TradeFilter.load_or_none()
97
+
98
+
99
+ # ============================================================================
100
+ # CONSTANTS
101
+ # ============================================================================
102
+
103
+ _TREND_ICON = {
104
+ "bullish": "▲",
105
+ "ranging": "◆",
106
+ "bearish": "▼",
107
+ }
108
+
109
+ _BREAK_LABEL = {
110
+ 1: "↑UP",
111
+ -1: "↓DN",
112
+ 0: " — ",
113
+ }
114
+
115
+ _DIR_LABEL = {
116
+ 1: "LONG",
117
+ -1: "SHORT",
118
+ 0: "NONE",
119
+ }
120
+
121
+
122
+ # ============================================================================
123
+ # AMDE CONFIGURATION
124
+ # ============================================================================
125
+
126
+ AMDE_CONFIG = {
127
+ # Final meta score threshold
128
+ "meta_threshold": 0.64,
129
+
130
+ # Hard rejection threshold
131
+ "hard_threshold": 0.42,
132
+
133
+ # Historical analysis
134
+ "history_bars": 180,
135
+
136
+ # Forward bars used for directional outcome estimation
137
+ "forward_bars": 5,
138
+
139
+ # Monte Carlo iterations
140
+ "mc_iterations": 2500,
141
+
142
+ # Bootstrap sample size
143
+ "mc_sample_size": 100,
144
+
145
+ # Maximum noise allowed
146
+ "max_noise": 0.78,
147
+
148
+ # Minimum historical observations
149
+ "min_history": 30,
150
+
151
+ # Signal freshness
152
+ "freshness_half_life": 12,
153
+
154
+ # Meta weights
155
+ "weights": {
156
+ "rule": 0.20,
157
+ "ml": 0.22,
158
+ "trend": 0.12,
159
+ "momentum": 0.10,
160
+ "stability": 0.10,
161
+ "freshness": 0.06,
162
+ "history": 0.08,
163
+ "monte_carlo": 0.07,
164
+ "noise": 0.05,
165
+ },
166
+ }
167
+
168
+
169
+ # ============================================================================
170
+ # BASIC HELPERS
171
+ # ============================================================================
172
+
173
+ def safe_float(x, default=0.0):
174
+ try:
175
+ value = float(x)
176
+ if math.isfinite(value):
177
+ return value
178
+ except Exception:
179
+ pass
180
+ return default
181
+
182
+
183
+ def clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float:
184
+ return max(lo, min(hi, safe_float(x)))
185
+
186
+
187
+ def sigmoid(x: float) -> float:
188
+ x = np.clip(x, -40, 40)
189
+ return float(1.0 / (1.0 + np.exp(-x)))
190
+
191
+
192
+ def normalize_signed(x: float, scale: float = 1.0) -> float:
193
+ """
194
+ Converts arbitrary signed value into 0..1.
195
+ """
196
+ if scale == 0:
197
+ return 0.5
198
+
199
+ return clamp(0.5 + 0.5 * np.tanh(x / scale))
200
+
201
+
202
+ # ============================================================================
203
+ # DIRECTION
204
+ # ============================================================================
205
+
206
+ def infer_direction(
207
+ trend: str,
208
+ breakout: int,
209
+ ) -> int:
210
+
211
+ if trend == "bullish" or breakout == 1:
212
+ return 1
213
+
214
+ if trend == "bearish" or breakout == -1:
215
+ return -1
216
+
217
+ return 0
218
+
219
+
220
+ # ============================================================================
221
+ # DATA PREPARATION
222
+ # ============================================================================
223
+
224
+ def prepare_close_series(df: pd.DataFrame) -> pd.Series:
225
+
226
+ if "close" not in df.columns:
227
+ return pd.Series(dtype=float)
228
+
229
+ s = pd.to_numeric(
230
+ df["close"],
231
+ errors="coerce",
232
+ ).dropna()
233
+
234
+ return s
235
+
236
+
237
+ def prepare_returns(df: pd.DataFrame) -> pd.Series:
238
+
239
+ close = prepare_close_series(df)
240
+
241
+ if len(close) < 3:
242
+ return pd.Series(dtype=float)
243
+
244
+ ret = close.pct_change().replace(
245
+ [np.inf, -np.inf],
246
+ np.nan,
247
+ ).dropna()
248
+
249
+ return ret
250
+
251
+
252
+ # ============================================================================
253
+ # TREND PERSISTENCE
254
+ # ============================================================================
255
+
256
+ def calculate_trend_persistence(
257
+ df: pd.DataFrame,
258
+ direction: int,
259
+ lookback: int = 40,
260
+ ) -> float:
261
+
262
+ close = prepare_close_series(df)
263
+
264
+ if len(close) < 10 or direction == 0:
265
+ return 0.5
266
+
267
+ close = close.iloc[-lookback:]
268
+
269
+ returns = close.pct_change().dropna()
270
+
271
+ if len(returns) < 5:
272
+ return 0.5
273
+
274
+ signed = returns * direction
275
+
276
+ positive_fraction = float(
277
+ (signed > 0).mean()
278
+ )
279
+
280
+ cumulative_direction = float(
281
+ np.sign(
282
+ signed.sum()
283
+ )
284
+ )
285
+
286
+ persistence = positive_fraction
287
+
288
+ if cumulative_direction > 0:
289
+ persistence += 0.10
290
+ elif cumulative_direction < 0:
291
+ persistence -= 0.10
292
+
293
+ return clamp(persistence)
294
+
295
+
296
+ # ============================================================================
297
+ # MOMENTUM ALIGNMENT
298
+ # ============================================================================
299
+
300
+ def calculate_momentum_alignment(
301
+ df: pd.DataFrame,
302
+ direction: int,
303
+ ) -> float:
304
+
305
+ close = prepare_close_series(df)
306
+
307
+ if len(close) < 30 or direction == 0:
308
+ return 0.5
309
+
310
+ windows = [5, 10, 20]
311
+
312
+ values = []
313
+
314
+ for w in windows:
315
+
316
+ if len(close) <= w:
317
+ continue
318
+
319
+ change = (
320
+ close.iloc[-1] /
321
+ close.iloc[-w - 1] -
322
+ 1.0
323
+ )
324
+
325
+ signed_change = change * direction
326
+
327
+ # ±3% normalized through sigmoid
328
+ score = sigmoid(
329
+ signed_change / 0.03
330
+ )
331
+
332
+ values.append(score)
333
+
334
+ if not values:
335
+ return 0.5
336
+
337
+ return clamp(
338
+ float(np.mean(values))
339
+ )
340
+
341
+
342
+ # ============================================================================
343
+ # ENTROPY / NOISE
344
+ # ============================================================================
345
+
346
+ def calculate_market_entropy(
347
+ df: pd.DataFrame,
348
+ lookback: int = 60,
349
+ ) -> float:
350
+
351
+ returns = prepare_returns(df)
352
+
353
+ if len(returns) < 20:
354
+ return 0.5
355
+
356
+ returns = returns.iloc[-lookback:]
357
+
358
+ signs = np.sign(returns)
359
+
360
+ up = float((signs > 0).mean())
361
+ down = float((signs < 0).mean())
362
+ flat = float((signs == 0).mean())
363
+
364
+ probs = [
365
+ up,
366
+ down,
367
+ flat,
368
+ ]
369
+
370
+ entropy = 0.0
371
+
372
+ for p in probs:
373
+
374
+ if p > 0:
375
+ entropy -= p * math.log(
376
+ p,
377
+ 3,
378
+ )
379
+
380
+ return clamp(entropy)
381
+
382
+
383
+ def calculate_noise_score(
384
+ df: pd.DataFrame,
385
+ ) -> float:
386
+
387
+ """
388
+ 0 = clean directional market
389
+ 1 = noisy / random market
390
+ """
391
+
392
+ returns = prepare_returns(df)
393
+
394
+ if len(returns) < 20:
395
+ return 0.5
396
+
397
+ returns = returns.iloc[-80:]
398
+
399
+ # Sign autocorrelation
400
+ signs = np.sign(returns)
401
+
402
+ if len(signs) > 5:
403
+ autocorr = safe_float(
404
+ pd.Series(signs).autocorr(),
405
+ 0.0,
406
+ )
407
+ else:
408
+ autocorr = 0.0
409
+
410
+ # Alternation rate
411
+ alternations = (
412
+ signs.iloc[1:].values !=
413
+ signs.iloc[:-1].values
414
+ )
415
+
416
+ alternation_rate = float(
417
+ np.mean(alternations)
418
+ )
419
+
420
+ entropy = calculate_market_entropy(
421
+ df
422
+ )
423
+
424
+ # Higher alternation + entropy = more noise
425
+ noise = (
426
+ 0.35 * alternation_rate +
427
+ 0.40 * entropy +
428
+ 0.25 * (0.5 - autocorr)
429
+ )
430
+
431
+ return clamp(noise)
432
+
433
+
434
+ # ============================================================================
435
+ # REGIME STABILITY
436
+ # ============================================================================
437
+
438
+ def calculate_regime_stability(
439
+ df: pd.DataFrame,
440
+ direction: int,
441
+ ) -> float:
442
+
443
+ close = prepare_close_series(df)
444
+
445
+ if len(close) < 40:
446
+ return 0.5
447
+
448
+ returns = close.pct_change().dropna()
449
+
450
+ if len(returns) < 30:
451
+ return 0.5
452
+
453
+ recent = returns.iloc[-30:]
454
+
455
+ directional = (
456
+ recent * direction
457
+ )
458
+
459
+ mean_move = safe_float(
460
+ directional.mean()
461
+ )
462
+
463
+ volatility = safe_float(
464
+ recent.std(),
465
+ 1e-9,
466
+ )
467
+
468
+ if volatility <= 1e-9:
469
+ return 0.5
470
+
471
+ signal_to_noise = (
472
+ mean_move / volatility
473
+ )
474
+
475
+ base = sigmoid(
476
+ signal_to_noise * 4.0
477
+ )
478
+
479
+ # Compare recent volatility with older volatility
480
+ old = returns.iloc[-80:-30]
481
+
482
+ if len(old) >= 10:
483
+
484
+ old_vol = safe_float(
485
+ old.std(),
486
+ volatility,
487
+ )
488
+
489
+ vol_ratio = (
490
+ volatility /
491
+ max(old_vol, 1e-9)
492
+ )
493
+
494
+ # Extreme volatility expansion reduces stability
495
+ if vol_ratio > 2.5:
496
+ base *= 0.75
497
+
498
+ return clamp(base)
499
+
500
+
501
+ # ============================================================================
502
+ # SIGNAL FRESHNESS
503
+ # ============================================================================
504
+
505
+ def calculate_signal_freshness(
506
+ df: pd.DataFrame,
507
+ direction: int,
508
+ lookback: int = 30,
509
+ ) -> float:
510
+
511
+ if direction == 0:
512
+ return 0.5
513
+
514
+ close = prepare_close_series(df)
515
+
516
+ if len(close) < lookback + 2:
517
+ return 0.5
518
+
519
+ returns = close.pct_change().dropna()
520
+
521
+ recent = returns.iloc[-lookback:]
522
+
523
+ signed = recent * direction
524
+
525
+ # Locate latest strong directional candle
526
+ threshold = (
527
+ signed.std() *
528
+ 0.75
529
+ )
530
+
531
+ candidates = np.where(
532
+ signed.values > threshold
533
+ )[0]
534
+
535
+ if len(candidates) == 0:
536
+ return 0.5
537
+
538
+ bars_since = (
539
+ len(signed) -
540
+ 1 -
541
+ candidates[-1]
542
+ )
543
+
544
+ half_life = AMDE_CONFIG[
545
+ "freshness_half_life"
546
+ ]
547
+
548
+ freshness = math.exp(
549
+ -bars_since /
550
+ max(half_life, 1)
551
+ )
552
+
553
+ return clamp(freshness)
554
+
555
+
556
+ # ============================================================================
557
+ # HISTORICAL DIRECTIONAL HIT RATE
558
+ # ============================================================================
559
+
560
+ def calculate_historical_edge(
561
+ df: pd.DataFrame,
562
+ direction: int,
563
+ forward_bars: int = 5,
564
+ lookback: int = 180,
565
+ ) -> Tuple[float, int]:
566
+
567
+ close = prepare_close_series(df)
568
+
569
+ if direction == 0:
570
+ return 0.5, 0
571
+
572
+ if len(close) < (
573
+ forward_bars + 30
574
+ ):
575
+ return 0.5, 0
576
+
577
+ close = close.iloc[
578
+ -lookback:
579
+ ]
580
+
581
+ values = close.values
582
+
583
+ outcomes = []
584
+
585
+ for i in range(
586
+ 0,
587
+ len(values) - forward_bars,
588
+ ):
589
+
590
+ current = values[i]
591
+ future = values[
592
+ i + forward_bars
593
+ ]
594
+
595
+ if current <= 0:
596
+ continue
597
+
598
+ move = (
599
+ future / current -
600
+ 1.0
601
+ )
602
+
603
+ signed_move = (
604
+ move * direction
605
+ )
606
+
607
+ if signed_move > 0:
608
+ outcomes.append(1)
609
+ else:
610
+ outcomes.append(0)
611
+
612
+ if len(outcomes) < AMDE_CONFIG[
613
+ "min_history"
614
+ ]:
615
+ return 0.5, len(outcomes)
616
+
617
+ hit_rate = float(
618
+ np.mean(outcomes)
619
+ )
620
+
621
+ return clamp(hit_rate), len(outcomes)
622
+
623
+
624
+ # ============================================================================
625
+ # MONTE CARLO ROBUSTNESS
626
+ # ============================================================================
627
+
628
+ def monte_carlo_robustness(
629
+ hit_rate: float,
630
+ iterations: int = 2500,
631
+ sample_size: int = 100,
632
+ rr: float = 2.0,
633
+ seed: Optional[int] = None,
634
+ ) -> Dict[str, float]:
635
+
636
+ """
637
+ Bootstrap/Monte-Carlo robustness.
638
+
639
+ Each synthetic trade:
640
+ win -> +RR
641
+ loss -> -1
642
+
643
+ This is NOT a future-price predictor.
644
+ It measures how stable a given hit-rate
645
+ would be under random sequencing.
646
+ """
647
+
648
+ hit_rate = clamp(
649
+ hit_rate,
650
+ 0.01,
651
+ 0.99,
652
+ )
653
+
654
+ rng = np.random.default_rng(seed)
655
+
656
+ samples = rng.random(
657
+ (
658
+ iterations,
659
+ sample_size,
660
+ )
661
+ ) < hit_rate
662
+
663
+ pnl = (
664
+ samples * rr -
665
+ (~samples) * 1.0
666
+ )
667
+
668
+ final_pnl = pnl.sum(
669
+ axis=1
670
+ )
671
+
672
+ probability_positive = float(
673
+ np.mean(final_pnl > 0)
674
+ )
675
+
676
+ median_pnl = float(
677
+ np.median(final_pnl)
678
+ )
679
+
680
+ worst_5 = float(
681
+ np.percentile(
682
+ final_pnl,
683
+ 5,
684
+ )
685
+ )
686
+
687
+ # Max drawdown distribution
688
+ cumulative = np.cumsum(
689
+ pnl,
690
+ axis=1,
691
+ )
692
+
693
+ running_max = np.maximum.accumulate(
694
+ cumulative,
695
+ axis=1,
696
+ )
697
+
698
+ drawdown = (
699
+ running_max -
700
+ cumulative
701
+ )
702
+
703
+ max_dd = np.max(
704
+ drawdown,
705
+ axis=1,
706
+ )
707
+
708
+ median_dd = float(
709
+ np.median(max_dd)
710
+ )
711
+
712
+ # Robustness:
713
+ # positive-ending probability
714
+ # + downside resilience
715
+ positive_component = (
716
+ probability_positive
717
+ )
718
+
719
+ downside_component = sigmoid(
720
+ -worst_5 / max(
721
+ sample_size * 0.20,
722
+ 1,
723
+ )
724
+ )
725
+
726
+ robustness = (
727
+ 0.70 *
728
+ positive_component +
729
+ 0.30 *
730
+ (1.0 - downside_component)
731
+ )
732
+
733
+ return {
734
+ "robustness": clamp(
735
+ robustness
736
+ ),
737
+ "prob_positive": probability_positive,
738
+ "median_pnl_r": median_pnl,
739
+ "worst_5pct_pnl_r": worst_5,
740
+ "median_max_dd_r": median_dd,
741
+ }
742
+
743
+
744
+ # ============================================================================
745
+ # ML PROBABILITY NORMALIZATION
746
+ # ============================================================================
747
+
748
+ def calculate_ml_component(
749
+ ml_prob: Optional[float],
750
+ threshold: Optional[float],
751
+ ) -> float:
752
+
753
+ if ml_prob is None:
754
+ return 0.50
755
+
756
+ p = clamp(ml_prob)
757
+
758
+ if threshold is None:
759
+ return p
760
+
761
+ threshold = clamp(
762
+ threshold,
763
+ 0.01,
764
+ 0.99,
765
+ )
766
+
767
+ # Center the probability relative to model threshold.
768
+ # Does NOT exaggerate the raw probability.
769
+ spread = max(
770
+ 1.0 - threshold,
771
+ threshold,
772
+ 0.05,
773
+ )
774
+
775
+ relative = (
776
+ p - threshold
777
+ ) / spread
778
+
779
+ return clamp(
780
+ 0.5 +
781
+ 0.5 * np.tanh(relative)
782
+ )
783
+
784
+
785
+ # ============================================================================
786
+ # RULE SCORE COMPONENT
787
+ # ============================================================================
788
+
789
+ def calculate_rule_quality(
790
+ scores: Dict[str, Any],
791
+ ) -> float:
792
+
793
+ values = [
794
+ safe_float(
795
+ scores.get(
796
+ "regime_score",
797
+ 0.5,
798
+ )
799
+ ),
800
+ safe_float(
801
+ scores.get(
802
+ "volume_score",
803
+ 0.5,
804
+ )
805
+ ),
806
+ safe_float(
807
+ scores.get(
808
+ "structure_score",
809
+ 0.5,
810
+ )
811
+ ),
812
+ safe_float(
813
+ scores.get(
814
+ "confidence_score",
815
+ 0.5,
816
+ )
817
+ ),
818
+ ]
819
+
820
+ values = [
821
+ clamp(v)
822
+ for v in values
823
+ ]
824
+
825
+ return clamp(
826
+ np.mean(values)
827
+ )
828
+
829
+
830
+ # ============================================================================
831
+ # ADAPTIVE META ENGINE
832
+ # ============================================================================
833
+
834
+ def calculate_meta_engine(
835
+ df: pd.DataFrame,
836
+ direction: int,
837
+ scores: Dict[str, Any],
838
+ ml_prob: Optional[float],
839
+ ml_threshold: Optional[float],
840
+ rr: float = 2.0,
841
+ ) -> Dict[str, Any]:
842
+
843
+ trend_persistence = (
844
+ calculate_trend_persistence(
845
+ df,
846
+ direction,
847
+ )
848
+ )
849
+
850
+ momentum_alignment = (
851
+ calculate_momentum_alignment(
852
+ df,
853
+ direction,
854
+ )
855
+ )
856
+
857
+ noise = calculate_noise_score(
858
+ df
859
+ )
860
+
861
+ stability = calculate_regime_stability(
862
+ df,
863
+ direction,
864
+ )
865
+
866
+ freshness = calculate_signal_freshness(
867
+ df,
868
+ direction,
869
+ )
870
+
871
+ history_edge, history_n = (
872
+ calculate_historical_edge(
873
+ df,
874
+ direction,
875
+ forward_bars=AMDE_CONFIG[
876
+ "forward_bars"
877
+ ],
878
+ lookback=AMDE_CONFIG[
879
+ "history_bars"
880
+ ],
881
+ )
882
+ )
883
+
884
+ mc = monte_carlo_robustness(
885
+ hit_rate=history_edge,
886
+ iterations=AMDE_CONFIG[
887
+ "mc_iterations"
888
+ ],
889
+ sample_size=AMDE_CONFIG[
890
+ "mc_sample_size"
891
+ ],
892
+ rr=rr,
893
+ )
894
+
895
+ rule_quality = (
896
+ calculate_rule_quality(
897
+ scores
898
+ )
899
+ )
900
+
901
+ ml_component = (
902
+ calculate_ml_component(
903
+ ml_prob,
904
+ ml_threshold,
905
+ )
906
+ )
907
+
908
+ # Noise becomes a positive score:
909
+ # clean market -> close to 1
910
+ noise_component = (
911
+ 1.0 - noise
912
+ )
913
+
914
+ weights = AMDE_CONFIG[
915
+ "weights"
916
+ ]
917
+
918
+ raw_meta = (
919
+ weights["rule"] *
920
+ rule_quality +
921
+
922
+ weights["ml"] *
923
+ ml_component +
924
+
925
+ weights["trend"] *
926
+ trend_persistence +
927
+
928
+ weights["momentum"] *
929
+ momentum_alignment +
930
+
931
+ weights["stability"] *
932
+ stability +
933
+
934
+ weights["freshness"] *
935
+ freshness +
936
+
937
+ weights["history"] *
938
+ history_edge +
939
+
940
+ weights["monte_carlo"] *
941
+ mc["robustness"] +
942
+
943
+ weights["noise"] *
944
+ noise_component
945
+ )
946
+
947
+ # --------------------------------------------------------------
948
+ # Uncertainty penalty
949
+ # --------------------------------------------------------------
950
+
951
+ uncertainty = (
952
+ 0.35 * abs(
953
+ ml_component -
954
+ history_edge
955
+ ) +
956
+
957
+ 0.25 * noise +
958
+
959
+ 0.20 * (
960
+ 1.0 -
961
+ stability
962
+ ) +
963
+
964
+ 0.20 * (
965
+ 1.0 -
966
+ freshness
967
+ )
968
+ )
969
+
970
+ meta_score = (
971
+ raw_meta *
972
+ (
973
+ 1.0 -
974
+ 0.20 * uncertainty
975
+ )
976
+ )
977
+
978
+ # --------------------------------------------------------------
979
+ # Hard rejection conditions
980
+ # --------------------------------------------------------------
981
+
982
+ hard_reject_reasons = []
983
+
984
+ if noise >= AMDE_CONFIG[
985
+ "max_noise"
986
+ ]:
987
+ hard_reject_reasons.append(
988
+ "HIGH_NOISE"
989
+ )
990
+
991
+ if stability < 0.20:
992
+ hard_reject_reasons.append(
993
+ "UNSTABLE_REGIME"
994
+ )
995
+
996
+ if (
997
+ history_n >= AMDE_CONFIG[
998
+ "min_history"
999
+ ]
1000
+ and history_edge < 0.35
1001
+ ):
1002
+ hard_reject_reasons.append(
1003
+ "WEAK_HISTORICAL_EDGE"
1004
+ )
1005
+
1006
+ if meta_score < AMDE_CONFIG[
1007
+ "hard_threshold"
1008
+ ]:
1009
+ hard_reject_reasons.append(
1010
+ "LOW_META_SCORE"
1011
+ )
1012
+
1013
+ hard_rejected = (
1014
+ len(hard_reject_reasons) > 0
1015
+ )
1016
+
1017
+ approved = (
1018
+ not hard_rejected
1019
+ and
1020
+ meta_score >= AMDE_CONFIG[
1021
+ "meta_threshold"
1022
+ ]
1023
+ )
1024
+
1025
+ # --------------------------------------------------------------
1026
+ # Confidence
1027
+ # --------------------------------------------------------------
1028
+
1029
+ confidence = (
1030
+ 0.35 * meta_score +
1031
+ 0.20 * trend_persistence +
1032
+ 0.15 * stability +
1033
+ 0.15 * history_edge +
1034
+ 0.15 * mc["robustness"]
1035
+ )
1036
+
1037
+ confidence = clamp(
1038
+ confidence
1039
+ )
1040
+
1041
+ return {
1042
+ "meta_score": meta_score,
1043
+
1044
+ "approved": approved,
1045
+
1046
+ "hard_rejected": hard_rejected,
1047
+
1048
+ "hard_reject_reasons":
1049
+ hard_reject_reasons,
1050
 
1051
+ "confidence": confidence,
1052
 
1053
+ "rule_quality":
1054
+ rule_quality,
 
 
 
 
 
 
 
 
 
 
 
 
 
1055
 
1056
+ "ml_component":
1057
+ ml_component,
 
 
 
 
1058
 
1059
+ "trend_persistence":
1060
+ trend_persistence,
1061
 
1062
+ "momentum_alignment":
1063
+ momentum_alignment,
 
1064
 
1065
+ "noise":
1066
+ noise,
1067
 
1068
+ "noise_component":
1069
+ noise_component,
1070
+
1071
+ "regime_stability":
1072
+ stability,
1073
+
1074
+ "signal_freshness":
1075
+ freshness,
1076
+
1077
+ "historical_edge":
1078
+ history_edge,
1079
+
1080
+ "historical_samples":
1081
+ history_n,
1082
+
1083
+ "uncertainty":
1084
+ uncertainty,
1085
 
1086
+ "mc_robustness":
1087
+ mc["robustness"],
1088
+
1089
+ "mc_prob_positive":
1090
+ mc["prob_positive"],
1091
+
1092
+ "mc_median_pnl_r":
1093
+ mc["median_pnl_r"],
1094
+
1095
+ "mc_worst_5pct_pnl_r":
1096
+ mc["worst_5pct_pnl_r"],
1097
+
1098
+ "mc_median_max_dd_r":
1099
+ mc["median_max_dd_r"],
1100
+ }
1101
+
1102
+
1103
+ # ============================================================================
1104
+ # SINGLE SYMBOL ANALYSIS
1105
+ # ============================================================================
1106
 
1107
  def analyze_single(
1108
  symbol: str,
 
1112
  equity_drawdown_pct: float = 0.0,
1113
  use_ml: bool = True,
1114
  ) -> Dict[str, Any]:
1115
+
1116
+ # ================================================================
1117
+ # RULE ENGINE
1118
+ # ================================================================
1119
+
1120
+ regime_data = detect_regime(
1121
+ df
1122
+ )
1123
+
1124
+ volume_data = analyze_volume(
1125
+ df,
1126
+ atr_series=regime_data[
1127
+ "atr_series"
1128
+ ],
1129
+ )
1130
+
1131
+ structure_sc = (
1132
+ compute_structure_score(
1133
+ regime_data
1134
+ )
1135
+ )
1136
+
1137
+ direction = infer_direction(
1138
+ regime_data["trend"],
1139
+ volume_data["breakout"],
1140
+ )
1141
+
1142
+ vetoed, veto_reason = apply_veto(
1143
+ regime_data,
1144
+ volume_data,
1145
+ structure_sc,
1146
+ direction=direction,
1147
+ )
1148
+
1149
+ scores = score_token(
1150
+ regime_data,
1151
+ volume_data,
1152
+ vetoed,
1153
+ )
1154
+
1155
+ # ================================================================
1156
+ # ML FILTER
1157
+ # ================================================================
1158
+
1159
+ ml_prob = None
1160
+ ml_approved = None
1161
  ml_reject_reason = ""
1162
 
1163
+ if (
1164
+ use_ml
1165
+ and
1166
+ _TRADE_FILTER is not None
1167
+ and
1168
+ not vetoed
1169
+ ):
1170
+
1171
  try:
1172
+
1173
+ feat = build_feature_dict(
1174
+ regime_data,
1175
+ volume_data,
1176
+ scores,
1177
+ )
1178
+
1179
  if validate_features(feat):
1180
+
1181
+ result = (
1182
+ _TRADE_FILTER.predict(
1183
+ regime_data,
1184
+ volume_data,
1185
+ scores,
1186
+ )
1187
+ )
1188
+
1189
+ ml_prob = result.probability
1190
  ml_approved = result.approved
1191
+ ml_reject_reason = (
1192
+ result.reject_reason
1193
+ )
1194
+
1195
  else:
1196
+
1197
+ ml_approved = None
1198
+
1199
  except Exception as e:
1200
+
1201
+ logger.warning(
1202
+ f"{symbol}: ML filter error: {e}"
1203
+ )
1204
+
1205
  ml_approved = None
1206
 
1207
+ # ================================================================
1208
+ # ADAPTIVE META ENGINE
1209
+ # ================================================================
1210
+
1211
+ ml_threshold = None
1212
+
1213
+ if _TRADE_FILTER is not None:
1214
+ ml_threshold = (
1215
+ _TRADE_FILTER.threshold
1216
+ )
1217
+
1218
+ meta = calculate_meta_engine(
1219
+ df=df,
1220
+ direction=direction,
1221
+ scores=scores,
1222
+ ml_prob=ml_prob,
1223
+ ml_threshold=ml_threshold,
1224
+ )
1225
+
1226
+ # ================================================================
1227
+ # FINAL DECISION
1228
+ # ================================================================
1229
+
1230
+ ml_pass = (
1231
+ ml_approved is None
1232
+ or
1233
+ ml_approved
1234
+ )
1235
+
1236
  final_approved = (
1237
+ not vetoed
1238
+ and
1239
+ ml_pass
1240
+ and
1241
+ meta["approved"]
1242
+ )
1243
+
1244
+ # ================================================================
1245
+ # RISK ENGINE
1246
+ # ================================================================
1247
+
1248
+ risk_data = {}
1249
+
1250
+ if final_approved:
1251
+
1252
+ risk_data = evaluate_risk(
1253
+ close=float(
1254
+ df["close"].iloc[-1]
1255
+ ),
1256
+
1257
+ atr=regime_data[
1258
+ "atr"
1259
+ ],
1260
+
1261
+ atr_pct=regime_data[
1262
+ "atr_pct"
1263
+ ],
1264
+
1265
+ regime_score=regime_data[
1266
+ "regime_score"
1267
+ ],
1268
+
1269
+ vol_ratio=regime_data[
1270
+ "vol_ratio"
1271
+ ],
1272
+
1273
+ volume_score=volume_data[
1274
+ "volume_score"
1275
+ ],
1276
+
1277
+ regime_confidence=
1278
+ regime_data[
1279
+ "regime_confidence"
1280
+ ],
1281
+
1282
+ vol_compressed=
1283
+ regime_data[
1284
+ "vol_compressed"
1285
+ ],
1286
+
1287
+ consec_losses=
1288
+ consec_losses,
1289
+
1290
+ equity_drawdown_pct=
1291
+ equity_drawdown_pct,
1292
+
1293
+ account_equity=
1294
+ account_equity,
1295
+ )
1296
+
1297
+ # ================================================================
1298
+ # RESULT
1299
+ # ================================================================
1300
 
1301
  return {
1302
+
1303
+ "symbol":
1304
+ symbol,
1305
+
1306
+ "close":
1307
+ float(
1308
+ df["close"].iloc[-1]
1309
+ ),
1310
+
1311
+ "trend":
1312
+ regime_data["trend"],
1313
+
1314
+ "adx":
1315
+ regime_data["adx"],
1316
+
1317
+ "di_plus":
1318
+ regime_data["di_plus"],
1319
+
1320
+ "di_minus":
1321
+ regime_data["di_minus"],
1322
+
1323
+ "vol_ratio":
1324
+ regime_data["vol_ratio"],
1325
+
1326
+ "vol_compressed":
1327
+ regime_data["vol_compressed"],
1328
+
1329
+ "vol_expanding_from_base":
1330
+ regime_data[
1331
+ "vol_expanding_from_base"
1332
+ ],
1333
+
1334
+ "vol_expanding":
1335
+ regime_data[
1336
+ "vol_expanding"
1337
+ ],
1338
+
1339
+ "dist_atr":
1340
+ regime_data["dist_atr"],
1341
+
1342
+ "price_extended":
1343
+ (
1344
+ regime_data[
1345
+ "price_extended_long"
1346
+ ]
1347
+ or
1348
+ regime_data[
1349
+ "price_extended_short"
1350
+ ]
1351
+ ),
1352
+
1353
+ "regime_confidence":
1354
+ regime_data[
1355
+ "regime_confidence"
1356
+ ],
1357
+
1358
+ "spike":
1359
+ volume_data["spike"],
1360
+
1361
+ "climax":
1362
+ volume_data["climax"],
1363
+
1364
+ "absorption":
1365
+ volume_data["absorption"],
1366
+
1367
+ "failed_breakout":
1368
+ volume_data["failed_breakout"],
1369
+
1370
+ "recent_failed":
1371
+ volume_data[
1372
+ "recent_failed_count"
1373
+ ],
1374
+
1375
+ "breakout":
1376
+ volume_data["breakout"],
1377
+
1378
+ "obv_slope":
1379
+ volume_data[
1380
+ "obv_slope_norm"
1381
+ ],
1382
+
1383
+ "delta_sign":
1384
+ volume_data["delta_sign"],
1385
+
1386
+ "direction":
1387
+ direction,
1388
+
1389
+ "rule_vetoed":
1390
+ vetoed,
1391
+
1392
+ "veto_reason":
1393
+ veto_reason,
1394
+
1395
+ "ml_prob":
1396
+ ml_prob,
1397
+
1398
+ "ml_approved":
1399
+ ml_approved,
1400
+
1401
+ "ml_reject_reason":
1402
+ ml_reject_reason,
1403
+
1404
+ "final_approved":
1405
+ final_approved,
1406
+
1407
+ "regime_score":
1408
+ scores["regime_score"],
1409
+
1410
+ "volume_score":
1411
+ scores["volume_score"],
1412
+
1413
+ "structure_score":
1414
+ scores["structure_score"],
1415
+
1416
+ "confidence_score":
1417
+ scores["confidence_score"],
1418
+
1419
+ "total_score":
1420
+ scores["total_score"],
1421
+
1422
+ "meta":
1423
+ meta,
1424
+
1425
+ "risk":
1426
+ risk_data,
1427
  }
1428
 
1429
 
1430
+ # ============================================================================
1431
+ # ML STATUS
1432
+ # ============================================================================
1433
+
1434
  def _ml_status(d: Dict) -> str:
1435
+
1436
  if _TRADE_FILTER is None:
1437
  return "NO_MODEL"
1438
+
1439
  if d["rule_vetoed"]:
1440
  return "RULE_VET"
1441
+
1442
  if d["ml_prob"] is None:
1443
+ return "ML_ERR"
1444
+
1445
+ prob_str = (
1446
+ f"{d['ml_prob']:.3f}"
1447
+ )
1448
+
1449
+ if d["ml_approved"]:
1450
+ return f"✓{prob_str}"
1451
+
1452
+ return f"✗{prob_str}"
1453
+
1454
+
1455
+ # ============================================================================
1456
+ # META STATUS
1457
+ # ============================================================================
1458
+
1459
+ def _meta_status(
1460
+ d: Dict,
1461
+ ) -> str:
1462
 
1463
+ meta = d["meta"]
1464
+
1465
+ score = meta[
1466
+ "meta_score"
1467
+ ]
1468
+
1469
+ if meta["hard_rejected"]:
1470
+ return f"✗{score:.3f}"
1471
+
1472
+ if meta["approved"]:
1473
+ return f"✓{score:.3f}"
1474
+
1475
+ return f"~{score:.3f}"
1476
+
1477
+
1478
+ # ============================================================================
1479
+ # RANKED TABLE
1480
+ # ============================================================================
1481
+
1482
+ def build_ranked_table(
1483
+ ranked: list,
1484
+ top_n: int,
1485
+ ) -> str:
1486
 
 
1487
  hdr = (
1488
+ f"{'#':>3} "
1489
+ f"{'Symbol':<14} "
1490
+ f"{'Score':>7} "
1491
+ f"{'Tier':>4} "
1492
+ f"{'Meta':>7} "
1493
+ f"{'MC':>6} "
1494
+ f"{'Noise':>6} "
1495
+ f"{'Stab':>6} "
1496
+ f"{'Fresh':>6} "
1497
+ f"{'Trend':>7} "
1498
+ f"{'ADX':>5} "
1499
+ f"{'VR':>5} "
1500
+ f"{'ML':>8} "
1501
+ f"{'Status'}\n"
1502
+ )
1503
+
1504
+ sep = "─" * 130 + "\n"
1505
+
1506
+ rows = (
1507
+ hdr +
1508
+ sep
1509
  )
 
 
1510
 
1511
+ for rank, (
1512
+ sym,
1513
+ d,
1514
+ ) in enumerate(
1515
+ ranked[:top_n],
1516
+ 1,
1517
+ ):
1518
+
1519
+ icon = _TREND_ICON.get(
1520
+ d["trend"],
1521
+ "?",
1522
+ )
1523
+
1524
+ tier = quality_tier(
1525
+ d["total_score"]
1526
+ )
1527
+
1528
+ ml_str = _ml_status(
1529
+ d
1530
+ )
1531
+
1532
+ meta = d["meta"]
1533
+
1534
+ meta_str = (
1535
+ f"{meta['meta_score']:.3f}"
1536
+ )
1537
+
1538
+ mc_str = (
1539
+ f"{meta['mc_robustness']:.3f}"
1540
+ )
1541
+
1542
+ noise_str = (
1543
+ f"{meta['noise']:.3f}"
1544
+ )
1545
+
1546
+ stab_str = (
1547
+ f"{meta['regime_stability']:.3f}"
1548
+ )
1549
+
1550
+ fresh_str = (
1551
+ f"{meta['signal_freshness']:.3f}"
1552
+ )
1553
 
1554
  if d["rule_vetoed"]:
1555
+
1556
+ status = (
1557
+ "RULE_VET"
1558
+ )
1559
+
1560
  elif not d["final_approved"]:
1561
+
1562
+ if meta["hard_rejected"]:
1563
+ status = "META_VET"
1564
+ else:
1565
+ status = "FILTERED"
1566
+
1567
  else:
1568
+
1569
+ status = "OK"
1570
 
1571
  rows += (
1572
+ f"{rank:>3} "
1573
+ f"{sym:<14} "
1574
+ f"{d['total_score']:>7.4f} "
1575
+ f"{tier:>4} "
1576
+ f"{meta_str:>7} "
1577
+ f"{mc_str:>6} "
1578
+ f"{noise_str:>6} "
1579
+ f"{stab_str:>6} "
1580
+ f"{fresh_str:>6} "
1581
+ f"{icon} "
1582
+ f"{d['trend']:<5} "
1583
+ f"{d['adx']:>5.1f} "
1584
+ f"{d['vol_ratio']:>5.2f} "
1585
+ f"{ml_str:>8} "
1586
+ f"{status}\n"
1587
  )
1588
+
1589
  return rows
1590
 
1591
 
1592
+ # ============================================================================
1593
+ # BEST DETAIL
1594
+ # ============================================================================
1595
+
1596
+ def build_best_detail(
1597
+ data: Dict[str, Any],
1598
+ ) -> str:
1599
+
1600
+ r = data.get(
1601
+ "risk",
1602
+ {},
1603
+ )
1604
+
1605
+ meta = data[
1606
+ "meta"
1607
+ ]
1608
+
1609
+ sym = data[
1610
+ "symbol"
1611
+ ]
1612
+
1613
+ icon = _TREND_ICON.get(
1614
+ data["trend"],
1615
+ "?",
1616
+ )
1617
+
1618
+ # --------------------------------------------------------------
1619
+ # Volume state
1620
+ # --------------------------------------------------------------
1621
 
1622
  vol_state = []
1623
+
1624
+ if data[
1625
+ "vol_compressed"
1626
+ ]:
1627
+ vol_state.append(
1628
+ "COMPRESSED"
1629
+ )
1630
+
1631
+ if data[
1632
+ "vol_expanding_from_base"
1633
+ ]:
1634
+ vol_state.append(
1635
+ "EXPANDING FROM BASE ✓"
1636
+ )
1637
+
1638
+ if (
1639
+ data["vol_expanding"]
1640
+ and
1641
+ not data[
1642
+ "vol_expanding_from_base"
1643
+ ]
1644
+ ):
1645
+ vol_state.append(
1646
+ "EXPANDING (no base)"
1647
+ )
1648
+
1649
+ vol_state_str = (
1650
+ " | ".join(
1651
+ vol_state
1652
+ )
1653
+ or
1654
+ "NORMAL"
1655
+ )
1656
+
1657
+ # --------------------------------------------------------------
1658
+ # ML section
1659
+ # --------------------------------------------------------------
1660
 
1661
  ml_section = ""
1662
+
1663
  if _TRADE_FILTER is not None:
1664
+
1665
+ prob_str = (
1666
+ f"{data['ml_prob']:.4f}"
1667
+ if data["ml_prob"]
1668
+ is not None
1669
+ else "N/A"
1670
+ )
1671
+
1672
+ thresh_str = (
1673
+ f"{_TRADE_FILTER.threshold:.4f}"
1674
+ )
1675
+
1676
+ decision = (
1677
+ "APPROVED ✓"
1678
+ if data[
1679
+ "ml_approved"
1680
+ ]
1681
+ else
1682
+ "FILTERED ✗"
1683
+ )
1684
+
1685
  ml_section = (
1686
+ "\n"
1687
+ " ── ML PROBABILITY FILTER ───────────────\n"
1688
  f" P(win): {prob_str}\n"
1689
  f" Threshold: {thresh_str}\n"
1690
  f" ML Decision: {decision}\n"
1691
  )
1692
 
1693
+ # --------------------------------------------------------------
1694
+ # AMDE section
1695
+ # --------------------------------------------------------------
1696
+
1697
+ meta_section = (
1698
+ "\n"
1699
+ " ── ADAPTIVE META-DECISION ENGINE ────────\n"
1700
+ f" META SCORE: {meta['meta_score']:.4f}\n"
1701
+ f" META CONFIDENCE: {meta['confidence']:.4f}\n"
1702
+ f" Trend Persistence:{meta['trend_persistence']:.4f}\n"
1703
+ f" Momentum Align: {meta['momentum_alignment']:.4f}\n"
1704
+ f" Regime Stability: {meta['regime_stability']:.4f}\n"
1705
+ f" Signal Freshness: {meta['signal_freshness']:.4f}\n"
1706
+ f" Noise: {meta['noise']:.4f}\n"
1707
+ f" Uncertainty: {meta['uncertainty']:.4f}\n"
1708
+ f" Historical Edge: {meta['historical_edge']:.4f}\n"
1709
+ f" History Samples: {meta['historical_samples']}\n"
1710
+ f" MC Robustness: {meta['mc_robustness']:.4f}\n"
1711
+ f" MC P(positive): {meta['mc_prob_positive']:.4f}\n"
1712
+ f" MC Median PnL: {meta['mc_median_pnl_r']:.2f}R\n"
1713
+ f" MC Worst 5%: {meta['mc_worst_5pct_pnl_r']:.2f}R\n"
1714
+ f" MC Median MaxDD: {meta['mc_median_max_dd_r']:.2f}R\n"
1715
+ )
1716
+
1717
+ if meta[
1718
+ "hard_reject_reasons"
1719
+ ]:
1720
+
1721
+ meta_section += (
1722
+ " Meta Veto: "
1723
+ +
1724
+ ", ".join(
1725
+ meta[
1726
+ "hard_reject_reasons"
1727
+ ]
1728
+ )
1729
+ +
1730
+ "\n"
1731
+ )
1732
+
1733
+ # --------------------------------------------------------------
1734
+ # Risk
1735
+ # --------------------------------------------------------------
1736
+
1737
  risk_section = ""
1738
+
1739
  if r:
1740
+
1741
  risk_section = (
1742
+ "\n"
1743
+ " ── RISK PARAMETERS ──────────────────────\n"
1744
  f" Entry: {r.get('entry_price', 0):.8f}\n"
1745
+ f" ATR: {r.get('atr', 0):.8f} "
1746
+ f"({r.get('atr_pct', 0):.3f}%)\n"
1747
  f" Stop Mult: {r.get('stop_mult', 0):.1f}x ATR\n"
1748
+ f" LONG → Stop: {r.get('stop_long', 0):.8f} "
1749
+ f"Target: {r.get('target_long', 0):.8f}\n"
1750
+ f" SHORT → Stop: {r.get('stop_short', 0):.8f} "
1751
+ f"Target: {r.get('target_short', 0):.8f}\n"
1752
  f" R:R Ratio: 1 : {r.get('rr_ratio', 2):.1f}\n"
1753
  f" Risk Fraction: {r.get('risk_fraction', 0):.4f}%\n"
1754
  f" $ At Risk: ${r.get('dollar_at_risk', 0):.2f}\n"
1755
  f" Position Size: ${r.get('position_notional', 0):.2f} notional\n"
1756
  f" Leverage (est): {r.get('leverage_implied', 0):.2f}x\n"
1757
  f" Consec. Losses: {r.get('consec_losses', 0)}\n"
1758
+ f" Sizing Halted: "
1759
+ f"{'YES ⛔' if r.get('sizing_halted') else 'no'}\n"
1760
  )
1761
 
1762
+ # --------------------------------------------------------------
1763
+ # Final
1764
+ # --------------------------------------------------------------
1765
+
1766
+ lines = [
1767
+
1768
+ "═" * 68,
1769
+
1770
+ f" BEST APPROVED SETUP: "
1771
+ f"{sym} "
1772
+ f"[{_DIR_LABEL.get(data['direction'], '?')}]",
1773
+
1774
+ "═" * 68,
1775
+
1776
+ f" Trend: "
1777
+ f"{icon} "
1778
+ f"{data['trend'].upper()}",
1779
+
1780
+ f" ADX: "
1781
+ f"{data['adx']:.1f} "
1782
+ f"(DI+ {data['di_plus']:.1f} / "
1783
+ f"DI- {data['di_minus']:.1f})",
1784
+
1785
+ f" Vol State: "
1786
+ f"{vol_state_str}",
1787
+
1788
+ f" Dist from Mean: "
1789
+ f"{data['dist_atr']:.2f} ATR",
1790
+
1791
+ f" Regime Confidence:"
1792
+ f"{data['regime_confidence']:.3f}",
1793
+
1794
+ "",
1795
+
1796
+ " ── SCORES ──────────────────────────────────────────",
1797
+
1798
+ f" Regime: "
1799
+ f"{format_score_bar(data['regime_score'])}",
1800
+
1801
+ f" Volume: "
1802
+ f"{format_score_bar(data['volume_score'])}",
1803
+
1804
+ f" Structure: "
1805
+ f"{format_score_bar(data['structure_score'])}",
1806
+
1807
+ f" Confidence: "
1808
+ f"{format_score_bar(data['confidence_score'])}",
1809
+
1810
+ f" TOTAL: "
1811
+ f"{format_score_bar(data['total_score'])}",
1812
+
1813
+ ]
1814
+
1815
+ lines.append(
1816
+ ml_section
1817
+ )
1818
+
1819
+ lines.append(
1820
+ meta_section
1821
+ )
1822
+
1823
+ lines.append(
1824
+ risk_section
1825
+ )
1826
+
1827
+ lines.append(
1828
+ "═" * 68
1829
+ )
1830
+
1831
+ return "\n".join(
1832
+ lines
1833
+ )
1834
+
1835
 
1836
+ # ============================================================================
1837
+ # SYMBOL PARSER
1838
+ # ============================================================================
1839
+
1840
+ def parse_symbols(
1841
+ raw: str,
1842
+ ) -> List[str]:
1843
 
 
1844
  out = []
1845
+
1846
+ for tok in (
1847
+ raw
1848
+ .replace(",", " ")
1849
+ .replace("\n", " ")
1850
+ .split()
1851
+ ):
1852
+
1853
  tok = tok.strip().upper()
1854
+
1855
+ if not tok:
1856
+ continue
1857
+
1858
+ out.append(
1859
+ tok
1860
+ if "-" in tok
1861
+ else
1862
+ f"{tok}-USDT"
1863
+ )
1864
+
1865
+ return (
1866
+ out
1867
+ or
1868
+ DEFAULT_SYMBOLS
1869
+ )
1870
 
1871
 
1872
+ # ============================================================================
1873
+ # RUN ANALYSIS
1874
+ # ============================================================================
1875
+
1876
  def run_analysis(
1877
  symbols_input: str,
1878
  equity: float,
 
1881
  top_n: int,
1882
  use_live: bool,
1883
  use_ml: bool,
1884
+ progress=gr.Progress(
1885
+ track_tqdm=False
1886
+ ),
1887
  ) -> str:
1888
+
1889
  t0 = time.time()
1890
+
1891
  lines = []
1892
 
1893
+ # ================================================================
1894
+ # HEADER
1895
+ # ================================================================
1896
+
1897
+ ml_status_str = (
1898
+ "ACTIVE"
1899
+ if (
1900
+ _TRADE_FILTER
1901
+ is not None
1902
+ and
1903
+ use_ml
1904
+ )
1905
+ else
1906
+ (
1907
+ "DISABLED"
1908
+ if not use_ml
1909
+ else
1910
+ "NOT TRAINED"
1911
+ )
1912
  )
1913
 
1914
  lines += [
1915
+
1916
+ "━" * 78,
1917
+
1918
+ " OKX QUANTITATIVE ANALYSIS ENGINE v4",
1919
+
1920
+ " Adaptive Meta-Decision Engine",
1921
+
1922
  f" ML Filter: {ml_status_str}",
1923
+
1924
+ "━" * 78,
1925
+
1926
  ]
1927
 
1928
+ # ================================================================
1929
+ # ML INFORMATION
1930
+ # ================================================================
1931
+
1932
+ if (
1933
+ _TRADE_FILTER
1934
+ is not None
1935
+ and
1936
+ use_ml
1937
+ ):
1938
+
1939
+ lines.append(
1940
+ f" ML threshold: "
1941
+ f"{_TRADE_FILTER.threshold:.4f}"
1942
+ f" | Stats: "
1943
+ f"{_TRADE_FILTER.stats()}"
1944
+ )
1945
+
1946
+ # ================================================================
1947
+ # SYMBOLS
1948
+ # ================================================================
1949
 
1950
  if use_live:
1951
+
1952
+ lines.append(
1953
+ "⟳ Fetching live OKX instrument list..."
1954
+ )
1955
+
1956
+ symbols = (
1957
+ fetch_instruments(
1958
+ "SPOT"
1959
+ )
1960
+ or
1961
+ DEFAULT_SYMBOLS
1962
+ )
1963
+
1964
+ lines.append(
1965
+ f"✓ {len(symbols)} live USDT instruments"
1966
+ )
1967
+
1968
  else:
1969
+
1970
+ symbols = parse_symbols(
1971
+ symbols_input
1972
+ )
1973
+
1974
+ lines.append(
1975
+ f"✓ {len(symbols)} symbol(s)"
1976
+ )
1977
+
1978
+ # ================================================================
1979
+ # PARAMETERS
1980
+ # ================================================================
1981
 
1982
  lines.append(
1983
+
1984
+ f" Equity: ${equity:,.0f}"
1985
+ f" | Losses: {int(consec_losses)}"
1986
+ f" | DD: {drawdown_pct:.1f}%"
1987
+ f" | TF: {TIMEFRAME}"
1988
+
1989
  )
1990
+
1991
  lines.append("")
1992
 
1993
+ total = len(
1994
+ symbols
1995
+ )
1996
+
1997
+ # ================================================================
1998
+ # FETCH
1999
+ # ================================================================
2000
 
2001
+ def prog_cb(
2002
+ i,
2003
+ t,
2004
+ sym,
2005
+ ):
2006
+
2007
+ progress(
2008
+ i / max(t, 1),
2009
+ desc=(
2010
+ f"Fetching "
2011
+ f"{sym} "
2012
+ f"({i}/{t})"
2013
+ ),
2014
+ )
2015
+
2016
+ ohlcv_map = fetch_multiple(
2017
+ symbols,
2018
+ min_bars=50,
2019
+ progress_callback=prog_cb,
2020
+ )
2021
+
2022
+ lines.append(
2023
+ f"✓ Fetched "
2024
+ f"{len(ohlcv_map)}/{total}"
2025
+ )
2026
 
 
 
2027
  lines.append("")
2028
 
2029
+ # ================================================================
2030
+ # ANALYZE
2031
+ # ================================================================
2032
+
2033
+ all_results: Dict[
2034
+ str,
2035
+ Any
2036
+ ] = {}
2037
+
2038
  errors = []
2039
 
2040
+ for sym, df in (
2041
+ ohlcv_map.items()
2042
+ ):
2043
+
2044
  try:
2045
+
2046
+ all_results[sym] = (
2047
+ analyze_single(
2048
+ sym,
2049
+ df,
2050
+ account_equity=
2051
+ equity,
2052
+ consec_losses=
2053
+ int(
2054
+ consec_losses
2055
+ ),
2056
+ equity_drawdown_pct=
2057
+ drawdown_pct /
2058
+ 100.0,
2059
+ use_ml=
2060
+ use_ml,
2061
+ )
2062
  )
2063
+
2064
  except Exception as exc:
2065
+
2066
+ logger.error(
2067
+ f"{sym}: {exc}",
2068
+ exc_info=True,
2069
+ )
2070
+
2071
+ errors.append(
2072
+ sym
2073
+ )
2074
+
2075
+ # ================================================================
2076
+ # ERRORS
2077
+ # ================================================================
2078
 
2079
  if errors:
 
2080
 
2081
+ lines.append(
2082
+ f"⚠ Errors: "
2083
+ f"{', '.join(errors)}"
2084
+ )
2085
+
2086
+ # ================================================================
2087
+ # RANK
2088
+ # ================================================================
2089
+
2090
+ ranked = rank_tokens(
2091
+ all_results
2092
+ )
2093
+
2094
+ rule_vetoed_n = sum(
2095
+ 1
2096
+ for _,
2097
+ d in ranked
2098
+ if d["rule_vetoed"]
2099
+ )
2100
+
2101
+ ml_filtered_n = sum(
2102
+ 1
2103
+ for _,
2104
+ d in ranked
2105
+ if (
2106
+ not d["rule_vetoed"]
2107
+ and
2108
+ not d["final_approved"]
2109
+ and
2110
+ not d["meta"][
2111
+ "hard_rejected"
2112
+ ]
2113
+ )
2114
+ )
2115
+
2116
+ meta_filtered_n = sum(
2117
+ 1
2118
+ for _,
2119
+ d in ranked
2120
+ if (
2121
+ not d["rule_vetoed"]
2122
+ and
2123
+ d["meta"][
2124
+ "hard_rejected"
2125
+ ]
2126
+ )
2127
+ )
2128
+
2129
+ approved_n = sum(
2130
+ 1
2131
+ for _,
2132
+ d in ranked
2133
+ if d["final_approved"]
2134
+ )
2135
 
2136
+ # ================================================================
2137
+ # SUMMARY
2138
+ # ================================================================
2139
 
2140
  lines += [
2141
+
2142
+ f" {len(all_results)} analyzed"
2143
+ f" | {approved_n} approved"
2144
+ f" | {rule_vetoed_n} rule-vetoed"
2145
+ f" | {ml_filtered_n} ML-filtered"
2146
+ f" | {meta_filtered_n} meta-vetoed",
2147
+
2148
+ "",
2149
+
2150
+ " ADAPTIVE META ENGINE",
2151
+
2152
+ f" Threshold: "
2153
+ f"{AMDE_CONFIG['meta_threshold']:.3f}",
2154
+
2155
+ f" Hard threshold: "
2156
+ f"{AMDE_CONFIG['hard_threshold']:.3f}",
2157
+
2158
+ f" MC iterations: "
2159
+ f"{AMDE_CONFIG['mc_iterations']}",
2160
+
2161
  "",
2162
+
2163
  " RANKED SETUPS",
2164
+
2165
+ "─" * 130,
2166
+
2167
+ build_ranked_table(
2168
+ ranked,
2169
+ int(top_n),
2170
+ ),
2171
+
2172
+ ]
2173
+
2174
+ # ================================================================
2175
+ # BEST APPROVED
2176
+ # ================================================================
2177
+
2178
+ final_approved = [
2179
+ (s, d)
2180
+ for s, d in ranked
2181
+ if d["final_approved"]
2182
  ]
2183
 
 
2184
  if final_approved:
2185
+
2186
+ best_sym, best_data = (
2187
+ final_approved[0]
2188
+ )
2189
+
2190
+ lines += [
2191
+ "",
2192
+ build_best_detail(
2193
+ best_data
2194
+ ),
2195
+ ]
2196
+
2197
  else:
2198
+
2199
+ lines += [
2200
+
2201
+ "",
2202
+
2203
+ " ⚠ No fully approved setups.",
2204
+
2205
+ " Possible causes:",
2206
+
2207
+ " • unfavorable market regime",
2208
+
2209
+ " • ML rejection",
2210
+
2211
+ " • high market noise",
2212
+
2213
+ " • unstable regime",
2214
+
2215
+ " • weak historical directional edge",
2216
+
2217
+ " • low Monte Carlo robustness",
2218
+
2219
+ " • meta score below threshold",
2220
+
2221
+ ]
2222
+
2223
+ # ================================================================
2224
+ # SESSION STATS
2225
+ # ================================================================
2226
+
2227
+ if (
2228
+ _TRADE_FILTER
2229
+ is not None
2230
+ and
2231
+ use_ml
2232
+ ):
2233
+
2234
  lines += [
2235
  "",
2236
+ f" ML session stats: "
2237
+ f"{_TRADE_FILTER.stats()}",
 
2238
  ]
2239
 
2240
+ # ================================================================
2241
+ # FOOTER
2242
+ # ================================================================
2243
+
2244
+ lines += [
2245
+
2246
+ "",
2247
+
2248
+ f" ✓ Complete in "
2249
+ f"{time.time() - t0:.1f}s",
2250
+
2251
+ "━" * 78,
2252
+
2253
+ ]
2254
+
2255
+ return "\n".join(
2256
+ lines
2257
+ )
2258
 
 
 
2259
 
2260
+ # ============================================================================
2261
+ # GRADIO APP
2262
+ # ============================================================================
2263
 
2264
  def build_app() -> gr.Blocks:
2265
+
2266
  with gr.Blocks(
2267
+
2268
+ title=
2269
+ "OKX Quant Engine v4",
2270
+
2271
  theme=gr.themes.Base(
2272
+
2273
  primary_hue="slate",
2274
+
2275
  neutral_hue="zinc",
2276
+
2277
+ font=[
2278
+ gr.themes.GoogleFont(
2279
+ "JetBrains Mono"
2280
+ ),
2281
+ "monospace",
2282
+ ],
2283
  ),
2284
+
2285
  css="""
2286
+
2287
+ body,
2288
+ .gradio-container {
2289
  background: #060a10 !important;
2290
+ font-family:
2291
+ 'JetBrains Mono',
2292
+ monospace !important;
2293
+ max-width: 1400px !important;
2294
  }
2295
+
2296
  .gr-button-primary {
2297
+ background:
2298
+ linear-gradient(
2299
+ 90deg,
2300
+ #1a6bff,
2301
+ #0044cc
2302
+ ) !important;
2303
+
2304
  border: none !important;
2305
+
2306
  font-weight: 700 !important;
2307
+
2308
+ letter-spacing:
2309
+ 0.06em !important;
2310
  }
2311
+
2312
  #output_box textarea {
2313
+
2314
+ font-family:
2315
+ 'JetBrains Mono',
2316
+ monospace !important;
2317
+
2318
+ font-size:
2319
+ 12px !important;
2320
+
2321
+ line-height:
2322
+ 1.55 !important;
2323
+
2324
+ background:
2325
+ #0a0e18 !important;
2326
+
2327
+ color:
2328
+ #b0c4de !important;
2329
+
2330
+ border:
2331
+ 1px solid #182030 !important;
2332
+
2333
+ min-height:
2334
+ 800px !important;
2335
+ }
2336
+
2337
+ label {
2338
+
2339
+ color:
2340
+ #4a6080 !important;
2341
+
2342
+ font-size:
2343
+ 11px !important;
2344
+
2345
+ text-transform:
2346
+ uppercase !important;
2347
+
2348
+ letter-spacing:
2349
+ 0.09em !important;
2350
+ }
2351
+
2352
+ h1,
2353
+ h2 {
2354
+
2355
+ color:
2356
+ #c0d4f0 !important;
2357
+
2358
+ font-family:
2359
+ 'JetBrains Mono',
2360
+ monospace !important;
2361
+ }
2362
+
2363
+ p {
2364
+
2365
+ color:
2366
+ #384858 !important;
2367
+
2368
+ font-size:
2369
+ 12px !important;
2370
+ }
2371
+
2372
+ .gr-panel {
2373
+
2374
+ background:
2375
+ #0c1020 !important;
2376
+
2377
+ border:
2378
+ 1px solid #182030 !important;
2379
  }
2380
+
2381
+ """
2382
+
 
 
2383
  ) as app:
2384
+
2385
+ # ============================================================
2386
+ # HEADER
2387
+ # ============================================================
2388
+
2389
+ gr.Markdown(
2390
+ "# ◈ OKX QUANT ENGINE v4"
2391
+ )
2392
+
2393
  gr.Markdown(
2394
+ "ADX · absorption detection · "
2395
+ "volatility compression · "
2396
+ "fake breakout filter · "
2397
+ "ML probability · "
2398
+ "**Adaptive Meta-Decision Engine** · "
2399
+ "Monte Carlo robustness · "
2400
+ "adaptive risk"
2401
  )
2402
 
2403
+ # ============================================================
2404
+ # INPUTS
2405
+ # ============================================================
2406
+
2407
  with gr.Row():
2408
+
2409
+ with gr.Column(
2410
+ scale=2
2411
+ ):
2412
+
2413
  symbols_box = gr.Textbox(
2414
+
2415
+ label=
2416
+ "Symbols "
2417
+ "(comma / newline)",
2418
+
2419
+ placeholder=
2420
+ "BTC-USDT, ETH-USDT, SOL-USDT ...",
2421
+
2422
+ lines=4,
2423
+
2424
+ value="",
2425
  )
2426
+
2427
+ with gr.Column(
2428
+ scale=1
2429
+ ):
2430
+
2431
  equity_slider = gr.Slider(
2432
+
2433
+ label=
2434
+ "Account Equity ($)",
2435
+
2436
+ minimum=100,
2437
+
2438
+ maximum=1_000_000,
2439
+
2440
+ step=500,
2441
+
2442
+ value=
2443
+ DEFAULT_ACCOUNT_EQUITY,
2444
  )
2445
+
2446
  top_n_slider = gr.Slider(
2447
+
2448
+ label=
2449
+ "Top N to Display",
2450
+
2451
+ minimum=5,
2452
+
2453
+ maximum=100,
2454
+
2455
+ step=5,
2456
+
2457
+ value=
2458
+ TOP_N_DEFAULT,
2459
+ )
2460
+
2461
+ with gr.Column(
2462
+ scale=1
2463
+ ):
2464
+
2465
+ consec_loss = gr.Slider(
2466
+
2467
+ label=
2468
+ "Consecutive Losses",
2469
+
2470
+ minimum=0,
2471
+
2472
+ maximum=10,
2473
+
2474
+ step=1,
2475
+
2476
+ value=0,
2477
+ )
2478
+
2479
+ drawdown = gr.Slider(
2480
+
2481
+ label=
2482
+ "Drawdown from Peak (%)",
2483
+
2484
+ minimum=0.0,
2485
+
2486
+ maximum=30.0,
2487
+
2488
+ step=0.5,
2489
+
2490
+ value=0.0,
2491
+ )
2492
+
2493
+ live_check = gr.Checkbox(
2494
+
2495
+ label=
2496
+ "Fetch live OKX instruments",
2497
+
2498
+ value=False,
2499
  )
2500
+
2501
+ ml_check = gr.Checkbox(
2502
+
2503
+ label=(
2504
+ "Enable ML Filter "
2505
+ "(model: "
2506
+ f"{'LOADED' if _TRADE_FILTER else 'NOT TRAINED'})"
2507
+ ),
2508
+
2509
+ value=(
2510
+ _TRADE_FILTER
2511
+ is not None
2512
+ ),
2513
  )
2514
 
2515
+ # ============================================================
2516
+ # AMDE SETTINGS DISPLAY
2517
+ # ============================================================
2518
+
2519
+ gr.Markdown(
2520
+ f"""
2521
+ ### ⚡ Adaptive Meta Engine
2522
+
2523
+ **Meta threshold:** `{AMDE_CONFIG['meta_threshold']:.2f}`
2524
+ **Hard veto:** `{AMDE_CONFIG['hard_threshold']:.2f}`
2525
+ **Historical window:** `{AMDE_CONFIG['history_bars']}` candles
2526
+ **Monte Carlo:** `{AMDE_CONFIG['mc_iterations']:,}` simulations
2527
+ **Forward horizon:** `{AMDE_CONFIG['forward_bars']}` candles
2528
+
2529
+ The engine combines rule quality, ML probability,
2530
+ trend persistence, momentum, regime stability,
2531
+ signal freshness, historical directional edge,
2532
+ market noise and Monte Carlo robustness.
2533
+ """
2534
+ )
2535
+
2536
+ # ============================================================
2537
+ # BUTTON
2538
+ # ============================================================
2539
+
2540
+ run_btn = gr.Button(
2541
+ "▶ RUN ADAPTIVE ANALYSIS",
2542
+ variant="primary",
2543
+ size="lg",
2544
+ )
2545
+
2546
+ # ============================================================
2547
+ # OUTPUT
2548
+ # ============================================================
2549
+
2550
  output_box = gr.Textbox(
2551
+
2552
+ label=
2553
+ "Analysis Output",
2554
+
2555
+ lines=55,
2556
+
2557
+ max_lines=180,
2558
+
2559
  interactive=False,
2560
+
2561
+ elem_id=
2562
+ "output_box",
2563
  )
2564
 
2565
+ # ============================================================
2566
+ # ACTION
2567
+ # ============================================================
2568
+
2569
  run_btn.click(
2570
+
2571
  fn=run_analysis,
2572
+
2573
+ inputs=[
2574
+ symbols_box,
2575
+ equity_slider,
2576
+ consec_loss,
2577
+ drawdown,
2578
+ top_n_slider,
2579
+ live_check,
2580
+ ml_check,
2581
+ ],
2582
+
2583
+ outputs=
2584
+ output_box,
2585
  )
2586
 
2587
+ # ============================================================
2588
+ # FOOTER
2589
+ # ============================================================
2590
+
2591
  gr.Markdown(
2592
+ """
2593
+ **Research use only. Not financial advice.**
2594
+
2595
+ Train ML:
2596
+ `python train.py --use-defaults`
2597
+
2598
+ Optimize ML threshold:
2599
+ `python threshold_optimizer.py`
2600
+
2601
+ AMDE performs statistical robustness analysis;
2602
+ it does not guarantee future returns or a particular win rate.
2603
+ """
2604
  )
2605
 
2606
  return app
2607
 
2608
 
2609
+ # ============================================================================
2610
+ # MAIN
2611
+ # ============================================================================
2612
+
2613
  if __name__ == "__main__":
2614
+
2615
  import argparse
2616
+
2617
  parser = argparse.ArgumentParser()
2618
+
2619
+ parser.add_argument(
2620
+ "--port",
2621
+ type=int,
2622
+ default=7860,
2623
+ )
2624
+
2625
+ parser.add_argument(
2626
+ "--share",
2627
+ action="store_true",
2628
+ )
2629
+
2630
+ args = parser.parse_args()
2631
+
2632
+ build_app().launch(
2633
+
2634
+ server_name=
2635
+ "0.0.0.0",
2636
+
2637
+ server_port=
2638
+ args.port,
2639
+
2640
+ share=
2641
+ args.share,
2642
+
2643
+ show_error=True,
2644
+ )