""" π¦ OpenClaw Live Scanner β Real-time market scanning with AI trade validation. """ import streamlit as st import pandas as pd from datetime import datetime from dotenv import load_dotenv load_dotenv() from src.scanner import scan_watchlist, WATCHLISTS, fetch_ticker_data, compute_indicators, compute_score from src.tv_scanner import scan_tradingview from src.ai_analyst import analyze_ticker, analyze_with_screenshot # ββββββββββββββββββββββββββββββββββββββββββββββ # Page Config # ββββββββββββββββββββββββββββββββββββββββββββββ st.set_page_config( page_title="π¦ OpenClaw Live Scanner", page_icon="π¦", layout="wide", ) st.title("π¦ OpenClaw Live Scanner") st.caption("Real-time market scanning with AI-powered GO / NO GO signals") # ββββββββββββββββββββββββββββββββββββββββββββββ # Helper: Display AI result # ββββββββββββββββββββββββββββββββββββββββββββββ def _display_ai_result(ai_result: dict, selected_provider: str): """Display AI analysis result with friendly error handling.""" if ai_result.get("_parsed"): action = ai_result.get("action", "HOLD") confidence = ai_result.get("confidence", 0) conf_pct = int(confidence * 100) if isinstance(confidence, float) and confidence <= 1 else int(confidence) provider_used = ai_result.get("_provider_used", selected_provider) # Show fallback notice if provider_used != selected_provider: st.info(f"β‘ Rate limit hit on {selected_provider} β used {provider_used} instead.") # Analysis type badge if ai_result.get("_analysis_type") == "vision+data": st.caption("π¬ Vision + Data analysis") st.markdown(f"**Signal:** {action} | **Confidence:** {conf_pct}%") c1, c2, c3 = st.columns(3) entry = ai_result.get("entry_price") stop = ai_result.get("stop_loss") target = ai_result.get("target_1") if entry: c1.metric("Entry", f"${entry:,.2f}") if stop: c2.metric("Stop Loss", f"${stop:,.2f}") if target: c3.metric("Target", f"${target:,.2f}") # Time estimate est_time = ai_result.get("estimated_timeframe", "") if est_time: st.markdown(f"β±οΈ **Estimated Timeframe:** {est_time}") # Visual patterns (from screenshot analysis) patterns = ai_result.get("visual_patterns", []) if patterns: st.markdown(f"ποΈ **Visual Patterns:** {', '.join(patterns)}") st.markdown(f"**Reasoning:** {ai_result.get('reasoning', '')}") key_levels = ai_result.get("key_levels", "") if key_levels: st.markdown(f"π **Key Levels:** {key_levels}") risks = ai_result.get("risks", []) if risks: st.markdown("**Risks:** " + " | ".join(risks)) else: # Parse failed β friendly error reasoning = ai_result.get("reasoning", "") if "rate" in reasoning.lower() or "429" in reasoning or "quota" in reasoning.lower(): st.warning("β‘ All AI providers are rate limited right now. Please wait a minute and try again.") elif "401" in reasoning or "auth" in reasoning.lower(): st.warning("π API key issue. Check your .env file has valid keys.") else: st.warning("β οΈ AI returned an unparseable response. Showing raw output:") with st.expander("Raw response"): st.code(ai_result.get("_raw", "No response"), language=None) def _show_advanced_summary(absorption: dict, scalper: dict): """Show a dedicated advanced indicators box inside the AI analysis section.""" parts = [] # Absorption if absorption: abs_detected = absorption.get("absorption_detected", False) abs_score = absorption.get("absorption_score", 0) abs_bias = absorption.get("signal_bias", "neutral") abs_events = absorption.get("events", []) abs_emoji = {"bullish": "π’", "bearish": "π΄", "neutral": "βͺ"}.get(abs_bias, "βͺ") if abs_detected or abs_score > 0: evt_str = ", ".join(e.replace("_", " ") for e in abs_events) if abs_events else "none" parts.append(f"{abs_emoji} **Absorption Bubbles:** {abs_bias.title()} ({int(abs_score * 100)}%) β {evt_str}") else: parts.append("βͺ **Absorption Bubbles:** No significant absorption detected") # Scalper if scalper: sc_signal = scalper.get("signal", "neutral") sc_conf = scalper.get("confidence", 0) sc_dir = scalper.get("direction", "neutral") sc_zone = scalper.get("zone", "neutral") sc_reversal = scalper.get("reversal") sc_emoji = {"buy": "πΌ", "sell": "π½", "neutral": "β"}.get(sc_signal, "β") sc_parts = [f"{sc_emoji} **Pro Scalper:** {sc_signal.upper()} ({int(sc_conf * 100)}%)"] sc_parts.append(f"Direction: {sc_dir.title()}") if sc_zone != "neutral": sc_parts.append(f"Zone: {sc_zone.title()}") if sc_reversal: sc_parts.append(f"Reversal: {sc_reversal.replace('_', ' ').title()}") parts.append(" | ".join(sc_parts)) if parts: st.markdown("---") st.markdown("**π¬ Advanced Indicators:**") for p in parts: st.markdown(f" {p}") st.markdown("---") # ββββββββββββββββββββββββββββββββββββββββββββββ # Sidebar # ββββββββββββββββββββββββββββββββββββββββββββββ st.sidebar.header("βοΈ Scanner Settings") # Data source data_source = st.sidebar.selectbox( "Data Source", options=["tradingview", "yfinance"], index=0, format_func=lambda x: { "tradingview": "πΊ TradingView (near real-time)", "yfinance": "π Yahoo Finance (15-min delay)", }[x], help="TradingView provides near real-time data with pre-computed indicators.", ) # Watchlist selection watchlist_name = st.sidebar.selectbox( "Watchlist", options=list(WATCHLISTS.keys()) + ["Custom"], index=0, ) if watchlist_name == "Custom": custom_tickers = st.sidebar.text_input( "Tickers (comma-separated)", value="NVDA, AAPL, BTC-USD", help="Enter stock/crypto tickers separated by commas", ) tickers = [t.strip().upper() for t in custom_tickers.split(",") if t.strip()] else: tickers = WATCHLISTS[watchlist_name] # Auto-select market based on watchlist default_market_map = { "US Tech": "america", "Crypto": "crypto", "Forex": "forex", "SGX": "singapore", "Custom": "america", } default_market = default_market_map.get(watchlist_name, "america") # Timeframe interval = st.sidebar.selectbox( "Timeframe", options=["15m", "30m", "1h", "4h", "1d", "1wk"], index=4, format_func=lambda x: { "15m": "15 Minutes (day trade)", "30m": "30 Minutes (day trade)", "1h": "1 Hour (intraday)", "4h": "4 Hour (swing)", "1d": "Daily (swing)", "1wk": "Weekly (position)", }[x], ) period_map = {"15m": "1d", "30m": "2d", "1h": "5d", "4h": "1mo", "1d": "3mo", "1wk": "1y"} period = period_map[interval] # TradingView market (only shown when TV is selected) tv_market = "america" if data_source == "tradingview": market_options = ["america", "crypto", "forex", "singapore"] tv_market = st.sidebar.selectbox( "Market", options=market_options, index=market_options.index(default_market), format_func=lambda x: { "america": "πΊπΈ US Stocks", "crypto": "βΏ Crypto", "forex": "π± Forex", "singapore": "πΈπ¬ SGX", }[x], ) # AI Provider ai_provider = st.sidebar.selectbox( "AI Provider", options=["google", "openrouter", "huggingface"], index=0, format_func=lambda x: { "google": "Google Gemini (free)", "openrouter": "OpenRouter Gemma (free)", "huggingface": "HuggingFace Qwen (free)", }[x], help="Auto-falls back to next provider if rate limited.", ) st.sidebar.divider() st.sidebar.caption(f"Scanning {len(tickers)} tickers | {interval} timeframe") # ββββββββββββββββββββββββββββββββββββββββββββββ # Main: Scanner # ββββββββββββββββββββββββββββββββββββββββββββββ # Scan button col_scan, col_time = st.columns([1, 2]) with col_scan: scan_clicked = st.button("π Scan Now", type="primary", use_container_width=True) with col_time: st.caption(f"Last scan: {st.session_state.get('last_scan_time', 'Never')}") # Clear old results if settings changed current_settings = f"{data_source}_{watchlist_name}_{interval}_{tv_market}" if st.session_state.get("_last_settings") != current_settings: st.session_state.scan_results = [] st.session_state._last_settings = current_settings if scan_clicked: with st.spinner(f"π‘ Scanning {len(tickers)} tickers via {data_source}..."): if data_source == "tradingview": # Map interval for TradingView tv_interval = {"15m": "15m", "30m": "30m", "1d": "1d", "1h": "1h", "4h": "4h", "1wk": "1w"}.get(interval, "1d") results = scan_tradingview(tickers, market=tv_market, interval=tv_interval) else: results = scan_watchlist(tickers, period=period, interval=interval) for r in results: r["source"] = "yfinance" st.session_state.scan_results = results st.session_state.last_scan_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Display results results = st.session_state.get("scan_results", []) if not results: st.info("π Click **Scan Now** to analyze your watchlist.") else: # Summary metrics go_count = sum(1 for r in results if r["signal"] == "GO") wait_count = sum(1 for r in results if r["signal"] == "WAIT") nogo_count = sum(1 for r in results if r["signal"] == "NO GO") m1, m2, m3, m4 = st.columns(4) m1.metric("Total Scanned", len(results)) m2.metric("π’ GO", go_count) m3.metric("π‘ WAIT", wait_count) m4.metric("π΄ NO GO", nogo_count) # Show data source source_label = results[0].get("source", "unknown") if results else "unknown" source_emoji = {"tradingview": "πΊ", "yfinance": "π"}.get(source_label, "") st.caption(f"Data source: {source_emoji} {source_label.title()}") st.divider() # Results table for r in results: ticker = r["ticker"] score = r["score"] signal = r["signal"] ind = r["indicators"] if "error" in ind: st.markdown(f"**{ticker}** β β οΈ Error: {ind['error']}") continue # Signal badge signal_emoji = {"GO": "π’", "WAIT": "π‘", "NO GO": "π΄", "ERROR": "β οΈ"}.get(signal, "βͺ") signal_color = {"GO": "#1a3a1a", "WAIT": "#3a3a1a", "NO GO": "#3a1a1a"}.get(signal, "#1a1a1a") border_color = {"GO": "green", "WAIT": "orange", "NO GO": "red"}.get(signal, "gray") # Change color change = ind.get("change_pct", 0) change_str = f"+{change}%" if change >= 0 else f"{change}%" change_color = "green" if change >= 0 else "red" # Card with st.container(): st.markdown( f"""