""" 🦞 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"""
{ticker} ${ind.get('current_price', 'N/A')} {change_str}
{signal_emoji} {signal} Score: {score}/10
RSI: {ind.get('rsi', 'N/A')} | MACD: {'Bullish' if ind.get('macd_bullish') else 'Bearish'} | Trend: {ind.get('trend', 'N/A').replace('_', ' ').title()} | Vol: {f"{ind['volume_ratio']}x avg" if ind.get('volume_ratio') else 'N/A'}
""", unsafe_allow_html=True, ) # ── Advanced Indicators: Absorption Bubbles + Pro Scalper ── absorption = r.get("absorption", {}) scalper = r.get("scalper", {}) adv_parts = [] # Absorption Bubbles display β€” always show status if absorption: if absorption.get("absorption_detected"): abs_bias = absorption.get("signal_bias", "neutral") abs_emoji = {"bullish": "🟒", "bearish": "πŸ”΄", "neutral": "βšͺ"}.get(abs_bias, "βšͺ") abs_label = f"{abs_emoji} Absorption: {abs_bias.title()}" # Show strength if available buy_str = absorption.get("recent_buying_strength", 0) sell_str = absorption.get("recent_selling_strength", 0) if buy_str > 0: abs_label += f" (Buy: {buy_str}Οƒ)" if sell_str > 0: abs_label += f" (Sell: {sell_str}Οƒ)" # Show events events = absorption.get("events", []) if events: abs_label += f" [{', '.join(e.replace('_', ' ') for e in events[:2])}]" adv_parts.append(abs_label) elif absorption.get("absorption_score", 0) > 0: abs_bias = absorption.get("signal_bias", "neutral") abs_emoji = {"bullish": "🟒", "bearish": "πŸ”΄", "neutral": "βšͺ"}.get(abs_bias, "βšͺ") score_pct = int(absorption.get("absorption_score", 0) * 100) events = absorption.get("events", []) evt_str = f" [{', '.join(e.replace('_', ' ') for e in events[:2])}]" if events else "" adv_parts.append(f"{abs_emoji} Absorption: {abs_bias.title()} ({score_pct}%){evt_str}") else: adv_parts.append("βšͺ Absorption: None detected") # Pro Scalper display β€” always show status if scalper: sc_signal = scalper.get("signal", "neutral") sc_conf = scalper.get("confidence", 0) sc_emoji = {"buy": "πŸ”Ό", "sell": "πŸ”½", "neutral": "βž–"}.get(sc_signal, "βž–") sc_label = f"{sc_emoji} Scalper: {sc_signal.upper()} ({int(sc_conf * 100)}%)" direction = scalper.get("direction", "neutral") if direction != "neutral": sc_label += f" | Dir: {direction.title()}" zone = scalper.get("zone", "neutral") if zone != "neutral": zone_emoji = {"overbought": "πŸ”₯", "oversold": "❄️"}.get(zone, "") sc_label += f" | {zone_emoji} {zone.title()}" reversal = scalper.get("reversal") if reversal: rev_emoji = "↩️" if "bullish" in reversal else "β†ͺ️" sc_label += f" | {rev_emoji} {reversal.replace('_', ' ').title()}" adv_parts.append(sc_label) if adv_parts: st.markdown( f"
" f"{' β€’ '.join(adv_parts)}
", unsafe_allow_html=True, ) # Expand for AI analysis (available for all signals) with st.expander(f"πŸ€– Get AI Analysis for {ticker}"): # Two options: data-only or data + screenshot analysis_tab = st.radio( "Analysis type", options=["πŸ“Š Data Only", "πŸ“Έ Data + Screenshot"], horizontal=True, key=f"tab_{ticker}", label_visibility="collapsed", ) if analysis_tab == "πŸ“Έ Data + Screenshot": from streamlit_paste_button import paste_image_button import io as _io paste_result = paste_image_button( label="πŸ“‹ Paste chart screenshot from clipboard", text_color="#ffffff", background_color="#ff6b35", hover_background_color="#e55a2b", key=f"paste_{ticker}", ) if paste_result.image_data is not None: buf = _io.BytesIO() paste_result.image_data.save(buf, format="PNG") image_bytes = buf.getvalue() st.image(image_bytes, caption=f"{ticker} chart", use_container_width=True) if st.button(f"πŸ”¬ Analyze {ticker} with Chart", key=f"vision_{ticker}"): with st.spinner(f"🧠 AI analyzing {ticker} (vision + data)..."): tf_label = { "15m": "15-minute", "30m": "30-minute", "1h": "1-hour", "4h": "4-hour", "1d": "Daily", "1wk": "Weekly", }.get(interval, "Daily") ai_result = analyze_with_screenshot( ticker=ticker, indicators=ind, score=score, image_bytes=image_bytes, provider=ai_provider, timeframe=tf_label, absorption=r.get("absorption"), scalper=r.get("scalper"), ) _show_advanced_summary(r.get("absorption", {}), r.get("scalper", {})) _display_ai_result(ai_result, ai_provider) else: if st.button(f"Analyze {ticker}", key=f"ai_{ticker}"): with st.spinner(f"🧠 AI analyzing {ticker}..."): tf_label = { "15m": "15-minute", "30m": "30-minute", "1h": "1-hour", "4h": "4-hour", "1d": "Daily", "1wk": "Weekly", }.get(interval, "Daily") ai_result = analyze_ticker( ticker=ticker, indicators=ind, score=score, provider=ai_provider, timeframe=tf_label, absorption=r.get("absorption"), scalper=r.get("scalper"), ) # Show advanced indicator summary before AI result _show_advanced_summary(r.get("absorption", {}), r.get("scalper", {})) _display_ai_result(ai_result, ai_provider) # ────────────────────────────────────────────── # Footer # ────────────────────────────────────────────── st.divider() st.caption( "🦞 OpenClaw Live Scanner v1.0 | " "AI-powered market scanning | " "⚠️ Not financial advice β€” always do your own research" )