Spaces:
Sleeping
Sleeping
| import logging | |
| import sys | |
| import time | |
| from typing import List, Optional | |
| import gradio as gr | |
| from config import ( | |
| DEFAULT_SYMBOLS, | |
| TOP_N_DEFAULT, | |
| DEFAULT_ACCOUNT_EQUITY, | |
| TIMEFRAME, | |
| CANDLE_LIMIT, | |
| ) | |
| from data_fetcher import fetch_multiple, fetch_instruments | |
| from regime import detect_regime | |
| from volume_analysis import analyze_volume | |
| from risk_engine import evaluate_risk | |
| from veto import apply_veto, veto_summary | |
| from scorer import compute_structure_score, score_token, rank_tokens, format_score_bar | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | |
| stream=sys.stdout, | |
| ) | |
| logger = logging.getLogger("main") | |
| _TREND_EMOJI = {"bullish": "π’", "ranging": "π‘", "bearish": "π΄"} | |
| _BREAKOUT_LABEL = {1: "β UP", -1: "β DOWN", 0: "β"} | |
| def analyze_single(symbol: str, df, account_equity: float) -> dict: | |
| regime_data = detect_regime(df) | |
| volume_data = analyze_volume(df) | |
| structure_score = compute_structure_score(regime_data) | |
| vetoed, veto_reason = apply_veto(regime_data, volume_data, structure_score) | |
| scores = score_token(regime_data, volume_data, vetoed) | |
| risk_data = evaluate_risk( | |
| close=float(df["close"].iloc[-1]), | |
| atr=regime_data["atr"], | |
| atr_pct=regime_data["atr_pct"], | |
| regime_score=regime_data["regime_score"], | |
| vol_ratio=regime_data["vol_ratio"], | |
| volume_score=volume_data["volume_score"], | |
| account_equity=account_equity, | |
| ) | |
| return { | |
| "symbol": symbol, | |
| "close": float(df["close"].iloc[-1]), | |
| "trend": regime_data["trend"], | |
| "vol_ratio": regime_data["vol_ratio"], | |
| "vol_expanding": regime_data["vol_expanding"], | |
| "atr_pct": regime_data["atr_pct"], | |
| "spike": volume_data["spike"], | |
| "climax": volume_data["climax"], | |
| "breakout": volume_data["breakout"], | |
| "obv_slope": volume_data["obv_slope_norm"], | |
| "delta_sign": volume_data["delta_sign"], | |
| "vetoed": vetoed, | |
| "veto_reason": veto_reason, | |
| "regime_score": scores["regime_score"], | |
| "volume_score": scores["volume_score"], | |
| "structure_score": scores["structure_score"], | |
| "total_score": scores["total_score"], | |
| "risk": risk_data, | |
| } | |
| def build_summary_table(ranked: list, top_n: int) -> str: | |
| header = ( | |
| f"{'#':>3} {'Symbol':<14} {'Score':>6} {'Regime':>7} " | |
| f"{'Volume':>7} {'Structure':>10} {'Trend':<8} " | |
| f"{'VolRatio':>8} {'Spike':>5} {'BOS':>5} {'Status'}\n" | |
| ) | |
| separator = "β" * 110 + "\n" | |
| rows = header + separator | |
| for rank, (sym, data) in enumerate(ranked[:top_n], 1): | |
| trend_icon = _TREND_EMOJI.get(data["trend"], "βͺ") | |
| breakout_lbl = _BREAKOUT_LABEL.get(data["breakout"], "β") | |
| spike_lbl = "β" if data["spike"] else "β" | |
| status = "VETOED" if data["vetoed"] else "OK" | |
| rows += ( | |
| f"{rank:>3} {sym:<14} {data['total_score']:>6.4f} " | |
| f"{data['regime_score']:>7.4f} {data['volume_score']:>7.4f} " | |
| f"{data['structure_score']:>10.4f} " | |
| f"{trend_icon} {data['trend']:<6} " | |
| f"{data['vol_ratio']:>8.2f} {spike_lbl:>5} " | |
| f"{breakout_lbl:>5} {status}\n" | |
| ) | |
| return rows | |
| def build_top_setup_detail(data: dict) -> str: | |
| r = data["risk"] | |
| sym = data["symbol"] | |
| trend_icon = _TREND_EMOJI.get(data["trend"], "βͺ") | |
| lines = [ | |
| "β" * 60, | |
| f" BEST SETUP: {sym}", | |
| "β" * 60, | |
| f" Trend: {trend_icon} {data['trend'].upper()}", | |
| f" Close Price: {r['entry_price']:.8f}", | |
| f"", | |
| f" ββ SCORES ββββββββββββββββββββββββββββββ", | |
| f" Regime: {format_score_bar(data['regime_score'])}", | |
| f" Volume: {format_score_bar(data['volume_score'])}", | |
| f" Structure: {format_score_bar(data['structure_score'])}", | |
| f" Total: {format_score_bar(data['total_score'])}", | |
| f"", | |
| f" ββ RISK PARAMETERS ββββββββββββββββββββββ", | |
| f" ATR: {r['atr']:.8f} ({r['atr_pct']:.3f}%)", | |
| f" Vol Ratio: {r['vol_ratio']:.2f}x (quality: {r['risk_quality']:.0%})", | |
| f" Risk Fraction: {r['risk_fraction']:.3f}%", | |
| f" $ At Risk: ${r['dollar_at_risk']:.2f}", | |
| f" Position Size: ${r['position_notional']:.2f} notional", | |
| f" Leverage (est): {r['leverage_implied']:.1f}x", | |
| f"", | |
| f" ββ LONG SCENARIO βββββββββββββββββββββββ", | |
| f" Stop Loss: {r['stop_long']:.8f}", | |
| f" Take Profit: {r['target_long']:.8f}", | |
| f" R:R Ratio: 1 : {r['rr_ratio']:.1f}", | |
| f"", | |
| f" ββ SHORT SCENARIO ββββββββββββββββββββββ", | |
| f" Stop Loss: {r['stop_short']:.8f}", | |
| f" Take Profit: {r['target_short']:.8f}", | |
| f" R:R Ratio: 1 : {r['rr_ratio']:.1f}", | |
| f"", | |
| f" Volume Spike: {'YES β' if data['spike'] else 'NO β'}", | |
| f" Volume Climax: {'YES β ' if data['climax'] else 'NO'}", | |
| f" Breakout: {_BREAKOUT_LABEL.get(data['breakout'], 'β')}", | |
| f" OBV Slope: {data['obv_slope']:+.4f}", | |
| f" Delta (5-bar): {'BUYING β' if data['delta_sign'] > 0 else 'SELLING β'}", | |
| "β" * 60, | |
| ] | |
| return "\n".join(lines) | |
| def parse_symbol_list(raw: str) -> List[str]: | |
| symbols = [] | |
| for tok in raw.replace(",", " ").replace("\n", " ").split(): | |
| tok = tok.strip().upper() | |
| if tok and "-" in tok: | |
| symbols.append(tok) | |
| elif tok: | |
| symbols.append(f"{tok}-USDT") | |
| return symbols if symbols else DEFAULT_SYMBOLS | |
| def run_analysis( | |
| symbols_input: str, | |
| equity: float, | |
| top_n: int, | |
| use_live_instruments: bool, | |
| progress=gr.Progress(track_tqdm=False), | |
| ) -> str: | |
| start_ts = time.time() | |
| output_lines = [] | |
| output_lines.append("β" * 60) | |
| output_lines.append(" OKX QUANTITATIVE ANALYSIS ENGINE") | |
| output_lines.append("β" * 60) | |
| if use_live_instruments: | |
| output_lines.append("β³ Fetching live instrument list from OKX...") | |
| symbols = fetch_instruments("SPOT") | |
| if not symbols: | |
| output_lines.append("β Failed to fetch live instruments. Using defaults.") | |
| symbols = DEFAULT_SYMBOLS | |
| else: | |
| output_lines.append(f"β Found {len(symbols)} live USDT spot instruments") | |
| else: | |
| symbols = parse_symbol_list(symbols_input) | |
| output_lines.append(f"β Analyzing {len(symbols)} symbol(s)") | |
| output_lines.append(f" Timeframe: {TIMEFRAME} | Candles: {CANDLE_LIMIT} | Equity: ${equity:,.0f}") | |
| output_lines.append("") | |
| fetched_count = [0] | |
| total = len(symbols) | |
| def progress_cb(i, t, sym): | |
| fetched_count[0] = i | |
| progress(i / t, desc=f"Fetching {sym} ({i}/{t})") | |
| ohlcv_map = fetch_multiple( | |
| symbols, | |
| min_bars=50, | |
| progress_callback=progress_cb, | |
| ) | |
| output_lines.append(f"β Fetched {len(ohlcv_map)}/{total} symbols successfully") | |
| output_lines.append("") | |
| all_results = {} | |
| failed = [] | |
| for sym, df in ohlcv_map.items(): | |
| try: | |
| all_results[sym] = analyze_single(sym, df, account_equity=equity) | |
| except Exception as exc: | |
| logger.error(f"Analysis error for {sym}: {exc}", exc_info=True) | |
| failed.append(sym) | |
| if failed: | |
| output_lines.append(f"β Analysis failed for: {', '.join(failed)}") | |
| output_lines.append("") | |
| ranked = rank_tokens(all_results) | |
| approved = [(s, d) for s, d in ranked if not d["vetoed"]] | |
| vetoed_count = sum(1 for _, d in ranked if d["vetoed"]) | |
| output_lines.append(f" RESULTS: {len(all_results)} analyzed | {len(approved)} approved | {vetoed_count} vetoed") | |
| output_lines.append("") | |
| output_lines.append(" TOP SETUPS RANKED BY TOTAL SCORE") | |
| output_lines.append("β" * 110) | |
| output_lines.append(build_summary_table(ranked, int(top_n))) | |
| if approved: | |
| best_sym, best_data = approved[0] | |
| output_lines.append("") | |
| output_lines.append(build_top_setup_detail(best_data)) | |
| else: | |
| output_lines.append("") | |
| output_lines.append(" β No approved setups found β all tokens vetoed.") | |
| elapsed = time.time() - start_ts | |
| output_lines.append("") | |
| output_lines.append(f" β Analysis complete in {elapsed:.1f}s") | |
| output_lines.append("β" * 60) | |
| return "\n".join(output_lines) | |
| def build_interface() -> gr.Blocks: | |
| with gr.Blocks( | |
| title="OKX Quant Analysis Engine", | |
| theme=gr.themes.Base( | |
| primary_hue="slate", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], | |
| ), | |
| css=""" | |
| body { background: #0a0a0f; } | |
| .gradio-container { | |
| background: #0a0a0f !important; | |
| max-width: 1100px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| .gr-button-primary { | |
| background: #1a6bff !important; | |
| border: none !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.05em !important; | |
| } | |
| .gr-button-primary:hover { background: #0050e0 !important; } | |
| #output_box textarea { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 13px !important; | |
| background: #0f0f1a !important; | |
| color: #c8d0e0 !important; | |
| border: 1px solid #1e2236 !important; | |
| min-height: 700px !important; | |
| } | |
| label, .gr-form label { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| color: #8899bb !important; | |
| font-size: 11px !important; | |
| letter-spacing: 0.08em !important; | |
| text-transform: uppercase !important; | |
| } | |
| .gr-panel { background: #0d0d18 !important; border: 1px solid #1e2236 !important; } | |
| h1 { color: #e0e8ff !important; font-family: 'JetBrains Mono', monospace !important; letter-spacing: 0.05em !important; } | |
| p { color: #5a6a8a !important; font-family: 'JetBrains Mono', monospace !important; font-size: 12px !important; } | |
| """, | |
| ) as app: | |
| gr.Markdown("# β OKX QUANT ANALYSIS ENGINE") | |
| gr.Markdown("Multi-token regime detection Β· volume analysis Β· dynamic risk Β· veto scoring") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| symbols_box = gr.Textbox( | |
| label="Symbols (comma or newline separated β leave blank for defaults)", | |
| placeholder="BTC-USDT, ETH-USDT, SOL-USDT ...", | |
| lines=4, | |
| value="", | |
| ) | |
| with gr.Column(scale=1): | |
| equity_slider = gr.Slider( | |
| label="Account Equity (USD)", | |
| minimum=100, | |
| maximum=1_000_000, | |
| step=100, | |
| value=DEFAULT_ACCOUNT_EQUITY, | |
| ) | |
| top_n_slider = gr.Slider( | |
| label="Top N Results to Show", | |
| minimum=5, | |
| maximum=100, | |
| step=5, | |
| value=TOP_N_DEFAULT, | |
| ) | |
| live_instruments = gr.Checkbox( | |
| label="Fetch live instruments from OKX (100+ symbols)", | |
| value=False, | |
| ) | |
| run_btn = gr.Button("βΆ RUN ANALYSIS", variant="primary", size="lg") | |
| output_box = gr.Textbox( | |
| label="Analysis Output", | |
| lines=40, | |
| max_lines=100, | |
| interactive=False, | |
| elem_id="output_box", | |
| ) | |
| run_btn.click( | |
| fn=run_analysis, | |
| inputs=[symbols_box, equity_slider, top_n_slider, live_instruments], | |
| outputs=output_box, | |
| ) | |
| gr.Markdown( | |
| "**Signals are for research purposes only. Not financial advice.** " | |
| "Data sourced from OKX public REST API." | |
| ) | |
| return app | |
| if __name__ == "__main__": | |
| app = build_interface() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_error=True, | |
| share=False, | |
| ) | |