GoshawkVortexAI commited on
Commit
57df227
·
verified ·
1 Parent(s): f89e879

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +247 -188
app.py CHANGED
@@ -1,13 +1,25 @@
1
  """
2
- main.py — Gradio interface orchestrating the full analysis pipeline.
3
-
4
- Changes vs prior version:
5
- - regime_data["atr_series"] passed into analyze_volume for consistent ATR
6
- - Consecutive loss state tracked per-session via gr.State
7
- - Equity drawdown guard plumbed into risk_engine
8
- - Direction inference: long only when bullish/ranging, short when bearish
9
- - Output shows absorption, failed breakout, ADX, compression state
10
- - Quality tier displayed alongside scores
 
 
 
 
 
 
 
 
 
 
 
 
11
  """
12
 
13
  import logging
@@ -30,6 +42,8 @@ from volume_analysis import analyze_volume
30
  from risk_engine import evaluate_risk
31
  from veto import apply_veto, veto_summary
32
  from scorer import compute_structure_score, score_token, rank_tokens, format_score_bar, quality_tier
 
 
33
 
34
  logging.basicConfig(
35
  level=logging.INFO,
@@ -38,9 +52,12 @@ logging.basicConfig(
38
  )
39
  logger = logging.getLogger("main")
40
 
41
- _TREND_ICON = {"bullish": "▲", "ranging": "◆", "bearish": "▼"}
42
- _BREAKOUT_LABEL = {1: "↑ UP", -1: "↓ DN", 0: " — "}
43
- _DIR_LABEL = {1: "LONG", -1: "SHORT", 0: "NONE"}
 
 
 
44
 
45
 
46
  def infer_direction(trend: str, breakout: int) -> int:
@@ -57,16 +74,42 @@ def analyze_single(
57
  account_equity: float,
58
  consec_losses: int = 0,
59
  equity_drawdown_pct: float = 0.0,
 
60
  ) -> Dict[str, Any]:
61
- regime_data = detect_regime(df)
62
- # Pass consistent ATR series from regime into volume analysis
63
- volume_data = analyze_volume(df, atr_series=regime_data["atr_series"])
64
- structure_score = compute_structure_score(regime_data)
65
-
66
- direction = infer_direction(regime_data["trend"], volume_data["breakout"])
67
- vetoed, veto_reason = apply_veto(regime_data, volume_data, structure_score, direction=direction)
68
  scores = score_token(regime_data, volume_data, vetoed)
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  risk_data = evaluate_risk(
71
  close=float(df["close"].iloc[-1]),
72
  atr=regime_data["atr"],
@@ -79,132 +122,151 @@ def analyze_single(
79
  consec_losses=consec_losses,
80
  equity_drawdown_pct=equity_drawdown_pct,
81
  account_equity=account_equity,
82
- )
83
 
84
  return {
85
- "symbol": symbol,
86
- "close": float(df["close"].iloc[-1]),
87
- "trend": regime_data["trend"],
88
- "adx": regime_data["adx"],
89
- "di_plus": regime_data["di_plus"],
90
- "di_minus": regime_data["di_minus"],
91
- "vol_ratio": regime_data["vol_ratio"],
92
- "vol_compressed": regime_data["vol_compressed"],
93
  "vol_expanding_from_base": regime_data["vol_expanding_from_base"],
94
- "vol_expanding": regime_data["vol_expanding"],
95
- "dist_atr": regime_data["dist_atr"],
96
- "price_extended": regime_data["price_extended_long"] or regime_data["price_extended_short"],
97
- "regime_confidence": regime_data["regime_confidence"],
98
- "spike": volume_data["spike"],
99
- "climax": volume_data["climax"],
100
- "absorption": volume_data["absorption"],
101
- "failed_breakout": volume_data["failed_breakout"],
102
- "recent_failed": volume_data["recent_failed_count"],
103
- "breakout": volume_data["breakout"],
104
- "obv_slope": volume_data["obv_slope_norm"],
105
- "delta_sign": volume_data["delta_sign"],
106
- "direction": direction,
107
- "vetoed": vetoed,
108
- "veto_reason": veto_reason,
109
- "regime_score": scores["regime_score"],
110
- "volume_score": scores["volume_score"],
111
- "structure_score": scores["structure_score"],
112
- "confidence_score": scores["confidence_score"],
113
- "total_score": scores["total_score"],
114
- "risk": risk_data,
 
 
 
 
115
  }
116
 
117
 
 
 
 
 
 
 
 
 
 
 
 
118
  def build_ranked_table(ranked: list, top_n: int) -> str:
119
- col_w = 110
120
  hdr = (
121
  f"{'#':>3} {'Symbol':<14} {'Score':>7} {'Tier':>4} "
122
- f"{'Regime':>6} {'Vol':>6} {'Struct':>6} {'Conf':>6} "
123
- f"{'Trend':>7} {'ADX':>5} {'VRatio':>6} "
124
- f"{'Absorb':>6} {'FakBO':>5} {'Status'}\n"
125
  )
126
- sep = "─" * col_w + "\n"
127
  rows = hdr + sep
128
 
129
  for rank, (sym, d) in enumerate(ranked[:top_n], 1):
130
- icon = _TREND_ICON.get(d["trend"], "?")
131
- tier = quality_tier(d["total_score"])
132
- absorb = "YES⚠" if d.get("absorption") else " no"
133
- fakebo = f"{d.get('recent_failed', 0)}x" if d.get("recent_failed", 0) > 0 else " —"
134
- status = "VETOED" if d["vetoed"] else "OK "
 
 
 
 
 
 
135
  rows += (
136
  f"{rank:>3} {sym:<14} {d['total_score']:>7.4f} {tier:>4} "
137
  f"{d['regime_score']:>6.3f} {d['volume_score']:>6.3f} "
138
- f"{d['structure_score']:>6.3f} {d['confidence_score']:>6.3f} "
139
- f"{icon} {d['trend']:<5} {d['adx']:>5.1f} {d['vol_ratio']:>6.2f} "
140
- f"{absorb:>6} {fakebo:>5} {status}\n"
141
  )
142
  return rows
143
 
144
 
145
  def build_best_detail(data: Dict[str, Any]) -> str:
146
- r = data["risk"]
147
  sym = data["symbol"]
148
  icon = _TREND_ICON.get(data["trend"], "?")
149
- dir_lbl = _DIR_LABEL.get(data["direction"], "?")
150
 
151
  vol_state = []
152
- if data["vol_compressed"]:
153
- vol_state.append("COMPRESSED")
154
- if data["vol_expanding_from_base"]:
155
- vol_state.append("EXPANDING FROM BASE ✓")
156
  if data["vol_expanding"] and not data["vol_expanding_from_base"]:
157
  vol_state.append("EXPANDING (no base)")
158
- vol_state_str = " | ".join(vol_state) if vol_state else "NORMAL"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  lines = [
161
  "═" * 64,
162
- f" BEST APPROVED SETUP: {sym} [{dir_lbl}]",
163
  "═" * 64,
164
  f" Trend: {icon} {data['trend'].upper()}",
165
  f" ADX: {data['adx']:.1f} (DI+ {data['di_plus']:.1f} / DI- {data['di_minus']:.1f})",
166
  f" Vol State: {vol_state_str}",
167
- f" Vol Ratio: {data['vol_ratio']:.2f}x",
168
  f" Dist from Mean: {data['dist_atr']:.2f} ATR",
169
  f" Regime Confidence:{data['regime_confidence']:.3f}",
170
- f"",
171
- f" ── SCORES ───────────────────────────────────────",
172
  f" Regime: {format_score_bar(data['regime_score'])}",
173
  f" Volume: {format_score_bar(data['volume_score'])}",
174
  f" Structure: {format_score_bar(data['structure_score'])}",
175
  f" Confidence: {format_score_bar(data['confidence_score'])}",
176
- f" ─────────────────────────────────────────────────",
177
  f" TOTAL: {format_score_bar(data['total_score'])}",
178
- f"",
179
- f" ── VOLUME FLAGS ─────────────────────────────────",
180
- f" Spike: {'YES ✓' if data['spike'] else 'no'}",
181
- f" Climax: {'YES ⚠' if data['climax'] else 'no'}",
182
- f" Absorption: {'YES ⚠ SELL PRESSURE' if data['absorption'] else 'no'}",
183
- f" Failed Breakout: {'YES ⚠' if data['failed_breakout'] else 'no'}",
184
- f" Recent Fakes: {data['recent_failed']}",
185
- f" Breakout Dir: {_BREAKOUT_LABEL.get(data['breakout'], '—')}",
186
- f" OBV Slope: {data['obv_slope']:+.4f}",
187
- f" Delta (5-bar): {'BUYING ↑' if data['delta_sign'] > 0 else 'SELLING ↓'}",
188
- f"",
189
- f" ── RISK PARAMETERS ─────────────────────────────",
190
- f" Entry: {r['entry_price']:.8f}",
191
- f" ATR: {r['atr']:.8f} ({r['atr_pct']:.3f}%)",
192
- f" Stop Multiplier: {r['stop_mult']:.1f}x ATR (adaptive)",
193
- f" Stop Distance: {r['stop_distance']:.8f}",
194
- f"",
195
- f" LONG → Stop: {r['stop_long']:.8f} Target: {r['target_long']:.8f}",
196
- f" SHORT → Stop: {r['stop_short']:.8f} Target: {r['target_short']:.8f}",
197
- f" R:R Ratio: 1 : {r['rr_ratio']:.1f}",
198
- f"",
199
- f" Risk Fraction: {r['risk_fraction']:.4f}%",
200
- f" $ At Risk: ${r['dollar_at_risk']:.2f}",
201
- f" Position Size: ${r['position_notional']:.2f} notional",
202
- f" Leverage (est): {r['leverage_implied']:.2f}x",
203
- f" Risk Quality: {r['risk_quality']:.0%}",
204
- f" Consec. Losses: {r['consec_losses']}",
205
- f" Drawdown Guard: {'HALTED ⛔' if r['sizing_halted'] else 'active'}",
206
- "═" * 64,
207
  ]
 
 
 
208
  return "\n".join(lines)
209
 
210
 
@@ -212,10 +274,9 @@ def parse_symbols(raw: str) -> List[str]:
212
  out = []
213
  for tok in raw.replace(",", " ").replace("\n", " ").split():
214
  tok = tok.strip().upper()
215
- if not tok:
216
- continue
217
- out.append(tok if "-" in tok else f"{tok}-USDT")
218
- return out if out else DEFAULT_SYMBOLS
219
 
220
 
221
  def run_analysis(
@@ -225,15 +286,25 @@ def run_analysis(
225
  drawdown_pct: float,
226
  top_n: int,
227
  use_live: bool,
 
228
  progress=gr.Progress(track_tqdm=False),
229
  ) -> str:
230
  t0 = time.time()
231
  lines = []
232
 
233
- lines.append("" * 66)
234
- lines.append(" OKX QUANTITATIVE ANALYSIS ENGINE v2")
235
- lines.append(" (with absorption, compression & fake-breakout filters)")
236
- lines.append("━" * 66)
 
 
 
 
 
 
 
 
 
237
 
238
  if use_live:
239
  lines.append("⟳ Fetching live OKX instrument list...")
@@ -241,11 +312,11 @@ def run_analysis(
241
  lines.append(f"✓ {len(symbols)} live USDT instruments")
242
  else:
243
  symbols = parse_symbols(symbols_input)
244
- lines.append(f"✓ {len(symbols)} symbol(s) to analyze")
245
 
246
  lines.append(
247
- f" Equity: ${equity:,.0f} | Consec Losses: {int(consec_losses)}"
248
- f" | Drawdown: {drawdown_pct:.1f}% | TF: {TIMEFRAME}"
249
  )
250
  lines.append("")
251
 
@@ -255,7 +326,7 @@ def run_analysis(
255
  progress(i / t, desc=f"Fetching {sym} ({i}/{t})")
256
 
257
  ohlcv_map = fetch_multiple(symbols, min_bars=50, progress_callback=prog_cb)
258
- lines.append(f"✓ Fetched {len(ohlcv_map)}/{total} | Analyzing...")
259
  lines.append("")
260
 
261
  all_results: Dict[str, Any] = {}
@@ -268,46 +339,52 @@ def run_analysis(
268
  account_equity=equity,
269
  consec_losses=int(consec_losses),
270
  equity_drawdown_pct=drawdown_pct / 100.0,
 
271
  )
272
  except Exception as exc:
273
- logger.error(f"{sym} analysis failed: {exc}", exc_info=True)
274
  errors.append(sym)
275
 
276
  if errors:
277
- lines.append(f"⚠ Failed: {', '.join(errors)}")
278
 
279
  ranked = rank_tokens(all_results)
280
- approved = [(s, d) for s, d in ranked if not d["vetoed"]]
281
- vetoed_n = len(ranked) - len(approved)
282
 
283
- lines.append(
284
- f" RESULTS: {len(all_results)} analyzed | "
285
- f"{len(approved)} approved | {vetoed_n} vetoed"
286
- )
287
- lines.append("")
288
- lines.append(" RANKED SETUPS")
289
- lines.append("─" * 110)
290
- lines.append(build_ranked_table(ranked, int(top_n)))
291
-
292
- if approved:
293
- best_sym, best_data = approved[0]
294
- lines.append("")
295
- lines.append(build_best_detail(best_data))
 
 
 
 
296
  else:
297
- lines.append("")
298
- lines.append(" ⚠ No approved setups — all tokens vetoed.")
299
- lines.append(" Consider: checking market regime, reducing symbol list,")
300
- lines.append(" or verifying OKX API connectivity.")
 
 
301
 
302
- lines.append("")
303
- lines.append(f" ✓ Complete in {time.time() - t0:.1f}s")
304
- lines.append("━" * 66)
 
305
  return "\n".join(lines)
306
 
307
 
308
  def build_app() -> gr.Blocks:
309
  with gr.Blocks(
310
- title="OKX Quant Engine v2",
311
  theme=gr.themes.Base(
312
  primary_hue="slate",
313
  neutral_hue="zinc",
@@ -315,51 +392,43 @@ def build_app() -> gr.Blocks:
315
  ),
316
  css="""
317
  body, .gradio-container {
318
- background: #080c12 !important;
319
  font-family: 'JetBrains Mono', monospace !important;
320
- max-width: 1200px !important;
321
  }
322
  .gr-button-primary {
323
  background: linear-gradient(90deg, #1a6bff, #0044cc) !important;
324
  border: none !important;
325
  font-weight: 700 !important;
326
  letter-spacing: 0.06em !important;
327
- text-transform: uppercase !important;
328
  }
329
  #output_box textarea {
330
  font-family: 'JetBrains Mono', monospace !important;
331
- font-size: 12.5px !important;
332
  line-height: 1.55 !important;
333
- background: #0b0f18 !important;
334
- color: #b8c8e0 !important;
335
- border: 1px solid #1a2238 !important;
336
- min-height: 720px !important;
337
- }
338
- label, .label-wrap {
339
- font-family: 'JetBrains Mono', monospace !important;
340
- color: #5a7090 !important;
341
- font-size: 11px !important;
342
- letter-spacing: 0.09em !important;
343
- text-transform: uppercase !important;
344
  }
345
- h1, h2, h3 { color: #c8daf0 !important; font-family: 'JetBrains Mono', monospace !important; }
346
- p { color: #445566 !important; font-size: 12px !important; }
347
- .gr-panel, .gr-box { background: #0d1220 !important; border: 1px solid #1a2238 !important; }
 
348
  """,
349
  ) as app:
350
- gr.Markdown("# ◈ OKX QUANT ENGINE v2")
351
  gr.Markdown(
352
- "ADX regime · absorption detection · volatility compression filter"
353
- " · fake breakout identification · adaptive risk scaling"
354
  )
355
 
356
  with gr.Row():
357
  with gr.Column(scale=2):
358
  symbols_box = gr.Textbox(
359
- label="Symbols (comma / newline — blank = 100 defaults)",
360
  placeholder="BTC-USDT, ETH-USDT, SOL-USDT ...",
361
- lines=4,
362
- value="",
363
  )
364
  with gr.Column(scale=1):
365
  equity_slider = gr.Slider(
@@ -369,43 +438,35 @@ def build_app() -> gr.Blocks:
369
  )
370
  top_n_slider = gr.Slider(
371
  label="Top N to Display",
372
- minimum=5, maximum=100, step=5,
373
- value=TOP_N_DEFAULT,
374
  )
375
  with gr.Column(scale=1):
376
- consec_loss_input = gr.Slider(
377
- label="Consecutive Losses (current streak)",
378
- minimum=0, maximum=10, step=1, value=0,
379
- )
380
- drawdown_input = gr.Slider(
381
- label="Current Drawdown from Peak (%)",
382
- minimum=0.0, maximum=30.0, step=0.5, value=0.0,
383
- )
384
- live_check = gr.Checkbox(
385
- label="Fetch live instruments from OKX (100+ symbols)",
386
- value=False,
387
  )
388
 
389
  run_btn = gr.Button("▶ RUN ANALYSIS", variant="primary", size="lg")
390
  output_box = gr.Textbox(
391
  label="Analysis Output",
392
- lines=45, max_lines=120,
393
  interactive=False,
394
  elem_id="output_box",
395
  )
396
 
397
  run_btn.click(
398
  fn=run_analysis,
399
- inputs=[
400
- symbols_box, equity_slider, consec_loss_input,
401
- drawdown_input, top_n_slider, live_check,
402
- ],
403
  outputs=output_box,
404
  )
405
 
406
  gr.Markdown(
407
  "**Research use only. Not financial advice.** "
408
- "All signals are probabilistic no system eliminates risk."
 
409
  )
410
 
411
  return app
@@ -413,10 +474,8 @@ def build_app() -> gr.Blocks:
413
 
414
  if __name__ == "__main__":
415
  import argparse
416
- parser = argparse.ArgumentParser(description="OKX Quant Engine v2")
417
  parser.add_argument("--port", type=int, default=7860)
418
  parser.add_argument("--share", action="store_true")
419
- args = parser.parse_args()
420
-
421
- app = build_app()
422
- app.launch(server_name="0.0.0.0", server_port=args.port, share=args.share, show_error=True)
 
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
 
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,
 
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:
 
74
  account_equity: float,
75
  consec_losses: int = 0,
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"],
 
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
 
 
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(
 
286
  drawdown_pct: float,
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...")
 
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
 
 
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] = {}
 
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",
 
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(
 
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
 
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)