bryt666 commited on
Commit
4c80282
·
verified ·
1 Parent(s): 36ad6e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +365 -365
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[700,500]iahayaca[bb][i!ub14!la12,26,9][pd20,2]&r={rand_param}"
39
- elif timeframe == "Daily":
40
- return f"{base_url}{symbol},uu[700,500]dahayaca[bb][i!ub14!lu][pd20,2]&r={rand_param}"
41
- elif timeframe == "Weekly":
42
- return f"{base_url}{symbol},uu[700,500]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()
 
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()