Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,365 +1,365 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
import requests
|
| 6 |
-
import base64
|
| 7 |
-
from io import BytesIO
|
| 8 |
-
|
| 9 |
-
# Load watchlist configuration
|
| 10 |
-
def load_watchlists():
|
| 11 |
-
"""Load watchlist configuration from JSON file"""
|
| 12 |
-
config_path = Path("watchlists_config.json")
|
| 13 |
-
if config_path.exists():
|
| 14 |
-
with open(config_path, 'r') as f:
|
| 15 |
-
return json.load(f)
|
| 16 |
-
return {}
|
| 17 |
-
|
| 18 |
-
def load_stocks_from_file(filename):
|
| 19 |
-
"""Load stock symbols from a watchlist file"""
|
| 20 |
-
file_path = Path(filename)
|
| 21 |
-
if file_path.exists():
|
| 22 |
-
with open(file_path, 'r') as f:
|
| 23 |
-
content = f.read().strip()
|
| 24 |
-
# Split by comma and clean whitespace
|
| 25 |
-
stocks = [s.strip() for s in content.split(',') if s.strip()]
|
| 26 |
-
return stocks
|
| 27 |
-
return []
|
| 28 |
-
|
| 29 |
-
def generate_chart_url(symbol, timeframe):
|
| 30 |
-
"""Generate StockCharts URL based on symbol and timeframe"""
|
| 31 |
-
base_url = "https://stockcharts.com/c-sc/sc?chart="
|
| 32 |
-
|
| 33 |
-
# Random parameter for cache busting
|
| 34 |
-
import random
|
| 35 |
-
rand_param = random.random()
|
| 36 |
-
|
| 37 |
-
if timeframe == "5-Minute":
|
| 38 |
-
return f"{base_url}{symbol},uu[
|
| 39 |
-
elif timeframe == "Daily":
|
| 40 |
-
return f"{base_url}{symbol},uu[
|
| 41 |
-
elif timeframe == "Weekly":
|
| 42 |
-
return f"{base_url}{symbol},uu[
|
| 43 |
-
return ""
|
| 44 |
-
|
| 45 |
-
def download_chart_image(url):
|
| 46 |
-
"""Download chart image from StockCharts and convert to base64"""
|
| 47 |
-
try:
|
| 48 |
-
headers = {
|
| 49 |
-
'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',
|
| 50 |
-
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
| 51 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
| 52 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
| 53 |
-
'Referer': 'https://stockcharts.com/',
|
| 54 |
-
'Connection': 'keep-alive',
|
| 55 |
-
'Sec-Fetch-Dest': 'image',
|
| 56 |
-
'Sec-Fetch-Mode': 'no-cors',
|
| 57 |
-
'Sec-Fetch-Site': 'same-origin'
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
response = requests.get(url, headers=headers, timeout=10)
|
| 61 |
-
response.raise_for_status()
|
| 62 |
-
|
| 63 |
-
# Convert to base64
|
| 64 |
-
img_base64 = base64.b64encode(response.content).decode('utf-8')
|
| 65 |
-
return f"data:image/png;base64,{img_base64}"
|
| 66 |
-
except Exception as e:
|
| 67 |
-
print(f"Error downloading image from {url}: {str(e)}")
|
| 68 |
-
# Return a placeholder image URL or error message
|
| 69 |
-
return None
|
| 70 |
-
|
| 71 |
-
def generate_single_stock_images(symbol):
|
| 72 |
-
"""Generate HTML for single stock analysis (3 charts)"""
|
| 73 |
-
timeframes = ["5-Minute", "Daily", "Weekly"]
|
| 74 |
-
|
| 75 |
-
html = f"<h2>股票分析: {symbol}</h2>"
|
| 76 |
-
html += '<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 20px;">'
|
| 77 |
-
|
| 78 |
-
for tf in timeframes:
|
| 79 |
-
url = generate_chart_url(symbol, tf)
|
| 80 |
-
img_data = download_chart_image(url)
|
| 81 |
-
|
| 82 |
-
if img_data:
|
| 83 |
-
html += f'''
|
| 84 |
-
<div style="text-align: center;">
|
| 85 |
-
<h3>{tf}</h3>
|
| 86 |
-
<img src="{img_data}" alt="{symbol} - {tf}" style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px;">
|
| 87 |
-
</div>
|
| 88 |
-
'''
|
| 89 |
-
else:
|
| 90 |
-
html += f'''
|
| 91 |
-
<div style="text-align: center;">
|
| 92 |
-
<h3>{tf}</h3>
|
| 93 |
-
<p style="color: red;">加载图片失败</p>
|
| 94 |
-
</div>
|
| 95 |
-
'''
|
| 96 |
-
|
| 97 |
-
html += '</div>'
|
| 98 |
-
return html
|
| 99 |
-
|
| 100 |
-
def update_single_stock(symbol):
|
| 101 |
-
"""Update single stock tab with charts"""
|
| 102 |
-
if not symbol or not symbol.strip():
|
| 103 |
-
return "<p>请输入股票代码</p>"
|
| 104 |
-
|
| 105 |
-
symbol = symbol.strip().upper()
|
| 106 |
-
return generate_single_stock_images(symbol)
|
| 107 |
-
|
| 108 |
-
def generate_watchlist_images(watchlist_name, timeframe):
|
| 109 |
-
"""Generate HTML for watchlist with selected timeframe"""
|
| 110 |
-
if not watchlist_name or watchlist_name == "Select a watchlist":
|
| 111 |
-
return "<p>请选择一个观察列表</p>"
|
| 112 |
-
|
| 113 |
-
# Load watchlist config
|
| 114 |
-
config = load_watchlists()
|
| 115 |
-
if watchlist_name not in config:
|
| 116 |
-
return f"<p>观察列表 '{watchlist_name}' 未找到</p>"
|
| 117 |
-
|
| 118 |
-
# Load stocks from file
|
| 119 |
-
filename = config[watchlist_name]
|
| 120 |
-
stocks = load_stocks_from_file(filename)
|
| 121 |
-
|
| 122 |
-
if not stocks:
|
| 123 |
-
return f"<p>文件 {filename} 中没有找到股票</p>"
|
| 124 |
-
|
| 125 |
-
# Generate HTML
|
| 126 |
-
html = f"<h2>观察列表: {watchlist_name} ({timeframe})</h2>"
|
| 127 |
-
html += '<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 20px;">'
|
| 128 |
-
|
| 129 |
-
for symbol in stocks:
|
| 130 |
-
url = generate_chart_url(symbol, timeframe)
|
| 131 |
-
img_data = download_chart_image(url)
|
| 132 |
-
|
| 133 |
-
if img_data:
|
| 134 |
-
html += f'''
|
| 135 |
-
<div style="text-align: center;">
|
| 136 |
-
<h3>{symbol}</h3>
|
| 137 |
-
<img src="{img_data}" alt="{symbol} - {timeframe}" style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px;">
|
| 138 |
-
</div>
|
| 139 |
-
'''
|
| 140 |
-
else:
|
| 141 |
-
html += f'''
|
| 142 |
-
<div style="text-align: center;">
|
| 143 |
-
<h3>{symbol}</h3>
|
| 144 |
-
<p style="color: red;">加载图片失败</p>
|
| 145 |
-
</div>
|
| 146 |
-
'''
|
| 147 |
-
|
| 148 |
-
html += '</div>'
|
| 149 |
-
return html
|
| 150 |
-
|
| 151 |
-
def get_watchlist_names():
|
| 152 |
-
"""Get list of watchlist names from config"""
|
| 153 |
-
config = load_watchlists()
|
| 154 |
-
return ["Select a watchlist"] + list(config.keys())
|
| 155 |
-
|
| 156 |
-
def save_watchlist_config(config):
|
| 157 |
-
"""Save watchlist configuration to JSON file"""
|
| 158 |
-
config_path = Path("watchlists_config.json")
|
| 159 |
-
with open(config_path, 'w') as f:
|
| 160 |
-
json.dump(config, f, indent=2)
|
| 161 |
-
|
| 162 |
-
def save_stocks_to_file(filename, stocks_text):
|
| 163 |
-
"""Save stock symbols to a watchlist file"""
|
| 164 |
-
file_path = Path(filename)
|
| 165 |
-
with open(file_path, 'w') as f:
|
| 166 |
-
f.write(stocks_text.strip())
|
| 167 |
-
|
| 168 |
-
def add_or_update_watchlist(watchlist_name, stocks_text):
|
| 169 |
-
"""Add a new watchlist or update an existing one"""
|
| 170 |
-
if not watchlist_name or not watchlist_name.strip():
|
| 171 |
-
return "❌ 请输入观察列表名称", gr.update(), gr.update()
|
| 172 |
-
|
| 173 |
-
watchlist_name = watchlist_name.strip()
|
| 174 |
-
|
| 175 |
-
if not stocks_text or not stocks_text.strip():
|
| 176 |
-
return "❌ 请输入股票代码", gr.update(), gr.update()
|
| 177 |
-
|
| 178 |
-
# Generate filename from watchlist name
|
| 179 |
-
filename = watchlist_name.lower().replace(' ', '_') + '.txt'
|
| 180 |
-
|
| 181 |
-
# Save stocks to file
|
| 182 |
-
save_stocks_to_file(filename, stocks_text)
|
| 183 |
-
|
| 184 |
-
# Update config
|
| 185 |
-
config = load_watchlists()
|
| 186 |
-
config[watchlist_name] = filename
|
| 187 |
-
save_watchlist_config(config)
|
| 188 |
-
|
| 189 |
-
# Get updated list for dropdown
|
| 190 |
-
updated_choices = get_watchlist_names()
|
| 191 |
-
|
| 192 |
-
return (
|
| 193 |
-
f"✅ 观察列表 '{watchlist_name}' 已保存!",
|
| 194 |
-
gr.update(choices=updated_choices),
|
| 195 |
-
gr.update(choices=list(config.keys()), value=None)
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
def delete_watchlist(watchlist_name):
|
| 199 |
-
"""Delete a watchlist"""
|
| 200 |
-
if not watchlist_name:
|
| 201 |
-
return "❌ 请选择要删除的观察列表", gr.update(), gr.update()
|
| 202 |
-
|
| 203 |
-
config = load_watchlists()
|
| 204 |
-
|
| 205 |
-
if watchlist_name not in config:
|
| 206 |
-
return f"❌ 观察列表 '{watchlist_name}' 不存在", gr.update(), gr.update()
|
| 207 |
-
|
| 208 |
-
# Get filename and delete file
|
| 209 |
-
filename = config[watchlist_name]
|
| 210 |
-
file_path = Path(filename)
|
| 211 |
-
if file_path.exists():
|
| 212 |
-
file_path.unlink()
|
| 213 |
-
|
| 214 |
-
# Remove from config
|
| 215 |
-
del config[watchlist_name]
|
| 216 |
-
save_watchlist_config(config)
|
| 217 |
-
|
| 218 |
-
# Get updated list for dropdown
|
| 219 |
-
updated_choices = get_watchlist_names()
|
| 220 |
-
|
| 221 |
-
return (
|
| 222 |
-
f"✅ 观察列表 '{watchlist_name}' 已删除!",
|
| 223 |
-
gr.update(choices=updated_choices),
|
| 224 |
-
gr.update(choices=list(config.keys()) if config else [], value=None)
|
| 225 |
-
)
|
| 226 |
-
|
| 227 |
-
def load_watchlist_for_edit(watchlist_name):
|
| 228 |
-
"""Load watchlist content for editing"""
|
| 229 |
-
if not watchlist_name:
|
| 230 |
-
return "", ""
|
| 231 |
-
|
| 232 |
-
config = load_watchlists()
|
| 233 |
-
if watchlist_name not in config:
|
| 234 |
-
return "", ""
|
| 235 |
-
|
| 236 |
-
filename = config[watchlist_name]
|
| 237 |
-
stocks = load_stocks_from_file(filename)
|
| 238 |
-
stocks_text = ", ".join(stocks)
|
| 239 |
-
|
| 240 |
-
return watchlist_name, stocks_text
|
| 241 |
-
|
| 242 |
-
# Create Gradio interface
|
| 243 |
-
with gr.Blocks(title="股票分析仪表板") as demo:
|
| 244 |
-
gr.Markdown("# 股票分析仪表板")
|
| 245 |
-
|
| 246 |
-
with gr.Tabs():
|
| 247 |
-
# Tab 1: Single Stock Analysis
|
| 248 |
-
with gr.Tab("单股分析"):
|
| 249 |
-
gr.Markdown("### 输入股票代码查看图表")
|
| 250 |
-
|
| 251 |
-
with gr.Row():
|
| 252 |
-
stock_input = gr.Textbox(
|
| 253 |
-
label="股票代码",
|
| 254 |
-
placeholder="例如: QQQ, AAPL, SPY",
|
| 255 |
-
scale=3
|
| 256 |
-
)
|
| 257 |
-
analyze_btn = gr.Button("分析", scale=1)
|
| 258 |
-
|
| 259 |
-
single_stock_output = gr.HTML()
|
| 260 |
-
|
| 261 |
-
# Event handlers
|
| 262 |
-
analyze_btn.click(
|
| 263 |
-
fn=update_single_stock,
|
| 264 |
-
inputs=[stock_input],
|
| 265 |
-
outputs=[single_stock_output]
|
| 266 |
-
)
|
| 267 |
-
|
| 268 |
-
stock_input.submit(
|
| 269 |
-
fn=update_single_stock,
|
| 270 |
-
inputs=[stock_input],
|
| 271 |
-
outputs=[single_stock_output]
|
| 272 |
-
)
|
| 273 |
-
|
| 274 |
-
# Tab 2: Watchlist
|
| 275 |
-
with gr.Tab("观察列表"):
|
| 276 |
-
gr.Markdown("### 选择观察列表和时间周期查看所有图表")
|
| 277 |
-
|
| 278 |
-
with gr.Row():
|
| 279 |
-
watchlist_dropdown = gr.Dropdown(
|
| 280 |
-
choices=get_watchlist_names(),
|
| 281 |
-
value="Select a watchlist",
|
| 282 |
-
label="选择观察列表",
|
| 283 |
-
scale=2
|
| 284 |
-
)
|
| 285 |
-
|
| 286 |
-
timeframe_dropdown = gr.Dropdown(
|
| 287 |
-
choices=["5-Minute", "Daily", "Weekly"],
|
| 288 |
-
value="Daily",
|
| 289 |
-
label="选择时间周期",
|
| 290 |
-
scale=1
|
| 291 |
-
)
|
| 292 |
-
|
| 293 |
-
watchlist_output = gr.HTML()
|
| 294 |
-
|
| 295 |
-
# Event handlers - update on any dropdown change
|
| 296 |
-
watchlist_dropdown.change(
|
| 297 |
-
fn=generate_watchlist_images,
|
| 298 |
-
inputs=[watchlist_dropdown, timeframe_dropdown],
|
| 299 |
-
outputs=[watchlist_output]
|
| 300 |
-
)
|
| 301 |
-
|
| 302 |
-
timeframe_dropdown.change(
|
| 303 |
-
fn=generate_watchlist_images,
|
| 304 |
-
inputs=[watchlist_dropdown, timeframe_dropdown],
|
| 305 |
-
outputs=[watchlist_output]
|
| 306 |
-
)
|
| 307 |
-
|
| 308 |
-
# Tab 3: Manage Watchlists
|
| 309 |
-
with gr.Tab("管理观察列表"):
|
| 310 |
-
gr.Markdown("### 添加、修改或删除观察列表")
|
| 311 |
-
|
| 312 |
-
with gr.Row():
|
| 313 |
-
with gr.Column():
|
| 314 |
-
gr.Markdown("#### 添加/修改观察列表")
|
| 315 |
-
|
| 316 |
-
manage_name_input = gr.Textbox(
|
| 317 |
-
label="观察列表名称",
|
| 318 |
-
placeholder="例如: Tech Stocks"
|
| 319 |
-
)
|
| 320 |
-
|
| 321 |
-
manage_stocks_input = gr.TextArea(
|
| 322 |
-
label="股票代码(逗号分隔)",
|
| 323 |
-
placeholder="例如: AAPL, MSFT, GOOGL, NVDA",
|
| 324 |
-
lines=5
|
| 325 |
-
)
|
| 326 |
-
|
| 327 |
-
with gr.Row():
|
| 328 |
-
save_btn = gr.Button("💾 保存", variant="primary")
|
| 329 |
-
|
| 330 |
-
save_status = gr.Markdown()
|
| 331 |
-
|
| 332 |
-
with gr.Column():
|
| 333 |
-
gr.Markdown("#### 编辑/删除现有列表")
|
| 334 |
-
|
| 335 |
-
edit_dropdown = gr.Dropdown(
|
| 336 |
-
choices=list(load_watchlists().keys()),
|
| 337 |
-
label="选择要编辑的观察列表",
|
| 338 |
-
value=None
|
| 339 |
-
)
|
| 340 |
-
|
| 341 |
-
with gr.Row():
|
| 342 |
-
load_btn = gr.Button("📝 加载到左侧编辑")
|
| 343 |
-
delete_btn = gr.Button("🗑️ 删除", variant="stop")
|
| 344 |
-
|
| 345 |
-
# Event handlers for management tab
|
| 346 |
-
save_btn.click(
|
| 347 |
-
fn=add_or_update_watchlist,
|
| 348 |
-
inputs=[manage_name_input, manage_stocks_input],
|
| 349 |
-
outputs=[save_status, watchlist_dropdown, edit_dropdown]
|
| 350 |
-
)
|
| 351 |
-
|
| 352 |
-
delete_btn.click(
|
| 353 |
-
fn=delete_watchlist,
|
| 354 |
-
inputs=[edit_dropdown],
|
| 355 |
-
outputs=[save_status, watchlist_dropdown, edit_dropdown]
|
| 356 |
-
)
|
| 357 |
-
|
| 358 |
-
load_btn.click(
|
| 359 |
-
fn=load_watchlist_for_edit,
|
| 360 |
-
inputs=[edit_dropdown],
|
| 361 |
-
outputs=[manage_name_input, manage_stocks_input]
|
| 362 |
-
)
|
| 363 |
-
|
| 364 |
-
if __name__ == "__main__":
|
| 365 |
-
demo.launch()
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import requests
|
| 6 |
+
import base64
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
|
| 9 |
+
# Load watchlist configuration
|
| 10 |
+
def load_watchlists():
|
| 11 |
+
"""Load watchlist configuration from JSON file"""
|
| 12 |
+
config_path = Path("watchlists_config.json")
|
| 13 |
+
if config_path.exists():
|
| 14 |
+
with open(config_path, 'r') as f:
|
| 15 |
+
return json.load(f)
|
| 16 |
+
return {}
|
| 17 |
+
|
| 18 |
+
def load_stocks_from_file(filename):
|
| 19 |
+
"""Load stock symbols from a watchlist file"""
|
| 20 |
+
file_path = Path(filename)
|
| 21 |
+
if file_path.exists():
|
| 22 |
+
with open(file_path, 'r') as f:
|
| 23 |
+
content = f.read().strip()
|
| 24 |
+
# Split by comma and clean whitespace
|
| 25 |
+
stocks = [s.strip() for s in content.split(',') if s.strip()]
|
| 26 |
+
return stocks
|
| 27 |
+
return []
|
| 28 |
+
|
| 29 |
+
def generate_chart_url(symbol, timeframe):
|
| 30 |
+
"""Generate StockCharts URL based on symbol and timeframe"""
|
| 31 |
+
base_url = "https://stockcharts.com/c-sc/sc?chart="
|
| 32 |
+
|
| 33 |
+
# Random parameter for cache busting
|
| 34 |
+
import random
|
| 35 |
+
rand_param = random.random()
|
| 36 |
+
|
| 37 |
+
if timeframe == "5-Minute":
|
| 38 |
+
return f"{base_url}{symbol},uu[900,700]iahayaca[bb][i!ub14!la12,26,9][pd20,2]&r={rand_param}"
|
| 39 |
+
elif timeframe == "Daily":
|
| 40 |
+
return f"{base_url}{symbol},uu[900,700]dahayaca[bb][i!ub14!lu][pd20,2]&r={rand_param}"
|
| 41 |
+
elif timeframe == "Weekly":
|
| 42 |
+
return f"{base_url}{symbol},uu[900,700]whhayaca[bb][i!ub14!lu][pd20,2]&r={rand_param}"
|
| 43 |
+
return ""
|
| 44 |
+
|
| 45 |
+
def download_chart_image(url):
|
| 46 |
+
"""Download chart image from StockCharts and convert to base64"""
|
| 47 |
+
try:
|
| 48 |
+
headers = {
|
| 49 |
+
'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',
|
| 50 |
+
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
| 51 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
| 52 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
| 53 |
+
'Referer': 'https://stockcharts.com/',
|
| 54 |
+
'Connection': 'keep-alive',
|
| 55 |
+
'Sec-Fetch-Dest': 'image',
|
| 56 |
+
'Sec-Fetch-Mode': 'no-cors',
|
| 57 |
+
'Sec-Fetch-Site': 'same-origin'
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
response = requests.get(url, headers=headers, timeout=10)
|
| 61 |
+
response.raise_for_status()
|
| 62 |
+
|
| 63 |
+
# Convert to base64
|
| 64 |
+
img_base64 = base64.b64encode(response.content).decode('utf-8')
|
| 65 |
+
return f"data:image/png;base64,{img_base64}"
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"Error downloading image from {url}: {str(e)}")
|
| 68 |
+
# Return a placeholder image URL or error message
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
def generate_single_stock_images(symbol):
|
| 72 |
+
"""Generate HTML for single stock analysis (3 charts)"""
|
| 73 |
+
timeframes = ["5-Minute", "Daily", "Weekly"]
|
| 74 |
+
|
| 75 |
+
html = f"<h2>股票分析: {symbol}</h2>"
|
| 76 |
+
html += '<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 20px;">'
|
| 77 |
+
|
| 78 |
+
for tf in timeframes:
|
| 79 |
+
url = generate_chart_url(symbol, tf)
|
| 80 |
+
img_data = download_chart_image(url)
|
| 81 |
+
|
| 82 |
+
if img_data:
|
| 83 |
+
html += f'''
|
| 84 |
+
<div style="text-align: center;">
|
| 85 |
+
<h3>{tf}</h3>
|
| 86 |
+
<img src="{img_data}" alt="{symbol} - {tf}" style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px;">
|
| 87 |
+
</div>
|
| 88 |
+
'''
|
| 89 |
+
else:
|
| 90 |
+
html += f'''
|
| 91 |
+
<div style="text-align: center;">
|
| 92 |
+
<h3>{tf}</h3>
|
| 93 |
+
<p style="color: red;">加载图片失败</p>
|
| 94 |
+
</div>
|
| 95 |
+
'''
|
| 96 |
+
|
| 97 |
+
html += '</div>'
|
| 98 |
+
return html
|
| 99 |
+
|
| 100 |
+
def update_single_stock(symbol):
|
| 101 |
+
"""Update single stock tab with charts"""
|
| 102 |
+
if not symbol or not symbol.strip():
|
| 103 |
+
return "<p>请输入股票代码</p>"
|
| 104 |
+
|
| 105 |
+
symbol = symbol.strip().upper()
|
| 106 |
+
return generate_single_stock_images(symbol)
|
| 107 |
+
|
| 108 |
+
def generate_watchlist_images(watchlist_name, timeframe):
|
| 109 |
+
"""Generate HTML for watchlist with selected timeframe"""
|
| 110 |
+
if not watchlist_name or watchlist_name == "Select a watchlist":
|
| 111 |
+
return "<p>请选择一个观察列表</p>"
|
| 112 |
+
|
| 113 |
+
# Load watchlist config
|
| 114 |
+
config = load_watchlists()
|
| 115 |
+
if watchlist_name not in config:
|
| 116 |
+
return f"<p>观察列表 '{watchlist_name}' 未找到</p>"
|
| 117 |
+
|
| 118 |
+
# Load stocks from file
|
| 119 |
+
filename = config[watchlist_name]
|
| 120 |
+
stocks = load_stocks_from_file(filename)
|
| 121 |
+
|
| 122 |
+
if not stocks:
|
| 123 |
+
return f"<p>文件 {filename} 中没有找到股票</p>"
|
| 124 |
+
|
| 125 |
+
# Generate HTML
|
| 126 |
+
html = f"<h2>观察列表: {watchlist_name} ({timeframe})</h2>"
|
| 127 |
+
html += '<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 20px;">'
|
| 128 |
+
|
| 129 |
+
for symbol in stocks:
|
| 130 |
+
url = generate_chart_url(symbol, timeframe)
|
| 131 |
+
img_data = download_chart_image(url)
|
| 132 |
+
|
| 133 |
+
if img_data:
|
| 134 |
+
html += f'''
|
| 135 |
+
<div style="text-align: center;">
|
| 136 |
+
<h3>{symbol}</h3>
|
| 137 |
+
<img src="{img_data}" alt="{symbol} - {timeframe}" style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px;">
|
| 138 |
+
</div>
|
| 139 |
+
'''
|
| 140 |
+
else:
|
| 141 |
+
html += f'''
|
| 142 |
+
<div style="text-align: center;">
|
| 143 |
+
<h3>{symbol}</h3>
|
| 144 |
+
<p style="color: red;">加载图片失败</p>
|
| 145 |
+
</div>
|
| 146 |
+
'''
|
| 147 |
+
|
| 148 |
+
html += '</div>'
|
| 149 |
+
return html
|
| 150 |
+
|
| 151 |
+
def get_watchlist_names():
|
| 152 |
+
"""Get list of watchlist names from config"""
|
| 153 |
+
config = load_watchlists()
|
| 154 |
+
return ["Select a watchlist"] + list(config.keys())
|
| 155 |
+
|
| 156 |
+
def save_watchlist_config(config):
|
| 157 |
+
"""Save watchlist configuration to JSON file"""
|
| 158 |
+
config_path = Path("watchlists_config.json")
|
| 159 |
+
with open(config_path, 'w') as f:
|
| 160 |
+
json.dump(config, f, indent=2)
|
| 161 |
+
|
| 162 |
+
def save_stocks_to_file(filename, stocks_text):
|
| 163 |
+
"""Save stock symbols to a watchlist file"""
|
| 164 |
+
file_path = Path(filename)
|
| 165 |
+
with open(file_path, 'w') as f:
|
| 166 |
+
f.write(stocks_text.strip())
|
| 167 |
+
|
| 168 |
+
def add_or_update_watchlist(watchlist_name, stocks_text):
|
| 169 |
+
"""Add a new watchlist or update an existing one"""
|
| 170 |
+
if not watchlist_name or not watchlist_name.strip():
|
| 171 |
+
return "❌ 请输入观察列表名称", gr.update(), gr.update()
|
| 172 |
+
|
| 173 |
+
watchlist_name = watchlist_name.strip()
|
| 174 |
+
|
| 175 |
+
if not stocks_text or not stocks_text.strip():
|
| 176 |
+
return "❌ 请输入股票代码", gr.update(), gr.update()
|
| 177 |
+
|
| 178 |
+
# Generate filename from watchlist name
|
| 179 |
+
filename = watchlist_name.lower().replace(' ', '_') + '.txt'
|
| 180 |
+
|
| 181 |
+
# Save stocks to file
|
| 182 |
+
save_stocks_to_file(filename, stocks_text)
|
| 183 |
+
|
| 184 |
+
# Update config
|
| 185 |
+
config = load_watchlists()
|
| 186 |
+
config[watchlist_name] = filename
|
| 187 |
+
save_watchlist_config(config)
|
| 188 |
+
|
| 189 |
+
# Get updated list for dropdown
|
| 190 |
+
updated_choices = get_watchlist_names()
|
| 191 |
+
|
| 192 |
+
return (
|
| 193 |
+
f"✅ 观察列表 '{watchlist_name}' 已保存!",
|
| 194 |
+
gr.update(choices=updated_choices),
|
| 195 |
+
gr.update(choices=list(config.keys()), value=None)
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def delete_watchlist(watchlist_name):
|
| 199 |
+
"""Delete a watchlist"""
|
| 200 |
+
if not watchlist_name:
|
| 201 |
+
return "❌ 请选择要删除的观察列表", gr.update(), gr.update()
|
| 202 |
+
|
| 203 |
+
config = load_watchlists()
|
| 204 |
+
|
| 205 |
+
if watchlist_name not in config:
|
| 206 |
+
return f"❌ 观察列表 '{watchlist_name}' 不存在", gr.update(), gr.update()
|
| 207 |
+
|
| 208 |
+
# Get filename and delete file
|
| 209 |
+
filename = config[watchlist_name]
|
| 210 |
+
file_path = Path(filename)
|
| 211 |
+
if file_path.exists():
|
| 212 |
+
file_path.unlink()
|
| 213 |
+
|
| 214 |
+
# Remove from config
|
| 215 |
+
del config[watchlist_name]
|
| 216 |
+
save_watchlist_config(config)
|
| 217 |
+
|
| 218 |
+
# Get updated list for dropdown
|
| 219 |
+
updated_choices = get_watchlist_names()
|
| 220 |
+
|
| 221 |
+
return (
|
| 222 |
+
f"✅ 观察列表 '{watchlist_name}' 已删除!",
|
| 223 |
+
gr.update(choices=updated_choices),
|
| 224 |
+
gr.update(choices=list(config.keys()) if config else [], value=None)
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
def load_watchlist_for_edit(watchlist_name):
|
| 228 |
+
"""Load watchlist content for editing"""
|
| 229 |
+
if not watchlist_name:
|
| 230 |
+
return "", ""
|
| 231 |
+
|
| 232 |
+
config = load_watchlists()
|
| 233 |
+
if watchlist_name not in config:
|
| 234 |
+
return "", ""
|
| 235 |
+
|
| 236 |
+
filename = config[watchlist_name]
|
| 237 |
+
stocks = load_stocks_from_file(filename)
|
| 238 |
+
stocks_text = ", ".join(stocks)
|
| 239 |
+
|
| 240 |
+
return watchlist_name, stocks_text
|
| 241 |
+
|
| 242 |
+
# Create Gradio interface
|
| 243 |
+
with gr.Blocks(title="股票分析仪表板") as demo:
|
| 244 |
+
gr.Markdown("# 股票分析仪表板")
|
| 245 |
+
|
| 246 |
+
with gr.Tabs():
|
| 247 |
+
# Tab 1: Single Stock Analysis
|
| 248 |
+
with gr.Tab("单股分析"):
|
| 249 |
+
gr.Markdown("### 输入股票代码查看图表")
|
| 250 |
+
|
| 251 |
+
with gr.Row():
|
| 252 |
+
stock_input = gr.Textbox(
|
| 253 |
+
label="股票代码",
|
| 254 |
+
placeholder="例如: QQQ, AAPL, SPY",
|
| 255 |
+
scale=3
|
| 256 |
+
)
|
| 257 |
+
analyze_btn = gr.Button("分析", scale=1)
|
| 258 |
+
|
| 259 |
+
single_stock_output = gr.HTML()
|
| 260 |
+
|
| 261 |
+
# Event handlers
|
| 262 |
+
analyze_btn.click(
|
| 263 |
+
fn=update_single_stock,
|
| 264 |
+
inputs=[stock_input],
|
| 265 |
+
outputs=[single_stock_output]
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
stock_input.submit(
|
| 269 |
+
fn=update_single_stock,
|
| 270 |
+
inputs=[stock_input],
|
| 271 |
+
outputs=[single_stock_output]
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Tab 2: Watchlist
|
| 275 |
+
with gr.Tab("观察列表"):
|
| 276 |
+
gr.Markdown("### 选择观察列表和时间周期查看所有图表")
|
| 277 |
+
|
| 278 |
+
with gr.Row():
|
| 279 |
+
watchlist_dropdown = gr.Dropdown(
|
| 280 |
+
choices=get_watchlist_names(),
|
| 281 |
+
value="Select a watchlist",
|
| 282 |
+
label="选择观察列表",
|
| 283 |
+
scale=2
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
timeframe_dropdown = gr.Dropdown(
|
| 287 |
+
choices=["5-Minute", "Daily", "Weekly"],
|
| 288 |
+
value="Daily",
|
| 289 |
+
label="选择时间周期",
|
| 290 |
+
scale=1
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
watchlist_output = gr.HTML()
|
| 294 |
+
|
| 295 |
+
# Event handlers - update on any dropdown change
|
| 296 |
+
watchlist_dropdown.change(
|
| 297 |
+
fn=generate_watchlist_images,
|
| 298 |
+
inputs=[watchlist_dropdown, timeframe_dropdown],
|
| 299 |
+
outputs=[watchlist_output]
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
timeframe_dropdown.change(
|
| 303 |
+
fn=generate_watchlist_images,
|
| 304 |
+
inputs=[watchlist_dropdown, timeframe_dropdown],
|
| 305 |
+
outputs=[watchlist_output]
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Tab 3: Manage Watchlists
|
| 309 |
+
with gr.Tab("管理观察列表"):
|
| 310 |
+
gr.Markdown("### 添加、修改或删除观察列表")
|
| 311 |
+
|
| 312 |
+
with gr.Row():
|
| 313 |
+
with gr.Column():
|
| 314 |
+
gr.Markdown("#### 添加/修改观察列表")
|
| 315 |
+
|
| 316 |
+
manage_name_input = gr.Textbox(
|
| 317 |
+
label="观察列表名称",
|
| 318 |
+
placeholder="例如: Tech Stocks"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
manage_stocks_input = gr.TextArea(
|
| 322 |
+
label="股票代码(逗号分隔)",
|
| 323 |
+
placeholder="例如: AAPL, MSFT, GOOGL, NVDA",
|
| 324 |
+
lines=5
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
with gr.Row():
|
| 328 |
+
save_btn = gr.Button("💾 保存", variant="primary")
|
| 329 |
+
|
| 330 |
+
save_status = gr.Markdown()
|
| 331 |
+
|
| 332 |
+
with gr.Column():
|
| 333 |
+
gr.Markdown("#### 编辑/删除现有列表")
|
| 334 |
+
|
| 335 |
+
edit_dropdown = gr.Dropdown(
|
| 336 |
+
choices=list(load_watchlists().keys()),
|
| 337 |
+
label="选择要编辑的观察列表",
|
| 338 |
+
value=None
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
with gr.Row():
|
| 342 |
+
load_btn = gr.Button("📝 加载到左侧编辑")
|
| 343 |
+
delete_btn = gr.Button("🗑️ 删除", variant="stop")
|
| 344 |
+
|
| 345 |
+
# Event handlers for management tab
|
| 346 |
+
save_btn.click(
|
| 347 |
+
fn=add_or_update_watchlist,
|
| 348 |
+
inputs=[manage_name_input, manage_stocks_input],
|
| 349 |
+
outputs=[save_status, watchlist_dropdown, edit_dropdown]
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
delete_btn.click(
|
| 353 |
+
fn=delete_watchlist,
|
| 354 |
+
inputs=[edit_dropdown],
|
| 355 |
+
outputs=[save_status, watchlist_dropdown, edit_dropdown]
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
load_btn.click(
|
| 359 |
+
fn=load_watchlist_for_edit,
|
| 360 |
+
inputs=[edit_dropdown],
|
| 361 |
+
outputs=[manage_name_input, manage_stocks_input]
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
if __name__ == "__main__":
|
| 365 |
+
demo.launch()
|