GoshawkVortexAI commited on
Commit
9a1172d
Β·
verified Β·
1 Parent(s): bd01515

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +252 -172
app.py CHANGED
@@ -1,7 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import logging
2
  import sys
3
  import time
4
- from typing import List, Optional
5
 
6
  import gradio as gr
7
 
@@ -17,7 +29,7 @@ from regime import detect_regime
17
  from volume_analysis import analyze_volume
18
  from risk_engine import evaluate_risk
19
  from veto import apply_veto, veto_summary
20
- from scorer import compute_structure_score, score_token, rank_tokens, format_score_bar
21
 
22
  logging.basicConfig(
23
  level=logging.INFO,
@@ -26,16 +38,35 @@ logging.basicConfig(
26
  )
27
  logger = logging.getLogger("main")
28
 
29
- _TREND_EMOJI = {"bullish": "🟒", "ranging": "🟑", "bearish": "πŸ”΄"}
30
- _BREAKOUT_LABEL = {1: "↑ UP", -1: "↓ DOWN", 0: "β€”"}
 
 
31
 
 
 
 
 
 
 
32
 
33
- def analyze_single(symbol: str, df, account_equity: float) -> dict:
 
 
 
 
 
 
 
34
  regime_data = detect_regime(df)
35
- volume_data = analyze_volume(df)
 
36
  structure_score = compute_structure_score(regime_data)
37
- vetoed, veto_reason = apply_veto(regime_data, volume_data, structure_score)
 
 
38
  scores = score_token(regime_data, volume_data, vetoed)
 
39
  risk_data = evaluate_risk(
40
  close=float(df["close"].iloc[-1]),
41
  atr=regime_data["atr"],
@@ -43,300 +74,349 @@ def analyze_single(symbol: str, df, account_equity: float) -> dict:
43
  regime_score=regime_data["regime_score"],
44
  vol_ratio=regime_data["vol_ratio"],
45
  volume_score=volume_data["volume_score"],
 
 
 
 
46
  account_equity=account_equity,
47
  )
 
48
  return {
49
  "symbol": symbol,
50
  "close": float(df["close"].iloc[-1]),
51
  "trend": regime_data["trend"],
 
 
 
52
  "vol_ratio": regime_data["vol_ratio"],
 
 
53
  "vol_expanding": regime_data["vol_expanding"],
54
- "atr_pct": regime_data["atr_pct"],
 
 
55
  "spike": volume_data["spike"],
56
  "climax": volume_data["climax"],
 
 
 
57
  "breakout": volume_data["breakout"],
58
  "obv_slope": volume_data["obv_slope_norm"],
59
  "delta_sign": volume_data["delta_sign"],
 
60
  "vetoed": vetoed,
61
  "veto_reason": veto_reason,
62
  "regime_score": scores["regime_score"],
63
  "volume_score": scores["volume_score"],
64
  "structure_score": scores["structure_score"],
 
65
  "total_score": scores["total_score"],
66
  "risk": risk_data,
67
  }
68
 
69
 
70
- def build_summary_table(ranked: list, top_n: int) -> str:
71
- header = (
72
- f"{'#':>3} {'Symbol':<14} {'Score':>6} {'Regime':>7} "
73
- f"{'Volume':>7} {'Structure':>10} {'Trend':<8} "
74
- f"{'VolRatio':>8} {'Spike':>5} {'BOS':>5} {'Status'}\n"
 
 
75
  )
76
- separator = "─" * 110 + "\n"
77
- rows = header + separator
78
- for rank, (sym, data) in enumerate(ranked[:top_n], 1):
79
- trend_icon = _TREND_EMOJI.get(data["trend"], "βšͺ")
80
- breakout_lbl = _BREAKOUT_LABEL.get(data["breakout"], "β€”")
81
- spike_lbl = "βœ“" if data["spike"] else "βœ—"
82
- status = "VETOED" if data["vetoed"] else "OK"
 
 
83
  rows += (
84
- f"{rank:>3} {sym:<14} {data['total_score']:>6.4f} "
85
- f"{data['regime_score']:>7.4f} {data['volume_score']:>7.4f} "
86
- f"{data['structure_score']:>10.4f} "
87
- f"{trend_icon} {data['trend']:<6} "
88
- f"{data['vol_ratio']:>8.2f} {spike_lbl:>5} "
89
- f"{breakout_lbl:>5} {status}\n"
90
  )
91
  return rows
92
 
93
 
94
- def build_top_setup_detail(data: dict) -> str:
95
  r = data["risk"]
96
  sym = data["symbol"]
97
- trend_icon = _TREND_EMOJI.get(data["trend"], "βšͺ")
 
 
 
 
 
 
 
 
 
 
 
98
  lines = [
99
- "═" * 60,
100
- f" BEST SETUP: {sym}",
101
- "═" * 60,
102
- f" Trend: {trend_icon} {data['trend'].upper()}",
103
- f" Close Price: {r['entry_price']:.8f}",
 
 
 
 
104
  f"",
105
- f" ── SCORES ──────────────────────────────",
106
- f" Regime: {format_score_bar(data['regime_score'])}",
107
- f" Volume: {format_score_bar(data['volume_score'])}",
108
- f" Structure: {format_score_bar(data['structure_score'])}",
109
- f" Total: {format_score_bar(data['total_score'])}",
 
 
110
  f"",
111
- f" ── RISK PARAMETERS ──────────────────────",
112
- f" ATR: {r['atr']:.8f} ({r['atr_pct']:.3f}%)",
113
- f" Vol Ratio: {r['vol_ratio']:.2f}x (quality: {r['risk_quality']:.0%})",
114
- f" Risk Fraction: {r['risk_fraction']:.3f}%",
115
- f" $ At Risk: ${r['dollar_at_risk']:.2f}",
116
- f" Position Size: ${r['position_notional']:.2f} notional",
117
- f" Leverage (est): {r['leverage_implied']:.1f}x",
 
 
118
  f"",
119
- f" ── LONG SCENARIO ───────────────────────",
120
- f" Stop Loss: {r['stop_long']:.8f}",
121
- f" Take Profit: {r['target_long']:.8f}",
122
- f" R:R Ratio: 1 : {r['rr_ratio']:.1f}",
 
123
  f"",
124
- f" ── SHORT SCENARIO ──────────────────────",
125
- f" Stop Loss: {r['stop_short']:.8f}",
126
- f" Take Profit: {r['target_short']:.8f}",
127
- f" R:R Ratio: 1 : {r['rr_ratio']:.1f}",
128
  f"",
129
- f" Volume Spike: {'YES βœ“' if data['spike'] else 'NO βœ—'}",
130
- f" Volume Climax: {'YES ⚠' if data['climax'] else 'NO'}",
131
- f" Breakout: {_BREAKOUT_LABEL.get(data['breakout'], 'β€”')}",
132
- f" OBV Slope: {data['obv_slope']:+.4f}",
133
- f" Delta (5-bar): {'BUYING ↑' if data['delta_sign'] > 0 else 'SELLING ↓'}",
134
- "═" * 60,
 
 
135
  ]
136
  return "\n".join(lines)
137
 
138
 
139
- def parse_symbol_list(raw: str) -> List[str]:
140
- symbols = []
141
  for tok in raw.replace(",", " ").replace("\n", " ").split():
142
  tok = tok.strip().upper()
143
- if tok and "-" in tok:
144
- symbols.append(tok)
145
- elif tok:
146
- symbols.append(f"{tok}-USDT")
147
- return symbols if symbols else DEFAULT_SYMBOLS
148
 
149
 
150
  def run_analysis(
151
  symbols_input: str,
152
  equity: float,
 
 
153
  top_n: int,
154
- use_live_instruments: bool,
155
  progress=gr.Progress(track_tqdm=False),
156
  ) -> str:
157
- start_ts = time.time()
158
- output_lines = []
159
-
160
- output_lines.append("━" * 60)
161
- output_lines.append(" OKX QUANTITATIVE ANALYSIS ENGINE")
162
- output_lines.append("━" * 60)
163
-
164
- if use_live_instruments:
165
- output_lines.append("⟳ Fetching live instrument list from OKX...")
166
- symbols = fetch_instruments("SPOT")
167
- if not symbols:
168
- output_lines.append("⚠ Failed to fetch live instruments. Using defaults.")
169
- symbols = DEFAULT_SYMBOLS
170
- else:
171
- output_lines.append(f"βœ“ Found {len(symbols)} live USDT spot instruments")
172
  else:
173
- symbols = parse_symbol_list(symbols_input)
174
- output_lines.append(f"βœ“ Analyzing {len(symbols)} symbol(s)")
175
 
176
- output_lines.append(f" Timeframe: {TIMEFRAME} | Candles: {CANDLE_LIMIT} | Equity: ${equity:,.0f}")
177
- output_lines.append("")
 
 
 
178
 
179
- fetched_count = [0]
180
  total = len(symbols)
181
 
182
- def progress_cb(i, t, sym):
183
- fetched_count[0] = i
184
  progress(i / t, desc=f"Fetching {sym} ({i}/{t})")
185
 
186
- ohlcv_map = fetch_multiple(
187
- symbols,
188
- min_bars=50,
189
- progress_callback=progress_cb,
190
- )
191
-
192
- output_lines.append(f"βœ“ Fetched {len(ohlcv_map)}/{total} symbols successfully")
193
- output_lines.append("")
194
 
195
- all_results = {}
196
- failed = []
197
 
198
  for sym, df in ohlcv_map.items():
199
  try:
200
- all_results[sym] = analyze_single(sym, df, account_equity=equity)
 
 
 
 
 
201
  except Exception as exc:
202
- logger.error(f"Analysis error for {sym}: {exc}", exc_info=True)
203
- failed.append(sym)
204
 
205
- if failed:
206
- output_lines.append(f"⚠ Analysis failed for: {', '.join(failed)}")
207
- output_lines.append("")
208
 
209
  ranked = rank_tokens(all_results)
210
-
211
  approved = [(s, d) for s, d in ranked if not d["vetoed"]]
212
- vetoed_count = sum(1 for _, d in ranked if d["vetoed"])
213
 
214
- output_lines.append(f" RESULTS: {len(all_results)} analyzed | {len(approved)} approved | {vetoed_count} vetoed")
215
- output_lines.append("")
216
- output_lines.append(" TOP SETUPS RANKED BY TOTAL SCORE")
217
- output_lines.append("─" * 110)
218
- output_lines.append(build_summary_table(ranked, int(top_n)))
 
 
 
219
 
220
  if approved:
221
  best_sym, best_data = approved[0]
222
- output_lines.append("")
223
- output_lines.append(build_top_setup_detail(best_data))
224
  else:
225
- output_lines.append("")
226
- output_lines.append(" ⚠ No approved setups found β€” all tokens vetoed.")
227
-
228
- elapsed = time.time() - start_ts
229
- output_lines.append("")
230
- output_lines.append(f" βœ“ Analysis complete in {elapsed:.1f}s")
231
- output_lines.append("━" * 60)
232
-
233
- return "\n".join(output_lines)
234
 
235
 
236
- def build_interface() -> gr.Blocks:
237
  with gr.Blocks(
238
- title="OKX Quant Analysis Engine",
239
  theme=gr.themes.Base(
240
  primary_hue="slate",
241
- neutral_hue="slate",
242
  font=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
243
  ),
244
  css="""
245
- body { background: #0a0a0f; }
246
- .gradio-container {
247
- background: #0a0a0f !important;
248
- max-width: 1100px !important;
249
  font-family: 'JetBrains Mono', monospace !important;
 
250
  }
251
  .gr-button-primary {
252
- background: #1a6bff !important;
253
  border: none !important;
254
- font-family: 'JetBrains Mono', monospace !important;
255
  font-weight: 700 !important;
256
- letter-spacing: 0.05em !important;
 
257
  }
258
- .gr-button-primary:hover { background: #0050e0 !important; }
259
  #output_box textarea {
260
  font-family: 'JetBrains Mono', monospace !important;
261
- font-size: 13px !important;
262
- background: #0f0f1a !important;
263
- color: #c8d0e0 !important;
264
- border: 1px solid #1e2236 !important;
265
- min-height: 700px !important;
 
266
  }
267
- label, .gr-form label {
268
  font-family: 'JetBrains Mono', monospace !important;
269
- color: #8899bb !important;
270
  font-size: 11px !important;
271
- letter-spacing: 0.08em !important;
272
  text-transform: uppercase !important;
273
  }
274
- .gr-panel { background: #0d0d18 !important; border: 1px solid #1e2236 !important; }
275
- h1 { color: #e0e8ff !important; font-family: 'JetBrains Mono', monospace !important; letter-spacing: 0.05em !important; }
276
- p { color: #5a6a8a !important; font-family: 'JetBrains Mono', monospace !important; font-size: 12px !important; }
277
  """,
278
  ) as app:
279
-
280
- gr.Markdown("# β—ˆ OKX QUANT ANALYSIS ENGINE")
281
- gr.Markdown("Multi-token regime detection Β· volume analysis Β· dynamic risk Β· veto scoring")
 
 
282
 
283
  with gr.Row():
284
  with gr.Column(scale=2):
285
  symbols_box = gr.Textbox(
286
- label="Symbols (comma or newline separated β€” leave blank for defaults)",
287
  placeholder="BTC-USDT, ETH-USDT, SOL-USDT ...",
288
  lines=4,
289
  value="",
290
  )
291
  with gr.Column(scale=1):
292
  equity_slider = gr.Slider(
293
- label="Account Equity (USD)",
294
- minimum=100,
295
- maximum=1_000_000,
296
- step=100,
297
  value=DEFAULT_ACCOUNT_EQUITY,
298
  )
299
  top_n_slider = gr.Slider(
300
- label="Top N Results to Show",
301
- minimum=5,
302
- maximum=100,
303
- step=5,
304
  value=TOP_N_DEFAULT,
305
  )
306
- live_instruments = gr.Checkbox(
307
- label="Fetch live instruments from OKX (100+ symbols)",
 
 
 
 
 
 
 
 
 
308
  value=False,
309
  )
310
 
311
  run_btn = gr.Button("β–Ά RUN ANALYSIS", variant="primary", size="lg")
312
-
313
  output_box = gr.Textbox(
314
  label="Analysis Output",
315
- lines=40,
316
- max_lines=100,
317
  interactive=False,
318
  elem_id="output_box",
319
  )
320
 
321
  run_btn.click(
322
  fn=run_analysis,
323
- inputs=[symbols_box, equity_slider, top_n_slider, live_instruments],
 
 
 
324
  outputs=output_box,
325
  )
326
 
327
  gr.Markdown(
328
- "**Signals are for research purposes only. Not financial advice.** "
329
- "Data sourced from OKX public REST API."
330
  )
331
 
332
  return app
333
 
334
 
335
  if __name__ == "__main__":
336
- app = build_interface()
337
- app.launch(
338
- server_name="0.0.0.0",
339
- server_port=7860,
340
- show_error=True,
341
- share=False,
342
- )
 
 
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
14
  import sys
15
  import time
16
+ from typing import List, Optional, Dict, Any
17
 
18
  import gradio as gr
19
 
 
29
  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
  )
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:
47
+ if trend == "bullish" or breakout == 1:
48
+ return 1
49
+ if trend == "bearish" or breakout == -1:
50
+ return -1
51
+ return 0
52
 
53
+
54
+ def analyze_single(
55
+ symbol: str,
56
+ df,
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"],
 
74
  regime_score=regime_data["regime_score"],
75
  vol_ratio=regime_data["vol_ratio"],
76
  volume_score=volume_data["volume_score"],
77
+ regime_confidence=regime_data["regime_confidence"],
78
+ vol_compressed=regime_data["vol_compressed"],
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
 
211
+ 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(
222
  symbols_input: str,
223
  equity: float,
224
+ consec_losses: int,
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...")
240
+ symbols = fetch_instruments("SPOT") or DEFAULT_SYMBOLS
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
 
 
252
  total = len(symbols)
253
 
254
+ def prog_cb(i, t, sym):
 
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] = {}
262
+ errors = []
263
 
264
  for sym, df in ohlcv_map.items():
265
  try:
266
+ all_results[sym] = analyze_single(
267
+ sym, df,
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",
314
  font=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
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(
366
+ label="Account Equity ($)",
367
+ minimum=100, maximum=1_000_000, step=500,
 
 
368
  value=DEFAULT_ACCOUNT_EQUITY,
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
412
 
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)