import gradio as gr import json import os from pathlib import Path import requests import base64 from io import BytesIO # Load watchlist configuration def load_watchlists(): """Load watchlist configuration from JSON file""" config_path = Path("watchlists_config.json") if config_path.exists(): with open(config_path, 'r') as f: return json.load(f) return {} def load_stocks_from_file(filename): """Load stock symbols from a watchlist file""" file_path = Path(filename) if file_path.exists(): with open(file_path, 'r') as f: content = f.read().strip() # Split by comma and clean whitespace stocks = [s.strip() for s in content.split(',') if s.strip()] return stocks return [] def generate_chart_url(symbol, timeframe): """Generate StockCharts URL based on symbol and timeframe""" base_url = "https://stockcharts.com/c-sc/sc?chart=" # Random parameter for cache busting import random rand_param = random.random() if timeframe == "5-Minute": return f"{base_url}{symbol},uu[900,700]iahayaca[bb][i!ub14!la12,26,9][pd20,2]&r={rand_param}" elif timeframe == "Daily": return f"{base_url}{symbol},uu[900,700]dahayaca[bb][i!ub14!lu][pd20,2]&r={rand_param}" elif timeframe == "Weekly": return f"{base_url}{symbol},uu[900,700]whhayaca[bb][i!ub14!lu][pd20,2]&r={rand_param}" return "" def download_chart_image(url): """Download chart image from StockCharts and convert to base64""" try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Referer': 'https://stockcharts.com/', 'Connection': 'keep-alive', 'Sec-Fetch-Dest': 'image', 'Sec-Fetch-Mode': 'no-cors', 'Sec-Fetch-Site': 'same-origin' } response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # Convert to base64 img_base64 = base64.b64encode(response.content).decode('utf-8') return f"data:image/png;base64,{img_base64}" except Exception as e: print(f"Error downloading image from {url}: {str(e)}") # Return a placeholder image URL or error message return None def generate_single_stock_images(symbol): """Generate HTML for single stock analysis (3 charts)""" timeframes = ["5-Minute", "Daily", "Weekly"] html = f"

股票分析: {symbol}

" html += '
' for tf in timeframes: url = generate_chart_url(symbol, tf) img_data = download_chart_image(url) if img_data: html += f'''

{tf}

{symbol} - {tf}
''' else: html += f'''

{tf}

加载图片失败

''' html += '
' return html def update_single_stock(symbol): """Update single stock tab with charts""" if not symbol or not symbol.strip(): return "

请输入股票代码

" symbol = symbol.strip().upper() return generate_single_stock_images(symbol) def generate_watchlist_images(watchlist_name, timeframe): """Generate HTML for watchlist with selected timeframe""" if not watchlist_name or watchlist_name == "Select a watchlist": return "

请选择一个观察列表

" # Load watchlist config config = load_watchlists() if watchlist_name not in config: return f"

观察列表 '{watchlist_name}' 未找到

" # Load stocks from file filename = config[watchlist_name] stocks = load_stocks_from_file(filename) if not stocks: return f"

文件 {filename} 中没有找到股票

" # Generate HTML html = f"

观察列表: {watchlist_name} ({timeframe})

" html += '
' for symbol in stocks: url = generate_chart_url(symbol, timeframe) img_data = download_chart_image(url) if img_data: html += f'''

{symbol}

{symbol} - {timeframe}
''' else: html += f'''

{symbol}

加载图片失败

''' html += '
' return html def get_watchlist_names(): """Get list of watchlist names from config""" config = load_watchlists() return ["Select a watchlist"] + list(config.keys()) def save_watchlist_config(config): """Save watchlist configuration to JSON file""" config_path = Path("watchlists_config.json") with open(config_path, 'w') as f: json.dump(config, f, indent=2) def save_stocks_to_file(filename, stocks_text): """Save stock symbols to a watchlist file""" file_path = Path(filename) with open(file_path, 'w') as f: f.write(stocks_text.strip()) def add_or_update_watchlist(watchlist_name, stocks_text): """Add a new watchlist or update an existing one""" if not watchlist_name or not watchlist_name.strip(): return "❌ 请输入观察列表名称", gr.update(), gr.update() watchlist_name = watchlist_name.strip() if not stocks_text or not stocks_text.strip(): return "❌ 请输入股票代码", gr.update(), gr.update() # Generate filename from watchlist name filename = watchlist_name.lower().replace(' ', '_') + '.txt' # Save stocks to file save_stocks_to_file(filename, stocks_text) # Update config config = load_watchlists() config[watchlist_name] = filename save_watchlist_config(config) # Get updated list for dropdown updated_choices = get_watchlist_names() return ( f"✅ 观察列表 '{watchlist_name}' 已保存!", gr.update(choices=updated_choices), gr.update(choices=list(config.keys()), value=None) ) def delete_watchlist(watchlist_name): """Delete a watchlist""" if not watchlist_name: return "❌ 请选择要删除的观察列表", gr.update(), gr.update() config = load_watchlists() if watchlist_name not in config: return f"❌ 观察列表 '{watchlist_name}' 不存在", gr.update(), gr.update() # Get filename and delete file filename = config[watchlist_name] file_path = Path(filename) if file_path.exists(): file_path.unlink() # Remove from config del config[watchlist_name] save_watchlist_config(config) # Get updated list for dropdown updated_choices = get_watchlist_names() return ( f"✅ 观察列表 '{watchlist_name}' 已删除!", gr.update(choices=updated_choices), gr.update(choices=list(config.keys()) if config else [], value=None) ) def load_watchlist_for_edit(watchlist_name): """Load watchlist content for editing""" if not watchlist_name: return "", "" config = load_watchlists() if watchlist_name not in config: return "", "" filename = config[watchlist_name] stocks = load_stocks_from_file(filename) stocks_text = ", ".join(stocks) return watchlist_name, stocks_text # Create Gradio interface with gr.Blocks(title="股票分析仪表板") as demo: gr.Markdown("# 股票分析仪表板") with gr.Tabs(): # Tab 1: Single Stock Analysis with gr.Tab("单股分析"): gr.Markdown("### 输入股票代码查看图表") with gr.Row(): stock_input = gr.Textbox( label="股票代码", placeholder="例如: QQQ, AAPL, SPY", scale=3 ) analyze_btn = gr.Button("分析", scale=1) single_stock_output = gr.HTML() # Event handlers analyze_btn.click( fn=update_single_stock, inputs=[stock_input], outputs=[single_stock_output] ) stock_input.submit( fn=update_single_stock, inputs=[stock_input], outputs=[single_stock_output] ) # Tab 2: Watchlist with gr.Tab("观察列表"): gr.Markdown("### 选择观察列表和时间周期查看所有图表") with gr.Row(): watchlist_dropdown = gr.Dropdown( choices=get_watchlist_names(), value="Select a watchlist", label="选择观察列表", scale=2 ) timeframe_dropdown = gr.Dropdown( choices=["5-Minute", "Daily", "Weekly"], value="Daily", label="选择时间周期", scale=1 ) watchlist_output = gr.HTML() # Event handlers - update on any dropdown change watchlist_dropdown.change( fn=generate_watchlist_images, inputs=[watchlist_dropdown, timeframe_dropdown], outputs=[watchlist_output] ) timeframe_dropdown.change( fn=generate_watchlist_images, inputs=[watchlist_dropdown, timeframe_dropdown], outputs=[watchlist_output] ) # Tab 3: Manage Watchlists with gr.Tab("管理观察列表"): gr.Markdown("### 添加、修改或删除观察列表") with gr.Row(): with gr.Column(): gr.Markdown("#### 添加/修改观察列表") manage_name_input = gr.Textbox( label="观察列表名称", placeholder="例如: Tech Stocks" ) manage_stocks_input = gr.TextArea( label="股票代码(逗号分隔)", placeholder="例如: AAPL, MSFT, GOOGL, NVDA", lines=5 ) with gr.Row(): save_btn = gr.Button("💾 保存", variant="primary") save_status = gr.Markdown() with gr.Column(): gr.Markdown("#### 编辑/删除现有列表") edit_dropdown = gr.Dropdown( choices=list(load_watchlists().keys()), label="选择要编辑的观察列表", value=None ) with gr.Row(): load_btn = gr.Button("📝 加载到左侧编辑") delete_btn = gr.Button("🗑️ 删除", variant="stop") # Event handlers for management tab save_btn.click( fn=add_or_update_watchlist, inputs=[manage_name_input, manage_stocks_input], outputs=[save_status, watchlist_dropdown, edit_dropdown] ) delete_btn.click( fn=delete_watchlist, inputs=[edit_dropdown], outputs=[save_status, watchlist_dropdown, edit_dropdown] ) load_btn.click( fn=load_watchlist_for_edit, inputs=[edit_dropdown], outputs=[manage_name_input, manage_stocks_input] ) if __name__ == "__main__": demo.launch()