GoshawkVortexAI commited on
Commit
c653f88
·
verified ·
1 Parent(s): 8d2c98d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +286 -104
app.py CHANGED
@@ -1,54 +1,62 @@
1
  import logging
2
  import sys
3
- import json
4
- from typing import Optional, List
5
 
6
- import pandas as pd
7
 
8
- from config import DEFAULT_SYMBOLS, TOP_N_TOKENS, TIMEFRAME, CANDLE_LIMIT
 
 
 
 
 
 
9
  from data_fetcher import fetch_multiple, fetch_instruments
10
  from regime import detect_regime
11
  from volume_analysis import analyze_volume
12
  from risk_engine import evaluate_risk
13
- from veto import apply_veto
14
- from scorer import compute_structure_score, score_token, rank_tokens
15
 
16
  logging.basicConfig(
17
  level=logging.INFO,
18
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
19
- handlers=[logging.StreamHandler(sys.stdout)],
20
  )
21
  logger = logging.getLogger("main")
22
 
 
 
 
23
 
24
- def analyze_symbol(symbol: str, df: pd.DataFrame, account_equity: float = 10000.0):
25
  regime_data = detect_regime(df)
26
  volume_data = analyze_volume(df)
27
-
28
  structure_score = compute_structure_score(regime_data)
29
-
30
  vetoed, veto_reason = apply_veto(regime_data, volume_data, structure_score)
31
-
32
  scores = score_token(regime_data, volume_data, vetoed)
33
-
34
  risk_data = evaluate_risk(
35
- df_last_close=df["close"].iloc[-1],
36
  atr=regime_data["atr"],
37
  atr_pct=regime_data["atr_pct"],
38
  regime_score=regime_data["regime_score"],
39
  vol_ratio=regime_data["vol_ratio"],
 
40
  account_equity=account_equity,
41
  )
42
-
43
  return {
44
  "symbol": symbol,
45
- "close": round(df["close"].iloc[-1], 8),
46
  "trend": regime_data["trend"],
47
- "vol_ratio": round(regime_data["vol_ratio"], 3),
48
- "volatility_expanding": regime_data["volatility_expanding"],
49
- "volume_spike": volume_data["spike"],
50
- "volume_climax": volume_data["climax"],
 
51
  "breakout": volume_data["breakout"],
 
 
52
  "vetoed": vetoed,
53
  "veto_reason": veto_reason,
54
  "regime_score": scores["regime_score"],
@@ -59,102 +67,276 @@ def analyze_symbol(symbol: str, df: pd.DataFrame, account_equity: float = 10000.
59
  }
60
 
61
 
62
- def run(
63
- symbols: Optional[List[str]] = None,
64
- account_equity: float = 10000.0,
65
- fetch_live_instruments: bool = False,
66
- max_symbols: Optional[int] = None,
67
- top_n: int = TOP_N_TOKENS,
68
- ):
69
- if fetch_live_instruments:
70
- logger.info("Fetching live instrument list from OKX...")
71
- live_symbols = fetch_instruments("SPOT")
72
- if live_symbols:
73
- symbols = live_symbols
74
- logger.info(f"Found {len(symbols)} live USDT spot instruments")
75
- else:
76
- logger.warning("Failed to fetch instruments, using defaults")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  symbols = DEFAULT_SYMBOLS
78
- elif symbols is None:
79
- symbols = DEFAULT_SYMBOLS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- logger.info(f"Fetching OHLCV data for {len(symbols)} symbols...")
82
- ohlcv_map = fetch_multiple(symbols, timeframe=TIMEFRAME, limit=CANDLE_LIMIT, max_symbols=max_symbols)
83
- logger.info(f"Successfully fetched {len(ohlcv_map)} symbols")
 
 
84
 
85
- results = {}
86
  for sym, df in ohlcv_map.items():
87
  try:
88
- result = analyze_symbol(sym, df, account_equity=account_equity)
89
- results[sym] = result
90
- except Exception as e:
91
- logger.error(f"Error analyzing {sym}: {e}", exc_info=True)
92
 
93
- ranked = rank_tokens({sym: r for sym, r in results.items()})
 
 
94
 
95
- logger.info("\n" + "=" * 80)
96
- logger.info(f"TOP {top_n} SETUPS RANKED BY TOTAL SCORE")
97
- logger.info("=" * 80)
98
 
99
- top_results = []
100
- for rank, (sym, data) in enumerate(ranked[:top_n], 1):
101
- veto_str = f" [VETOED: {data['veto_reason']}]" if data["vetoed"] else ""
102
- logger.info(
103
- f"#{rank:>3} {sym:<15} "
104
- f"Score={data['total_score']:.4f} "
105
- f"(R={data['regime_score']:.2f} V={data['volume_score']:.2f} S={data['structure_score']:.2f}) "
106
- f"Trend={data['trend']:<8} "
107
- f"VolRatio={data['vol_ratio']:.2f} "
108
- f"Spike={data['volume_spike']} "
109
- f"Breakout={data['breakout']:>2}"
110
- f"{veto_str}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  )
112
- top_results.append(data)
113
-
114
- logger.info("=" * 80)
115
-
116
- non_vetoed = [(s, d) for s, d in ranked if not d["vetoed"]]
117
- if non_vetoed:
118
- best_sym, best_data = non_vetoed[0]
119
- logger.info(f"\nBEST SETUP: {best_sym}")
120
- risk = best_data["risk"]
121
- logger.info(f" Entry: {risk['entry_price']:.8f}")
122
- logger.info(f" Stop (Long): {risk['stop_price_long']:.8f}")
123
- logger.info(f" Target (Long): {risk['target_long']:.8f}")
124
- logger.info(f" ATR: {risk['atr']:.8f} ({risk['atr_pct']*100:.2f}%)")
125
- logger.info(f" Risk Fraction: {risk['risk_fraction']*100:.2f}%")
126
- logger.info(f" Position Size: ${risk['position_notional']:.2f} notional")
127
 
128
- return {
129
- "all_results": results,
130
- "ranked": ranked,
131
- "top_n": top_results,
132
- }
 
 
 
 
 
 
 
133
 
134
 
135
  if __name__ == "__main__":
136
- import argparse
137
-
138
- parser = argparse.ArgumentParser(description="OKX Trading Analysis Engine")
139
- parser.add_argument("--equity", type=float, default=10000.0, help="Account equity in USD")
140
- parser.add_argument("--top", type=int, default=TOP_N_TOKENS, help="Number of top setups to display")
141
- parser.add_argument("--max-symbols", type=int, default=None, help="Limit number of symbols analyzed")
142
- parser.add_argument("--live-instruments", action="store_true", help="Fetch live instrument list from OKX")
143
- parser.add_argument("--output", type=str, default=None, help="Save results to JSON file")
144
- args = parser.parse_args()
145
-
146
- output = run(
147
- account_equity=args.equity,
148
- fetch_live_instruments=args.live_instruments,
149
- max_symbols=args.max_symbols,
150
- top_n=args.top,
151
  )
152
-
153
- if args.output:
154
- serializable = {
155
- sym: {k: v for k, v in data.items() if k != "risk"}
156
- for sym, data in output["all_results"].items()
157
- }
158
- with open(args.output, "w") as f:
159
- json.dump(serializable, f, indent=2, default=str)
160
- logger.info(f"Results saved to {args.output}")
 
1
  import logging
2
  import sys
3
+ import time
4
+ from typing import List, Optional
5
 
6
+ import gradio as gr
7
 
8
+ from config import (
9
+ DEFAULT_SYMBOLS,
10
+ TOP_N_DEFAULT,
11
+ DEFAULT_ACCOUNT_EQUITY,
12
+ TIMEFRAME,
13
+ CANDLE_LIMIT,
14
+ )
15
  from data_fetcher import fetch_multiple, fetch_instruments
16
  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,
24
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
25
+ stream=sys.stdout,
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"],
42
  atr_pct=regime_data["atr_pct"],
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"],
 
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
  )