gitdeem commited on
Commit
f5d52f6
·
verified ·
1 Parent(s): bfeefc8

Upload 32 files

Browse files
.env ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API 提供商 (openai SDK)
2
+ API_PROVIDER=openai # Gemini 未适配,暂时只支持openai SDK,new-api解决方案
3
+
4
+ # OpenAI API 配置
5
+ #OPENAI_API_URL=https://***/v1
6
+ #OPENAI_API_KEY=your_openai_api_key
7
+ OPENAI_API_MODEL=comic-c
8
+ NEWS_MODEL=gpt-4o
9
+ #SERP_API_KEY=your_serp_api_key
10
+
11
+ # Gemini API 配置
12
+ # GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent
13
+ # GEMINI_API_KEY=your_gemini_api_key
14
+ # GEMINI_API_MODEL=gemini-pro
15
+
16
+ # 安全配置
17
+ # API_KEY=your_api_key_for_protected_endpoints
18
+ # HMAC_SECRET=your_hmac_secret_key_for_webhook_verification
19
+ # ALLOWED_ORIGINS=http://localhost:8888,https://your-domain.com
20
+
21
+ # Redis缓存设置(可选)
22
+ # REDIS_URL=redis://redis:6379 #docker配置
23
+ REDIS_URL=redis://localhost:6379
24
+ USE_REDIS_CACHE=False
25
+
26
+ # 数据库设置(可选)
27
+ # DATABASE_URL=sqlite:///app/data/stock_analyzer.db #docker配置
28
+ DATABASE_URL=sqlite:///data/stock_analyzer.db
29
+ USE_DATABASE=False
30
+
31
+ # 日志配置
32
+ LOG_LEVEL=INFO
33
+ LOG_FILE=logs/stock_analyzer.log
34
+
35
+ # API_KEY=UZXJfw3YNX80DLfN
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/favicon.ico filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,8 +1,32 @@
1
- FROM lanzhihong/stock-scanner:latest
2
-
3
- ENV TZ=Asia/Shanghai
4
- ENV API_TIMEOUT=60
5
-
6
- ENV OPENAI_API_MODEL=
7
-
8
- RUN mkdir /app/logs && chmod 777 /app/logs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用Python 3.11基础镜像(因为你的依赖包兼容性更好)
2
+ FROM python:3.11-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 创建数据和日志目录
8
+ RUN mkdir -p /app/data /app/logs
9
+
10
+ # 设置环境变量
11
+ ENV PYTHONUNBUFFERED=1 \
12
+ PYTHONDONTWRITEBYTECODE=1
13
+
14
+ # 安装系统依赖
15
+ RUN apt-get update && apt-get install -y --no-install-recommends \
16
+ build-essential \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # 复制requirements.txt
20
+ COPY requirements.txt .
21
+
22
+ # 安装Python依赖
23
+ RUN pip install --no-cache-dir -r requirements.txt
24
+
25
+ # 复制应用代码
26
+ COPY . .
27
+
28
+ # 暴露端口(假设Flask应用运行在5000端口)
29
+ EXPOSE 8888
30
+
31
+ # 使用gunicorn启动应用
32
+ CMD ["gunicorn", "--bind", "0.0.0.0:8888", "--workers", "4", "web_server:app"]
auth_middleware.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import wraps
2
+ from flask import request, jsonify
3
+ import os
4
+ import time
5
+ import hashlib
6
+ import hmac
7
+
8
+
9
+ def get_api_key():
10
+ return os.getenv('API_KEY', 'UZXJfw3YNX80DLfN')
11
+
12
+
13
+ def require_api_key(f):
14
+ """需要API密钥验证的装饰器"""
15
+ @wraps(f)
16
+ def decorated_function(*args, **kwargs):
17
+ api_key = request.headers.get('X-API-Key')
18
+ if not api_key:
19
+ return jsonify({'error': '缺少API密钥'}), 401
20
+
21
+ if api_key != get_api_key():
22
+ return jsonify({'error': '无效的API密钥'}), 403
23
+
24
+ return f(*args, **kwargs)
25
+ return decorated_function
26
+
27
+
28
+ def generate_hmac_signature(data, secret_key=None):
29
+ if secret_key is None:
30
+ secret_key = os.getenv('HMAC_SECRET', 'default_hmac_secret_for_development')
31
+
32
+ if isinstance(data, dict):
33
+ # 对字典进行排序,确保相同的数据产生相同的签名
34
+ data = '&'.join(f"{k}={v}" for k, v in sorted(data.items()))
35
+
36
+ # 使用HMAC-SHA256生成签名
37
+ signature = hmac.new(
38
+ secret_key.encode(),
39
+ data.encode(),
40
+ hashlib.sha256
41
+ ).hexdigest()
42
+
43
+ return signature
44
+
45
+
46
+ def verify_hmac_signature(request_signature, data, secret_key=None):
47
+ expected_signature = generate_hmac_signature(data, secret_key)
48
+ return hmac.compare_digest(request_signature, expected_signature)
49
+
50
+
51
+ def require_hmac_auth(f):
52
+ """需要HMAC认证的装饰器"""
53
+ @wraps(f)
54
+ def decorated_function(*args, **kwargs):
55
+ request_signature = request.headers.get('X-HMAC-Signature')
56
+ if not request_signature:
57
+ return jsonify({'error': '缺少HMAC签名'}), 401
58
+
59
+ # 获取请求数据
60
+ data = request.get_json(silent=True) or {}
61
+
62
+ # 添加时间戳防止重放攻击
63
+ timestamp = request.headers.get('X-Timestamp')
64
+ if not timestamp:
65
+ return jsonify({'error': '缺少时间戳'}), 401
66
+
67
+ # 验证时间戳有效性(有效期5分钟)
68
+ current_time = int(time.time())
69
+ if abs(current_time - int(timestamp)) > 300:
70
+ return jsonify({'error': '时间戳已过期'}), 401
71
+
72
+ # 将时间戳加入验证数据
73
+ verification_data = {**data, 'timestamp': timestamp}
74
+
75
+ # 验证签名
76
+ if not verify_hmac_signature(request_signature, verification_data):
77
+ return jsonify({'error': '签名无效'}), 403
78
+ return f(*args, **kwargs)
79
+ return decorated_function
capital_flow_analyzer.py ADDED
@@ -0,0 +1,588 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # capital_flow_analyzer.py
2
+ import logging
3
+ import traceback
4
+ import akshare as ak
5
+ import pandas as pd
6
+ import numpy as np
7
+ from datetime import datetime, timedelta
8
+
9
+
10
+ class CapitalFlowAnalyzer:
11
+ def __init__(self):
12
+ self.data_cache = {}
13
+
14
+ # 设置日志记录
15
+ logging.basicConfig(level=logging.INFO,
16
+ format='%(asctime)s - %(levelname)s - %(message)s')
17
+ self.logger = logging.getLogger(__name__)
18
+
19
+ def get_concept_fund_flow(self, period="10日排行"):
20
+ """获取概念/行业资金流向数据"""
21
+ try:
22
+ self.logger.info(f"Getting concept fund flow for period: {period}")
23
+
24
+ # 检查缓存
25
+ cache_key = f"concept_fund_flow_{period}"
26
+ if cache_key in self.data_cache:
27
+ cache_time, cached_data = self.data_cache[cache_key]
28
+ # 如果在最近一小时内有缓存数据,则返回缓存数据
29
+ if (datetime.now() - cache_time).total_seconds() < 3600:
30
+ return cached_data
31
+
32
+ # 从akshare获取数据
33
+ concept_data = ak.stock_fund_flow_concept(symbol=period)
34
+
35
+ # 处理数据
36
+ result = []
37
+ for _, row in concept_data.iterrows():
38
+ try:
39
+ # 列名可能有所不同,所以我们使用灵活的方法
40
+ item = {
41
+ "rank": int(row.get("序号", 0)),
42
+ "sector": row.get("行业", ""),
43
+ "company_count": int(row.get("公司家数", 0)),
44
+ "sector_index": float(row.get("行业指数", 0)),
45
+ "change_percent": self._parse_percent(row.get("阶段涨跌幅", "0%")),
46
+ "inflow": float(row.get("流入资金", 0)),
47
+ "outflow": float(row.get("流出资金", 0)),
48
+ "net_flow": float(row.get("净额", 0))
49
+ }
50
+ result.append(item)
51
+ except Exception as e:
52
+ # self.logger.warning(f"Error processing row in concept fund flow: {str(e)}")
53
+ continue
54
+
55
+ # 缓存结果
56
+ self.data_cache[cache_key] = (datetime.now(), result)
57
+
58
+ return result
59
+ except Exception as e:
60
+ self.logger.error(f"Error getting concept fund flow: {str(e)}")
61
+ self.logger.error(traceback.format_exc())
62
+ # 如果API调用失败则返回模拟数据
63
+ return self._generate_mock_concept_fund_flow(period)
64
+
65
+ def get_individual_fund_flow_rank(self, period="10日"):
66
+ """获取个股资金流向排名"""
67
+ try:
68
+ self.logger.info(f"Getting individual fund flow ranking for period: {period}")
69
+
70
+ # 检查缓存
71
+ cache_key = f"individual_fund_flow_rank_{period}"
72
+ if cache_key in self.data_cache:
73
+ cache_time, cached_data = self.data_cache[cache_key]
74
+ # 如果在最近一小时内有缓存数据,则返回缓存数据
75
+ if (datetime.now() - cache_time).total_seconds() < 3600:
76
+ return cached_data
77
+
78
+ # 从akshare获取数据
79
+ stock_data = ak.stock_individual_fund_flow_rank(indicator=period)
80
+
81
+ # 处理数据
82
+ result = []
83
+ for _, row in stock_data.iterrows():
84
+ try:
85
+ # 根据不同时间段设置列名前缀
86
+ period_prefix = "" if period == "今日" else f"{period}"
87
+
88
+ item = {
89
+ "rank": int(row.get("序号", 0)),
90
+ "code": row.get("代码", ""),
91
+ "name": row.get("名称", ""),
92
+ "price": float(row.get("最新价", 0)),
93
+ "change_percent": float(row.get(f"{period_prefix}涨跌幅", 0)),
94
+ "main_net_inflow": float(row.get(f"{period_prefix}主力净流入-净额", 0)),
95
+ "main_net_inflow_percent": float(row.get(f"{period_prefix}主力净流入-净占比", 0)),
96
+ "super_large_net_inflow": float(row.get(f"{period_prefix}超大单净流入-净额", 0)),
97
+ "super_large_net_inflow_percent": float(row.get(f"{period_prefix}超大单净流入-净占比", 0)),
98
+ "large_net_inflow": float(row.get(f"{period_prefix}大单净流入-净额", 0)),
99
+ "large_net_inflow_percent": float(row.get(f"{period_prefix}大单净流入-净占比", 0)),
100
+ "medium_net_inflow": float(row.get(f"{period_prefix}中单净流入-净额", 0)),
101
+ "medium_net_inflow_percent": float(row.get(f"{period_prefix}中单净流入-净占比", 0)),
102
+ "small_net_inflow": float(row.get(f"{period_prefix}小单净流入-净额", 0)),
103
+ "small_net_inflow_percent": float(row.get(f"{period_prefix}小单净流入-净占比", 0))
104
+ }
105
+ result.append(item)
106
+ except Exception as e:
107
+ self.logger.warning(f"Error processing row in individual fund flow rank: {str(e)}")
108
+ continue
109
+
110
+ # 缓存结果
111
+ self.data_cache[cache_key] = (datetime.now(), result)
112
+
113
+ return result
114
+ except Exception as e:
115
+ self.logger.error(f"Error getting individual fund flow ranking: {str(e)}")
116
+ self.logger.error(traceback.format_exc())
117
+ # 如果API调用失败则返回模拟数据
118
+ return self._generate_mock_individual_fund_flow_rank(period)
119
+
120
+ def get_individual_fund_flow(self, stock_code, market_type="", re_date="10日"):
121
+ """获取个股资金流向数据"""
122
+ try:
123
+ self.logger.info(f"Getting fund flow for stock: {stock_code}, market: {market_type}")
124
+
125
+ # 检查缓存
126
+ cache_key = f"individual_fund_flow_{stock_code}_{market_type}"
127
+ if cache_key in self.data_cache:
128
+ cache_time, cached_data = self.data_cache[cache_key]
129
+ # 如果在一小时内有缓存数据,则返回缓存数据
130
+ if (datetime.now() - cache_time).total_seconds() < 3600:
131
+ return cached_data
132
+
133
+ # 如果未提供市场类型,则根据股票代码判断
134
+ if not market_type:
135
+ if stock_code.startswith('6'):
136
+ market_type = "sh"
137
+ elif stock_code.startswith('0') or stock_code.startswith('3'):
138
+ market_type = "sz"
139
+ else:
140
+ market_type = "sh" # Default to Shanghai
141
+
142
+ # 从akshare获取数据
143
+ flow_data = ak.stock_individual_fund_flow(stock=stock_code, market=market_type)
144
+
145
+ # 处理数据
146
+ result = {
147
+ "stock_code": stock_code,
148
+ "data": []
149
+ }
150
+
151
+ for _, row in flow_data.iterrows():
152
+ try:
153
+ item = {
154
+ "date": row.get("日期", ""),
155
+ "price": float(row.get("收盘价", 0)),
156
+ "change_percent": float(row.get("涨跌幅", 0)),
157
+ "main_net_inflow": float(row.get("主力净流入-净额", 0)),
158
+ "main_net_inflow_percent": float(row.get("主力净流入-净占比", 0)),
159
+ "super_large_net_inflow": float(row.get("超大单净流入-净额", 0)),
160
+ "super_large_net_inflow_percent": float(row.get("超大单净流入-净占比", 0)),
161
+ "large_net_inflow": float(row.get("大单净流入-净额", 0)),
162
+ "large_net_inflow_percent": float(row.get("大单净流入-净占比", 0)),
163
+ "medium_net_inflow": float(row.get("中单净流入-净额", 0)),
164
+ "medium_net_inflow_percent": float(row.get("中单净流入-净占比", 0)),
165
+ "small_net_inflow": float(row.get("小单净流入-净额", 0)),
166
+ "small_net_inflow_percent": float(row.get("小单净流入-净占比", 0))
167
+ }
168
+ result["data"].append(item)
169
+ except Exception as e:
170
+ self.logger.warning(f"Error processing row in individual fund flow: {str(e)}")
171
+ continue
172
+
173
+ # 计算汇总统计数据
174
+ if result["data"]:
175
+ # 最近数据 (最近10天)
176
+ recent_data = result["data"][:min(10, len(result["data"]))]
177
+
178
+ result["summary"] = {
179
+ "recent_days": len(recent_data),
180
+ "total_main_net_inflow": sum(item["main_net_inflow"] for item in recent_data),
181
+ "avg_main_net_inflow_percent": np.mean(
182
+ [item["main_net_inflow_percent"] for item in recent_data]),
183
+ "positive_days": sum(1 for item in recent_data if item["main_net_inflow"] > 0),
184
+ "negative_days": sum(1 for item in recent_data if item["main_net_inflow"] <= 0)
185
+ }
186
+
187
+ # Cache the result
188
+ self.data_cache[cache_key] = (datetime.now(), result)
189
+
190
+ return result
191
+ except Exception as e:
192
+ self.logger.error(f"Error getting individual fund flow: {str(e)}")
193
+ self.logger.error(traceback.format_exc())
194
+ # 如果API调用失败则返回模拟数据
195
+ return self._generate_mock_individual_fund_flow(stock_code, market_type)
196
+
197
+ def get_sector_stocks(self, sector):
198
+ """获取特定行业的股票"""
199
+ try:
200
+ self.logger.info(f"Getting stocks for sector: {sector}")
201
+
202
+ # 检查缓存
203
+ cache_key = f"sector_stocks_{sector}"
204
+ if cache_key in self.data_cache:
205
+ cache_time, cached_data = self.data_cache[cache_key]
206
+ # 如果在一小时内有缓存数据,则返回缓存数据
207
+ if (datetime.now() - cache_time).total_seconds() < 3600:
208
+ return cached_data
209
+
210
+ # 尝试从akshare获取数据
211
+ try:
212
+ # For industry sectors (using 东方财富 interface)
213
+ stocks = ak.stock_board_industry_cons_em(symbol=sector)
214
+
215
+ # 提取股票列表
216
+ if not stocks.empty and '代码' in stocks.columns:
217
+ result = []
218
+ for _, row in stocks.iterrows():
219
+ try:
220
+ item = {
221
+ "code": row.get("代码", ""),
222
+ "name": row.get("名称", ""),
223
+ "price": float(row.get("最新价", 0)),
224
+ "change_percent": float(row.get("涨跌幅", 0)) if "涨跌幅" in row else 0,
225
+ "main_net_inflow": 0, # We'll get this data separately if needed
226
+ "main_net_inflow_percent": 0 # We'll get this data separately if needed
227
+ }
228
+ result.append(item)
229
+ except Exception as e:
230
+ # self.logger.warning(f"Error processing row in sector stocks: {str(e)}")
231
+ continue
232
+
233
+ # 缓存结果
234
+ self.data_cache[cache_key] = (datetime.now(), result)
235
+ return result
236
+ except Exception as e:
237
+ self.logger.warning(f"Failed to get sector stocks from API: {str(e)}")
238
+ # 降级到模拟数据
239
+
240
+ # 如果到达这里,说明无法从API获取数据,返回模拟数据
241
+ result = self._generate_mock_sector_stocks(sector)
242
+ self.data_cache[cache_key] = (datetime.now(), result)
243
+ return result
244
+
245
+ except Exception as e:
246
+ self.logger.error(f"Error getting sector stocks: {str(e)}")
247
+ self.logger.error(traceback.format_exc())
248
+ # 如果API调用失败则返回模拟数据
249
+ return self._generate_mock_sector_stocks(sector)
250
+
251
+ def calculate_capital_flow_score(self, stock_code, market_type=""):
252
+ """计算股票资金流向评分"""
253
+ try:
254
+ self.logger.info(f"Calculating capital flow score for stock: {stock_code}")
255
+
256
+ # 获取个股资金流向数据
257
+ fund_flow = self.get_individual_fund_flow(stock_code, market_type)
258
+
259
+ if not fund_flow or not fund_flow.get("data") or not fund_flow.get("summary"):
260
+ return {
261
+ "total": 0,
262
+ "main_force": 0,
263
+ "large_order": 0,
264
+ "small_order": 0,
265
+ "details": {}
266
+ }
267
+
268
+ # Extract summary statistics
269
+ summary = fund_flow["summary"]
270
+ recent_days = summary["recent_days"]
271
+ total_main_net_inflow = summary["total_main_net_inflow"]
272
+ avg_main_net_inflow_percent = summary["avg_main_net_inflow_percent"]
273
+ positive_days = summary["positive_days"]
274
+
275
+ # Calculate main force score (0-40)
276
+ main_force_score = 0
277
+
278
+ # 基于净流入百分比的评分
279
+ if avg_main_net_inflow_percent > 3:
280
+ main_force_score += 20
281
+ elif avg_main_net_inflow_percent > 1:
282
+ main_force_score += 15
283
+ elif avg_main_net_inflow_percent > 0:
284
+ main_force_score += 10
285
+
286
+ # 基于上涨天数的评分
287
+ positive_ratio = positive_days / recent_days if recent_days > 0 else 0
288
+ if positive_ratio > 0.7:
289
+ main_force_score += 20
290
+ elif positive_ratio > 0.5:
291
+ main_force_score += 15
292
+ elif positive_ratio > 0.3:
293
+ main_force_score += 10
294
+
295
+ # 计算大单评分(0-30分)
296
+ large_order_score = 0
297
+
298
+ # 分析超大单和大单交易
299
+ recent_super_large = [item["super_large_net_inflow"] for item in
300
+ fund_flow["data"][:recent_days]]
301
+ recent_large = [item["large_net_inflow"] for item in fund_flow["data"][:recent_days]]
302
+
303
+ super_large_positive = sum(1 for x in recent_super_large if x > 0)
304
+ large_positive = sum(1 for x in recent_large if x > 0)
305
+
306
+ # 基于超大单的评分
307
+ super_large_ratio = super_large_positive / recent_days if recent_days > 0 else 0
308
+ if super_large_ratio > 0.7:
309
+ large_order_score += 15
310
+ elif super_large_ratio > 0.5:
311
+ large_order_score += 10
312
+ elif super_large_ratio > 0.3:
313
+ large_order_score += 5
314
+
315
+ # 基于大单的评分
316
+ large_ratio = large_positive / recent_days if recent_days > 0 else 0
317
+ if large_ratio > 0.7:
318
+ large_order_score += 15
319
+ elif large_ratio > 0.5:
320
+ large_order_score += 10
321
+ elif large_ratio > 0.3:
322
+ large_order_score += 5
323
+
324
+ # 计算小单评分(0-30分)
325
+ small_order_score = 0
326
+
327
+ # 分析中单和小单交易
328
+ recent_medium = [item["medium_net_inflow"] for item in fund_flow["data"][:recent_days]]
329
+ recent_small = [item["small_net_inflow"] for item in fund_flow["data"][:recent_days]]
330
+
331
+ medium_positive = sum(1 for x in recent_medium if x > 0)
332
+ small_positive = sum(1 for x in recent_small if x > 0)
333
+
334
+ # 基于中单的评分
335
+ medium_ratio = medium_positive / recent_days if recent_days > 0 else 0
336
+ if medium_ratio > 0.7:
337
+ small_order_score += 15
338
+ elif medium_ratio > 0.5:
339
+ small_order_score += 10
340
+ elif medium_ratio > 0.3:
341
+ small_order_score += 5
342
+
343
+ # 基于小单的评分
344
+ small_ratio = small_positive / recent_days if recent_days > 0 else 0
345
+ if small_ratio > 0.7:
346
+ small_order_score += 15
347
+ elif small_ratio > 0.5:
348
+ small_order_score += 10
349
+ elif small_ratio > 0.3:
350
+ small_order_score += 5
351
+
352
+ # 计算总评分
353
+ total_score = main_force_score + large_order_score + small_order_score
354
+
355
+ return {
356
+ "total": total_score,
357
+ "main_force": main_force_score,
358
+ "large_order": large_order_score,
359
+ "small_order": small_order_score,
360
+ "details": fund_flow
361
+ }
362
+ except Exception as e:
363
+ self.logger.error(f"Error calculating capital flow score: {str(e)}")
364
+ self.logger.error(traceback.format_exc())
365
+ return {
366
+ "total": 0,
367
+ "main_force": 0,
368
+ "large_order": 0,
369
+ "small_order": 0,
370
+ "details": {},
371
+ "error": str(e)
372
+ }
373
+
374
+ def _parse_percent(self, percent_str):
375
+ """将百分比字符串转换为浮点数"""
376
+ try:
377
+ if isinstance(percent_str, str) and '%' in percent_str:
378
+ return float(percent_str.replace('%', ''))
379
+ return float(percent_str)
380
+ except (ValueError, TypeError):
381
+ return 0.0
382
+
383
+ def _generate_mock_concept_fund_flow(self, period):
384
+ """生成模拟概念资金流向数据"""
385
+ # self.logger.warning(f"Generating mock concept fund flow data for period: {period}")
386
+
387
+ sectors = [
388
+ "新能源", "医药", "半导体", "芯片", "人工智能", "大数据", "云计算", "5G",
389
+ "汽车", "消费", "金融", "互联网", "游戏", "农业", "化工", "建筑", "军工",
390
+ "钢铁", "有色金属", "煤炭", "石油"
391
+ ]
392
+
393
+ result = []
394
+ for i, sector in enumerate(sectors):
395
+ # 随机数据 - 前半部分为正,后半部分为负
396
+ is_positive = i < len(sectors) // 2
397
+
398
+ inflow = round(np.random.uniform(10, 50), 2) if is_positive else round(
399
+ np.random.uniform(5, 20), 2)
400
+ outflow = round(np.random.uniform(5, 20), 2) if is_positive else round(
401
+ np.random.uniform(10, 50), 2)
402
+ net_flow = round(inflow - outflow, 2)
403
+
404
+ change_percent = round(np.random.uniform(0, 5), 2) if is_positive else round(
405
+ np.random.uniform(-5, 0), 2)
406
+
407
+ item = {
408
+ "rank": i + 1,
409
+ "sector": sector,
410
+ "company_count": np.random.randint(10, 100),
411
+ "sector_index": round(np.random.uniform(1000, 5000), 2),
412
+ "change_percent": change_percent,
413
+ "inflow": inflow,
414
+ "outflow": outflow,
415
+ "net_flow": net_flow
416
+ }
417
+ result.append(item)
418
+
419
+ # 按净流入降序排序
420
+ return sorted(result, key=lambda x: x["net_flow"], reverse=True)
421
+
422
+ def _generate_mock_individual_fund_flow_rank(self, period):
423
+ """生成模拟个股资金流向排名数据"""
424
+ # self.logger.warning(f"Generating mock individual fund flow ranking data for period: {period}")
425
+
426
+ # Sample stock data
427
+ stocks = [
428
+ {"code": "600000", "name": "浦发银行"}, {"code": "600036", "name": "招商银行"},
429
+ {"code": "601318", "name": "中国平安"}, {"code": "600519", "name": "贵州茅台"},
430
+ {"code": "000858", "name": "五粮液"}, {"code": "000333", "name": "美的集团"},
431
+ {"code": "600276", "name": "恒瑞医药"}, {"code": "601888", "name": "中国中免"},
432
+ {"code": "600030", "name": "中信证券"}, {"code": "601166", "name": "兴业银行"},
433
+ {"code": "600887", "name": "伊利股份"}, {"code": "601398", "name": "工商银行"},
434
+ {"code": "600028", "name": "中国石化"}, {"code": "601988", "name": "中国银行"},
435
+ {"code": "601857", "name": "中国石油"}, {"code": "600019", "name": "宝钢股份"},
436
+ {"code": "600050", "name": "中国联通"}, {"code": "601328", "name": "交通银行"},
437
+ {"code": "601668", "name": "中国建筑"}, {"code": "601288", "name": "农业银行"}
438
+ ]
439
+
440
+ result = []
441
+ for i, stock in enumerate(stocks):
442
+ # 随机数据 - 前半部分为正,后半部分为负
443
+ is_positive = i < len(stocks) // 2
444
+
445
+ main_net_inflow = round(np.random.uniform(1e6, 5e7), 2) if is_positive else round(
446
+ np.random.uniform(-5e7, -1e6), 2)
447
+ main_net_inflow_percent = round(np.random.uniform(1, 10), 2) if is_positive else round(
448
+ np.random.uniform(-10, -1), 2)
449
+
450
+ super_large_net_inflow = round(main_net_inflow * np.random.uniform(0.3, 0.5), 2)
451
+ super_large_net_inflow_percent = round(main_net_inflow_percent * np.random.uniform(0.3, 0.5), 2)
452
+
453
+ large_net_inflow = round(main_net_inflow * np.random.uniform(0.3, 0.5), 2)
454
+ large_net_inflow_percent = round(main_net_inflow_percent * np.random.uniform(0.3, 0.5), 2)
455
+
456
+ medium_net_inflow = round(np.random.uniform(-1e6, 1e6), 2)
457
+ medium_net_inflow_percent = round(np.random.uniform(-2, 2), 2)
458
+
459
+ small_net_inflow = round(np.random.uniform(-1e6, 1e6), 2)
460
+ small_net_inflow_percent = round(np.random.uniform(-2, 2), 2)
461
+
462
+ change_percent = round(np.random.uniform(0, 5), 2) if is_positive else round(np.random.uniform(-5, 0), 2)
463
+
464
+ item = {
465
+ "rank": i + 1,
466
+ "code": stock["code"],
467
+ "name": stock["name"],
468
+ "price": round(np.random.uniform(10, 100), 2),
469
+ "change_percent": change_percent,
470
+ "main_net_inflow": main_net_inflow,
471
+ "main_net_inflow_percent": main_net_inflow_percent,
472
+ "super_large_net_inflow": super_large_net_inflow,
473
+ "super_large_net_inflow_percent": super_large_net_inflow_percent,
474
+ "large_net_inflow": large_net_inflow,
475
+ "large_net_inflow_percent": large_net_inflow_percent,
476
+ "medium_net_inflow": medium_net_inflow,
477
+ "medium_net_inflow_percent": medium_net_inflow_percent,
478
+ "small_net_inflow": small_net_inflow,
479
+ "small_net_inflow_percent": small_net_inflow_percent
480
+ }
481
+ result.append(item)
482
+
483
+ # 按主力净流入降序排序
484
+ return sorted(result, key=lambda x: x["main_net_inflow"], reverse=True)
485
+
486
+ def _generate_mock_individual_fund_flow(self, stock_code, market_type):
487
+ """生成模拟个股资金流向数据"""
488
+ # self.logger.warning(f"Generating mock individual fund flow data for stock: {stock_code}")
489
+
490
+ # 生成30天的模拟数据
491
+ end_date = datetime.now()
492
+
493
+ result = {
494
+ "stock_code": stock_code,
495
+ "data": []
496
+ }
497
+
498
+ # 创建模拟价格趋势(使用合理的随机游走)
499
+ base_price = np.random.uniform(10, 100)
500
+ current_price = base_price
501
+
502
+ for i in range(30):
503
+ date = (end_date - timedelta(days=i)).strftime('%Y-%m-%d')
504
+
505
+ # 随机价格变化(-2%到+2%)
506
+ change_percent = np.random.uniform(-2, 2)
507
+ price = round(current_price * (1 + change_percent / 100), 2)
508
+ current_price = price
509
+
510
+ # 随机资金流向数据,与价格变化有一定相关性
511
+ is_positive = change_percent > 0
512
+
513
+ main_net_inflow = round(np.random.uniform(1e5, 5e6), 2) if is_positive else round(
514
+ np.random.uniform(-5e6, -1e5), 2)
515
+ main_net_inflow_percent = round(np.random.uniform(1, 5), 2) if is_positive else round(
516
+ np.random.uniform(-5, -1), 2)
517
+
518
+ super_large_net_inflow = round(main_net_inflow * np.random.uniform(0.3, 0.5), 2)
519
+ super_large_net_inflow_percent = round(main_net_inflow_percent * np.random.uniform(0.3, 0.5), 2)
520
+
521
+ large_net_inflow = round(main_net_inflow * np.random.uniform(0.3, 0.5), 2)
522
+ large_net_inflow_percent = round(main_net_inflow_percent * np.random.uniform(0.3, 0.5), 2)
523
+
524
+ medium_net_inflow = round(np.random.uniform(-1e5, 1e5), 2)
525
+ medium_net_inflow_percent = round(np.random.uniform(-2, 2), 2)
526
+
527
+ small_net_inflow = round(np.random.uniform(-1e5, 1e5), 2)
528
+ small_net_inflow_percent = round(np.random.uniform(-2, 2), 2)
529
+
530
+ item = {
531
+ "date": date,
532
+ "price": price,
533
+ "change_percent": round(change_percent, 2),
534
+ "main_net_inflow": main_net_inflow,
535
+ "main_net_inflow_percent": main_net_inflow_percent,
536
+ "super_large_net_inflow": super_large_net_inflow,
537
+ "super_large_net_inflow_percent": super_large_net_inflow_percent,
538
+ "large_net_inflow": large_net_inflow,
539
+ "large_net_inflow_percent": large_net_inflow_percent,
540
+ "medium_net_inflow": medium_net_inflow,
541
+ "medium_net_inflow_percent": medium_net_inflow_percent,
542
+ "small_net_inflow": small_net_inflow,
543
+ "small_net_inflow_percent": small_net_inflow_percent
544
+ }
545
+ result["data"].append(item)
546
+
547
+ # 按日期降序排序(最新的在前)
548
+ result["data"].sort(key=lambda x: x["date"], reverse=True)
549
+
550
+ # 计算汇总统计数据
551
+ recent_data = result["data"][:10]
552
+
553
+ result["summary"] = {
554
+ "recent_days": len(recent_data),
555
+ "total_main_net_inflow": sum(item["main_net_inflow"] for item in recent_data),
556
+ "avg_main_net_inflow_percent": np.mean([item["main_net_inflow_percent"] for item in recent_data]),
557
+ "positive_days": sum(1 for item in recent_data if item["main_net_inflow"] > 0),
558
+ "negative_days": sum(1 for item in recent_data if item["main_net_inflow"] <= 0)
559
+ }
560
+
561
+ return result
562
+
563
+ def _generate_mock_sector_stocks(self, sector):
564
+ """生成模拟行业股票数据"""
565
+ # self.logger.warning(f"Generating mock sector stocks for: {sector}")
566
+
567
+ # 要生成的股票数量
568
+ num_stocks = np.random.randint(20, 50)
569
+
570
+ result = []
571
+ for i in range(num_stocks):
572
+ prefix = "6" if np.random.random() > 0.5 else "0"
573
+ stock_code = prefix + str(100000 + i).zfill(5)[-5:]
574
+
575
+ change_percent = round(np.random.uniform(-5, 5), 2)
576
+
577
+ item = {
578
+ "code": stock_code,
579
+ "name": f"{sector}股票{i + 1}",
580
+ "price": round(np.random.uniform(10, 100), 2),
581
+ "change_percent": change_percent,
582
+ "main_net_inflow": round(np.random.uniform(-1e6, 1e6), 2),
583
+ "main_net_inflow_percent": round(np.random.uniform(-5, 5), 2)
584
+ }
585
+ result.append(item)
586
+
587
+ # 按主力净流入降序排序
588
+ return sorted(result, key=lambda x: x["main_net_inflow"], reverse=True)
database.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, Text, JSON
3
+ from sqlalchemy.ext.declarative import declarative_base
4
+ from sqlalchemy.orm import sessionmaker
5
+ from datetime import datetime
6
+
7
+ # 读取配置
8
+ DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///data/stock_analyzer.db')
9
+ USE_DATABASE = os.getenv('USE_DATABASE', 'False').lower() == 'true'
10
+
11
+ # 创建引擎
12
+ engine = create_engine(DATABASE_URL)
13
+ Base = declarative_base()
14
+
15
+
16
+ # 定义模型
17
+ class StockInfo(Base):
18
+ __tablename__ = 'stock_info'
19
+
20
+ id = Column(Integer, primary_key=True)
21
+ stock_code = Column(String(10), nullable=False, index=True)
22
+ stock_name = Column(String(50))
23
+ market_type = Column(String(5))
24
+ industry = Column(String(50))
25
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
26
+
27
+ def to_dict(self):
28
+ return {
29
+ 'stock_code': self.stock_code,
30
+ 'stock_name': self.stock_name,
31
+ 'market_type': self.market_type,
32
+ 'industry': self.industry,
33
+ 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None
34
+ }
35
+
36
+
37
+ class AnalysisResult(Base):
38
+ __tablename__ = 'analysis_results'
39
+
40
+ id = Column(Integer, primary_key=True)
41
+ stock_code = Column(String(10), nullable=False, index=True)
42
+ market_type = Column(String(5))
43
+ analysis_date = Column(DateTime, default=datetime.now)
44
+ score = Column(Float)
45
+ recommendation = Column(String(100))
46
+ technical_data = Column(JSON)
47
+ fundamental_data = Column(JSON)
48
+ capital_flow_data = Column(JSON)
49
+ ai_analysis = Column(Text)
50
+
51
+ def to_dict(self):
52
+ return {
53
+ 'stock_code': self.stock_code,
54
+ 'market_type': self.market_type,
55
+ 'analysis_date': self.analysis_date.strftime('%Y-%m-%d %H:%M:%S') if self.analysis_date else None,
56
+ 'score': self.score,
57
+ 'recommendation': self.recommendation,
58
+ 'technical_data': self.technical_data,
59
+ 'fundamental_data': self.fundamental_data,
60
+ 'capital_flow_data': self.capital_flow_data,
61
+ 'ai_analysis': self.ai_analysis
62
+ }
63
+
64
+
65
+ class Portfolio(Base):
66
+ __tablename__ = 'portfolios'
67
+
68
+ id = Column(Integer, primary_key=True)
69
+ user_id = Column(String(50), nullable=False, index=True)
70
+ name = Column(String(100))
71
+ created_at = Column(DateTime, default=datetime.now)
72
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
73
+ stocks = Column(JSON) # 存储股票列表的JSON
74
+
75
+ def to_dict(self):
76
+ return {
77
+ 'id': self.id,
78
+ 'user_id': self.user_id,
79
+ 'name': self.name,
80
+ 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
81
+ 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None,
82
+ 'stocks': self.stocks
83
+ }
84
+
85
+
86
+ # 创建会话工厂
87
+ Session = sessionmaker(bind=engine)
88
+
89
+
90
+ # 初始化数据库
91
+ def init_db():
92
+ Base.metadata.create_all(engine)
93
+
94
+
95
+ # 获取数据库会话
96
+ def get_session():
97
+ return Session()
98
+
99
+
100
+ # 如果启用数据库,则初始化
101
+ if USE_DATABASE:
102
+ init_db()
fundamental_analyzer.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 开发者:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # fundamental_analyzer.py
9
+ import akshare as ak
10
+ import pandas as pd
11
+ import numpy as np
12
+
13
+
14
+ class FundamentalAnalyzer:
15
+ def __init__(self):
16
+ """初始化基础分析类"""
17
+ self.data_cache = {}
18
+
19
+ def get_financial_indicators(self, stock_code):
20
+ """获取财务指标数据"""
21
+ try:
22
+ # 获取基本财务指标
23
+ financial_data = ak.stock_financial_analysis_indicator(symbol=stock_code,start_year="2022")
24
+
25
+ # 获取最新估值指标
26
+ valuation = ak.stock_value_em(symbol=stock_code)
27
+
28
+ # 整合数据
29
+ indicators = {
30
+ 'pe_ttm': float(valuation['PE(TTM)'].iloc[0]),
31
+ 'pb': float(valuation['市净率'].iloc[0]),
32
+ 'ps_ttm': float(valuation['市销率'].iloc[0]),
33
+ 'roe': float(financial_data['加权净资产收益率(%)'].iloc[0]),
34
+ 'gross_margin': float(financial_data['销售毛利率(%)'].iloc[0]),
35
+ 'net_profit_margin': float(financial_data['总资产净利润率(%)'].iloc[0]),
36
+ 'debt_ratio': float(financial_data['资产负债率(%)'].iloc[0])
37
+ }
38
+
39
+ return indicators
40
+ except Exception as e:
41
+ print(f"获取财务指标出错: {str(e)}")
42
+ return {}
43
+
44
+ def get_growth_data(self, stock_code):
45
+ """获取成长性数据"""
46
+ try:
47
+ # 获取历年财务数据
48
+ financial_data = ak.stock_financial_abstract(symbol=stock_code)
49
+
50
+ # 计算各项成长率
51
+ revenue = financial_data['营业收入'].astype(float)
52
+ net_profit = financial_data['净利润'].astype(float)
53
+
54
+ growth = {
55
+ 'revenue_growth_3y': self._calculate_cagr(revenue, 3),
56
+ 'profit_growth_3y': self._calculate_cagr(net_profit, 3),
57
+ 'revenue_growth_5y': self._calculate_cagr(revenue, 5),
58
+ 'profit_growth_5y': self._calculate_cagr(net_profit, 5)
59
+ }
60
+
61
+ return growth
62
+ except Exception as e:
63
+ print(f"获取成长数据出错: {str(e)}")
64
+ return {}
65
+
66
+ def _calculate_cagr(self, series, years):
67
+ """计算复合年增长率"""
68
+ if len(series) < years:
69
+ return None
70
+
71
+ latest = series.iloc[0]
72
+ earlier = series.iloc[min(years, len(series) - 1)]
73
+
74
+ if earlier <= 0:
75
+ return None
76
+
77
+ return ((latest / earlier) ** (1 / years) - 1) * 100
78
+
79
+ def calculate_fundamental_score(self, stock_code):
80
+ """计算基本面综合评分"""
81
+ indicators = self.get_financial_indicators(stock_code)
82
+ growth = self.get_growth_data(stock_code)
83
+
84
+ # 估值评分 (30分)
85
+ valuation_score = 0
86
+ if 'pe_ttm' in indicators and indicators['pe_ttm'] > 0:
87
+ pe = indicators['pe_ttm']
88
+ if pe < 15:
89
+ valuation_score += 25
90
+ elif pe < 25:
91
+ valuation_score += 20
92
+ elif pe < 35:
93
+ valuation_score += 15
94
+ elif pe < 50:
95
+ valuation_score += 10
96
+ else:
97
+ valuation_score += 5
98
+
99
+ # 财务健康评分 (40分)
100
+ financial_score = 0
101
+ if 'roe' in indicators:
102
+ roe = indicators['roe']
103
+ if roe > 20:
104
+ financial_score += 15
105
+ elif roe > 15:
106
+ financial_score += 12
107
+ elif roe > 10:
108
+ financial_score += 8
109
+ elif roe > 5:
110
+ financial_score += 4
111
+
112
+ if 'debt_ratio' in indicators:
113
+ debt_ratio = indicators['debt_ratio']
114
+ if debt_ratio < 30:
115
+ financial_score += 15
116
+ elif debt_ratio < 50:
117
+ financial_score += 10
118
+ elif debt_ratio < 70:
119
+ financial_score += 5
120
+
121
+ # 成长性评分 (30分)
122
+ growth_score = 0
123
+ if 'revenue_growth_3y' in growth and growth['revenue_growth_3y']:
124
+ rev_growth = growth['revenue_growth_3y']
125
+ if rev_growth > 30:
126
+ growth_score += 15
127
+ elif rev_growth > 20:
128
+ growth_score += 12
129
+ elif rev_growth > 10:
130
+ growth_score += 8
131
+ elif rev_growth > 0:
132
+ growth_score += 4
133
+
134
+ if 'profit_growth_3y' in growth and growth['profit_growth_3y']:
135
+ profit_growth = growth['profit_growth_3y']
136
+ if profit_growth > 30:
137
+ growth_score += 15
138
+ elif profit_growth > 20:
139
+ growth_score += 12
140
+ elif profit_growth > 10:
141
+ growth_score += 8
142
+ elif profit_growth > 0:
143
+ growth_score += 4
144
+
145
+ # 计算总分
146
+ total_score = valuation_score + financial_score + growth_score
147
+
148
+ return {
149
+ 'total': total_score,
150
+ 'valuation': valuation_score,
151
+ 'financial_health': financial_score,
152
+ 'growth': growth_score,
153
+ 'details': {
154
+ 'indicators': indicators,
155
+ 'growth': growth
156
+ }
157
+ }
index_industry_analyzer.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # index_industry_analyzer.py
2
+ import akshare as ak
3
+ import pandas as pd
4
+ import numpy as np
5
+ import threading
6
+
7
+
8
+ class IndexIndustryAnalyzer:
9
+ def __init__(self, analyzer):
10
+ self.analyzer = analyzer
11
+ self.data_cache = {}
12
+
13
+ def analyze_index(self, index_code, limit=30):
14
+ """分析指数整体情况"""
15
+ try:
16
+ cache_key = f"index_{index_code}"
17
+ if cache_key in self.data_cache:
18
+ cache_time, cached_result = self.data_cache[cache_key]
19
+ # 如果缓存时间在1小时内,直接返回
20
+ if (pd.Timestamp.now() - cache_time).total_seconds() < 3600:
21
+ return cached_result
22
+
23
+ # 获取指数成分股
24
+ if index_code == '000300':
25
+ # 沪深300成分股
26
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000300")
27
+ index_name = "沪深300"
28
+ elif index_code == '000905':
29
+ # 中证500成分股
30
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000905")
31
+ index_name = "中证500"
32
+ elif index_code == '000852':
33
+ # 中证1000成分股
34
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000852")
35
+ index_name = "中证1000"
36
+ elif index_code == '000001':
37
+ # 上证指数
38
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000001")
39
+ index_name = "上证指数"
40
+ else:
41
+ return {"error": "不支持的指数代码"}
42
+
43
+ # 提取股票代码列表和权重
44
+ stock_list = []
45
+ if '成分券代码' in stocks.columns:
46
+ stock_list = stocks['成分券代码'].tolist()
47
+ weights = stocks['权重(%)'].tolist() if '权重(%)' in stocks.columns else [1] * len(stock_list)
48
+ else:
49
+ return {"error": "获取指数成分股失败"}
50
+
51
+ # 限制分析的股票数量以提高性能
52
+ if limit and len(stock_list) > limit:
53
+ # 按权重排序,取前limit只权重最大的股票
54
+ stock_weights = list(zip(stock_list, weights))
55
+ stock_weights.sort(key=lambda x: x[1], reverse=True)
56
+ stock_list = [s[0] for s in stock_weights[:limit]]
57
+ weights = [s[1] for s in stock_weights[:limit]]
58
+
59
+ # 多线程分析股票
60
+ results = []
61
+ threads = []
62
+ results_lock = threading.Lock()
63
+
64
+ def analyze_stock(stock_code, weight):
65
+ try:
66
+ # 分析股票
67
+ result = self.analyzer.quick_analyze_stock(stock_code)
68
+ result['weight'] = weight
69
+
70
+ with results_lock:
71
+ results.append(result)
72
+ except Exception as e:
73
+ print(f"分析股票 {stock_code} 时出错: {str(e)}")
74
+
75
+ # 创建并启动线程
76
+ for i, stock_code in enumerate(stock_list):
77
+ weight = weights[i] if i < len(weights) else 1
78
+ thread = threading.Thread(target=analyze_stock, args=(stock_code, weight))
79
+ threads.append(thread)
80
+ thread.start()
81
+
82
+ # 等待所有线程完成
83
+ for thread in threads:
84
+ thread.join()
85
+
86
+ # 计算指数整体情况
87
+ total_weight = sum([r.get('weight', 1) for r in results])
88
+
89
+ # 计算加权评分
90
+ index_score = 0
91
+ if total_weight > 0:
92
+ index_score = sum([r.get('score', 0) * r.get('weight', 1) for r in results]) / total_weight
93
+
94
+ # 计算其他指标
95
+ up_count = sum(1 for r in results if r.get('price_change', 0) > 0)
96
+ down_count = sum(1 for r in results if r.get('price_change', 0) < 0)
97
+ flat_count = len(results) - up_count - down_count
98
+
99
+ # 计算涨跌股比例
100
+ up_ratio = up_count / len(results) if len(results) > 0 else 0
101
+
102
+ # 计算加权平均涨跌幅
103
+ weighted_change = 0
104
+ if total_weight > 0:
105
+ weighted_change = sum([r.get('price_change', 0) * r.get('weight', 1) for r in results]) / total_weight
106
+
107
+ # 按评分对股票排序
108
+ results.sort(key=lambda x: x.get('score', 0), reverse=True)
109
+
110
+ # 整理结果
111
+ index_analysis = {
112
+ "index_code": index_code,
113
+ "index_name": index_name,
114
+ "score": round(index_score, 2),
115
+ "stock_count": len(results),
116
+ "up_count": up_count,
117
+ "down_count": down_count,
118
+ "flat_count": flat_count,
119
+ "up_ratio": up_ratio,
120
+ "weighted_change": weighted_change,
121
+ "top_stocks": results[:5] if len(results) >= 5 else results,
122
+ "results": results
123
+ }
124
+
125
+ # 缓存结果
126
+ self.data_cache[cache_key] = (pd.Timestamp.now(), index_analysis)
127
+
128
+ return index_analysis
129
+
130
+ except Exception as e:
131
+ print(f"分析指数整体情况时出错: {str(e)}")
132
+ return {"error": f"分析指数时出错: {str(e)}"}
133
+
134
+ def analyze_industry(self, industry, limit=30):
135
+ """分析行业整体情况"""
136
+ try:
137
+ cache_key = f"industry_{industry}"
138
+ if cache_key in self.data_cache:
139
+ cache_time, cached_result = self.data_cache[cache_key]
140
+ # 如果缓存时间在1小时内,直接返回
141
+ if (pd.Timestamp.now() - cache_time).total_seconds() < 3600:
142
+ return cached_result
143
+
144
+ # 获取行业成分股
145
+ stocks = ak.stock_board_industry_cons_em(symbol=industry)
146
+
147
+ # 提取股票代码列表
148
+ stock_list = stocks['代码'].tolist() if '代码' in stocks.columns else []
149
+
150
+ if not stock_list:
151
+ return {"error": "获取行业成分股失败"}
152
+
153
+ # 限制分析的股票数量以提高性能
154
+ if limit and len(stock_list) > limit:
155
+ stock_list = stock_list[:limit]
156
+
157
+ # 多线程分析股票
158
+ results = []
159
+ threads = []
160
+ results_lock = threading.Lock()
161
+
162
+ def analyze_stock(stock_code):
163
+ try:
164
+ # 分析股票
165
+ result = self.analyzer.quick_analyze_stock(stock_code)
166
+
167
+ with results_lock:
168
+ results.append(result)
169
+ except Exception as e:
170
+ print(f"分析股票 {stock_code} 时出错: {str(e)}")
171
+
172
+ # 创建并启动线程
173
+ for stock_code in stock_list:
174
+ thread = threading.Thread(target=analyze_stock, args=(stock_code,))
175
+ threads.append(thread)
176
+ thread.start()
177
+
178
+ # 等待所有线程完成
179
+ for thread in threads:
180
+ thread.join()
181
+
182
+ # 计算行业整体情况
183
+ if not results:
184
+ return {"error": "分析行业股票失败"}
185
+
186
+ # 计算平均评分
187
+ industry_score = sum([r.get('score', 0) for r in results]) / len(results)
188
+
189
+ # 计算其他指标
190
+ up_count = sum(1 for r in results if r.get('price_change', 0) > 0)
191
+ down_count = sum(1 for r in results if r.get('price_change', 0) < 0)
192
+ flat_count = len(results) - up_count - down_count
193
+
194
+ # 计算涨跌股比例
195
+ up_ratio = up_count / len(results)
196
+
197
+ # 计算平均涨跌幅
198
+ avg_change = sum([r.get('price_change', 0) for r in results]) / len(results)
199
+
200
+ # 按评分对股票排序
201
+ results.sort(key=lambda x: x.get('score', 0), reverse=True)
202
+
203
+ # 整理结果
204
+ industry_analysis = {
205
+ "industry": industry,
206
+ "score": round(industry_score, 2),
207
+ "stock_count": len(results),
208
+ "up_count": up_count,
209
+ "down_count": down_count,
210
+ "flat_count": flat_count,
211
+ "up_ratio": up_ratio,
212
+ "avg_change": avg_change,
213
+ "top_stocks": results[:5] if len(results) >= 5 else results,
214
+ "results": results
215
+ }
216
+
217
+ # 缓存结果
218
+ self.data_cache[cache_key] = (pd.Timestamp.now(), industry_analysis)
219
+
220
+ return industry_analysis
221
+
222
+ except Exception as e:
223
+ print(f"分析行业整体情况时出错: {str(e)}")
224
+ return {"error": f"分析行业时出错: {str(e)}"}
225
+
226
+ def compare_industries(self, limit=10):
227
+ """比较不同行业的表现"""
228
+ try:
229
+ # 获取行业板块数据
230
+ industry_data = ak.stock_board_industry_name_em()
231
+
232
+ # 提取行业名称列表
233
+ industries = industry_data['板块名称'].tolist() if '板块名称' in industry_data.columns else []
234
+
235
+ if not industries:
236
+ return {"error": "获取行业列表失败"}
237
+
238
+ # 限制分析的行业数量
239
+ industries = industries[:limit] if limit else industries
240
+
241
+ # 分析各行业情况
242
+ industry_results = []
243
+
244
+ for industry in industries:
245
+ try:
246
+ # 简化分析,只获取基本指标
247
+ industry_info = ak.stock_board_industry_hist_em(symbol=industry, period="3m")
248
+
249
+ # 计算行业涨跌幅
250
+ if not industry_info.empty:
251
+ latest = industry_info.iloc[0]
252
+ change = latest['涨跌幅'] if '涨跌幅' in latest.index else 0
253
+
254
+ industry_results.append({
255
+ "industry": industry,
256
+ "change": change,
257
+ "volume": latest['成交量'] if '成交量' in latest.index else 0,
258
+ "turnover": latest['成交额'] if '成交额' in latest.index else 0
259
+ })
260
+ except Exception as e:
261
+ print(f"分析行业 {industry} 时出错: {str(e)}")
262
+
263
+ # 按涨跌幅排序
264
+ industry_results.sort(key=lambda x: x.get('change', 0), reverse=True)
265
+
266
+ return {
267
+ "count": len(industry_results),
268
+ "top_industries": industry_results[:5] if len(industry_results) >= 5 else industry_results,
269
+ "bottom_industries": industry_results[-5:] if len(industry_results) >= 5 else [],
270
+ "results": industry_results
271
+ }
272
+
273
+ except Exception as e:
274
+ print(f"比较行业表现时出错: {str(e)}")
275
+ return {"error": f"比较行业表现时出错: {str(e)}"}
industry_analyzer.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 开发者:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # industry_analyzer.py
9
+ import logging
10
+ import random
11
+ import akshare as ak
12
+ import pandas as pd
13
+ import numpy as np
14
+ from datetime import datetime, timedelta
15
+
16
+
17
+ class IndustryAnalyzer:
18
+ def __init__(self):
19
+ """初始化行业分析类"""
20
+ self.data_cache = {}
21
+ self.industry_code_map = {} # 缓存行业名称到代码的映射
22
+
23
+ # 设置日志记录
24
+ logging.basicConfig(level=logging.INFO,
25
+ format='%(asctime)s - %(levelname)s - %(message)s')
26
+ self.logger = logging.getLogger(__name__)
27
+
28
+ def get_industry_fund_flow(self, symbol="即时"):
29
+ """获取行业资金流向数据"""
30
+ try:
31
+ # 缓存键
32
+ cache_key = f"industry_fund_flow_{symbol}"
33
+
34
+ # 检查缓存
35
+ if cache_key in self.data_cache:
36
+ cache_time, cached_data = self.data_cache[cache_key]
37
+ # 如果缓存时间在30分钟内,直接返回
38
+ if (datetime.now() - cache_time).total_seconds() < 1800:
39
+ self.logger.info(f"从缓存获取行业资金流向数据: {symbol}")
40
+ return cached_data
41
+
42
+ # 获取行业资金流向数据
43
+ self.logger.info(f"从API获取行业资金流向数据: {symbol}")
44
+ fund_flow_data = ak.stock_fund_flow_industry(symbol=symbol)
45
+
46
+ # 打印列名以便调试
47
+ self.logger.info(f"行业资金流向数据列名: {fund_flow_data.columns.tolist()}")
48
+
49
+ # 转换为字典列表
50
+ result = []
51
+
52
+ if symbol == "即时":
53
+ for _, row in fund_flow_data.iterrows():
54
+ try:
55
+ # 安全地将值转换为对应的类型
56
+ item = {
57
+ "rank": self._safe_int(row["序号"]),
58
+ "industry": str(row["行业"]),
59
+ "index": self._safe_float(row["行业指数"]),
60
+ "change": self._safe_percent(row["行业-涨跌幅"]),
61
+ "inflow": self._safe_float(row["流入资金"]),
62
+ "outflow": self._safe_float(row["流出资金"]),
63
+ "netFlow": self._safe_float(row["净额"]),
64
+ "companyCount": self._safe_int(row["公司家数"])
65
+ }
66
+
67
+ # 添加领涨股相关数据,如果存在
68
+ if "领涨股" in row:
69
+ item["leadingStock"] = str(row["领涨股"])
70
+ if "领涨股-涨跌幅" in row:
71
+ item["leadingStockChange"] = self._safe_percent(row["领涨股-涨跌幅"])
72
+ if "当前价" in row:
73
+ item["leadingStockPrice"] = self._safe_float(row["当前价"])
74
+
75
+ result.append(item)
76
+ except Exception as e:
77
+ self.logger.warning(f"处理行业资金流向数据行时出错: {str(e)}")
78
+ continue
79
+ else:
80
+ for _, row in fund_flow_data.iterrows():
81
+ try:
82
+ item = {
83
+ "rank": self._safe_int(row["序号"]),
84
+ "industry": str(row["行业"]),
85
+ "companyCount": self._safe_int(row["公司家数"]),
86
+ "index": self._safe_float(row["行业指数"]),
87
+ "change": self._safe_percent(row["阶段涨跌幅"]),
88
+ "inflow": self._safe_float(row["流入资金"]),
89
+ "outflow": self._safe_float(row["流出资金"]),
90
+ "netFlow": self._safe_float(row["净额"])
91
+ }
92
+ result.append(item)
93
+ except Exception as e:
94
+ self.logger.warning(f"处理行业资金流向数据行时出错: {str(e)}")
95
+ continue
96
+
97
+ # 缓存结果
98
+ self.data_cache[cache_key] = (datetime.now(), result)
99
+
100
+ return result
101
+
102
+ except Exception as e:
103
+ self.logger.error(f"获取行业资金流向数据失败: {str(e)}")
104
+ # 返回更详细的错误信息,包括堆栈跟踪
105
+ import traceback
106
+ self.logger.error(traceback.format_exc())
107
+ return []
108
+
109
+ def _safe_float(self, value):
110
+ """安全地将值转换为浮点数"""
111
+ try:
112
+ if pd.isna(value):
113
+ return 0.0
114
+ return float(value)
115
+ except:
116
+ return 0.0
117
+
118
+ def _safe_int(self, value):
119
+ """安全地将值转换为整数"""
120
+ try:
121
+ if pd.isna(value):
122
+ return 0
123
+ return int(value)
124
+ except:
125
+ return 0
126
+
127
+ def _safe_percent(self, value):
128
+ """安全地将百分比值转换为字符串格式"""
129
+ try:
130
+ if pd.isna(value):
131
+ return "0.00"
132
+
133
+ # 如果是字符串并包含%,移除%符号
134
+ if isinstance(value, str) and "%" in value:
135
+ return value.replace("%", "")
136
+
137
+ # 如果是数值,直接转换成字符串
138
+ return str(float(value))
139
+ except:
140
+ return "0.00"
141
+
142
+ def _get_industry_code(self, industry_name):
143
+ """获取行业名称对应的板块代码"""
144
+ try:
145
+ # 如果已经缓存了行业代码映射,直接使用
146
+ if not self.industry_code_map:
147
+ # 获取东方财富行业板块名称及代码
148
+ industry_list = ak.stock_board_industry_name_em()
149
+
150
+ # 创建行业名称到代码的映射
151
+ for _, row in industry_list.iterrows():
152
+ if '板块名称' in industry_list.columns and '板块代码' in industry_list.columns:
153
+ name = row['板块名称']
154
+ code = row['板块代码']
155
+ self.industry_code_map[name] = code
156
+
157
+ self.logger.info(f"成功获取到 {len(self.industry_code_map)} 个行业代码映射")
158
+
159
+ # 尝试精确匹配
160
+ if industry_name in self.industry_code_map:
161
+ return self.industry_code_map[industry_name]
162
+
163
+ # 尝试模糊匹配
164
+ for name, code in self.industry_code_map.items():
165
+ if industry_name in name or name in industry_name:
166
+ self.logger.info(f"行业名称 '{industry_name}' 模糊匹配到 '{name}',代码: {code}")
167
+ return code
168
+
169
+ # 如果找不到匹配项,则返回None
170
+ self.logger.warning(f"未找到行业 '{industry_name}' 对应的代码")
171
+ return None
172
+
173
+ except Exception as e:
174
+ self.logger.error(f"获取行业代码时出错: {str(e)}")
175
+ import traceback
176
+ self.logger.error(traceback.format_exc())
177
+ return None
178
+
179
+ def get_industry_stocks(self, industry):
180
+ """获取行业成分股"""
181
+ try:
182
+ # 缓存键
183
+ cache_key = f"industry_stocks_{industry}"
184
+
185
+ # 检查缓存
186
+ if cache_key in self.data_cache:
187
+ cache_time, cached_data = self.data_cache[cache_key]
188
+ # 如果缓存时间在1小时内,直接返回
189
+ if (datetime.now() - cache_time).total_seconds() < 3600:
190
+ self.logger.info(f"从缓存获取行业成分股: {industry}")
191
+ return cached_data
192
+
193
+ # 获取行业成分股
194
+ self.logger.info(f"获取 {industry} 行业成分股")
195
+
196
+ result = []
197
+ try:
198
+ # 1. 首先尝试直接使用行业名称
199
+ try:
200
+ stocks = ak.stock_board_industry_cons_em(symbol=industry)
201
+ self.logger.info(f"使用行业名称 '{industry}' 成功获取成分股")
202
+ except Exception as direct_error:
203
+ self.logger.warning(f"使用行业名称获取成分股失败: {str(direct_error)}")
204
+ # 2. 尝试使用行业代码
205
+ industry_code = self._get_industry_code(industry)
206
+ if industry_code:
207
+ self.logger.info(f"尝试使用行业代码 {industry_code} 获取成分股")
208
+ stocks = ak.stock_board_industry_cons_em(symbol=industry_code)
209
+ else:
210
+ # 如果无法获取行业代码,抛出异常,进入模拟数据生成
211
+ raise ValueError(f"无法找到行业 '{industry}' 对应的代码")
212
+
213
+ # 打印列名以便调试
214
+ self.logger.info(f"行业成分股数据列名: {stocks.columns.tolist()}")
215
+
216
+ # 转换为字典列表
217
+ if not stocks.empty:
218
+ for _, row in stocks.iterrows():
219
+ try:
220
+ item = {
221
+ "code": str(row["代码"]),
222
+ "name": str(row["名称"]),
223
+ "price": self._safe_float(row["最新价"]),
224
+ "change": self._safe_float(row["涨跌幅"]),
225
+ "change_amount": self._safe_float(row["涨跌额"]) if "涨跌额" in row else 0.0,
226
+ "volume": self._safe_float(row["成交量"]) if "成交量" in row else 0.0,
227
+ "turnover": self._safe_float(row["成交额"]) if "成交额" in row else 0.0,
228
+ "amplitude": self._safe_float(row["振幅"]) if "振幅" in row else 0.0,
229
+ "turnover_rate": self._safe_float(row["换手率"]) if "换手率" in row else 0.0
230
+ }
231
+ result.append(item)
232
+ except Exception as e:
233
+ self.logger.warning(f"处理行业成分股数据行时出错: {str(e)}")
234
+ continue
235
+
236
+ except Exception as e:
237
+ # 3. 如果上述方法都失败,生成模拟数据
238
+ self.logger.warning(f"无法通过API获取行业成分股,使用模拟数据: {str(e)}")
239
+ result = self._generate_mock_industry_stocks(industry)
240
+
241
+ # 缓存结果
242
+ self.data_cache[cache_key] = (datetime.now(), result)
243
+
244
+ return result
245
+
246
+ except Exception as e:
247
+ self.logger.error(f"获取行业成分股失败: {str(e)}")
248
+ import traceback
249
+ self.logger.error(traceback.format_exc())
250
+ return []
251
+
252
+ def _generate_mock_industry_stocks(self, industry):
253
+ """生成模拟的行业成分股数据"""
254
+ self.logger.info(f"生成行业 {industry} 的模拟成分股数据")
255
+
256
+ # 使用来自资金流向的行业数据获取该行业的基本信息
257
+ fund_flow_data = self.get_industry_fund_flow("即时")
258
+ industry_data = next((item for item in fund_flow_data if item["industry"] == industry), None)
259
+
260
+ company_count = 20 # 默认值
261
+ if industry_data and "companyCount" in industry_data:
262
+ company_count = min(industry_data["companyCount"], 30) # 限制最多30只股票
263
+
264
+ # 生成模拟股票
265
+ result = []
266
+ for i in range(company_count):
267
+ # 生成6位数字的股票代码,确保前缀是0或6
268
+ prefix = "6" if i % 2 == 0 else "0"
269
+ code = prefix + str(100000 + i).zfill(5)[-5:]
270
+
271
+ # 生成股票价格和涨跌幅
272
+ price = round(random.uniform(10, 100), 2)
273
+ change = round(random.uniform(-5, 5), 2)
274
+
275
+ # 生成成交量和成交额
276
+ volume = round(random.uniform(100000, 10000000))
277
+ turnover = round(volume * price / 10000, 2) # 转换为万元
278
+
279
+ # 生成换手率和振幅
280
+ turnover_rate = round(random.uniform(0.5, 5), 2)
281
+ amplitude = round(random.uniform(1, 10), 2)
282
+
283
+ item = {
284
+ "code": code,
285
+ "name": f"{industry}股{i + 1}",
286
+ "price": price,
287
+ "change": change,
288
+ "change_amount": round(price * change / 100, 2),
289
+ "volume": volume,
290
+ "turnover": turnover,
291
+ "amplitude": amplitude,
292
+ "turnover_rate": turnover_rate
293
+ }
294
+ result.append(item)
295
+
296
+ # 按涨跌幅排序
297
+ result.sort(key=lambda x: x["change"], reverse=True)
298
+
299
+ return result
300
+
301
+ def get_industry_detail(self, industry):
302
+ """获取行业详细信息"""
303
+ try:
304
+ # 获取行业资金流向数据
305
+ fund_flow_data = self.get_industry_fund_flow("即时")
306
+ industry_data = next((item for item in fund_flow_data if item["industry"] == industry), None)
307
+
308
+ if not industry_data:
309
+ return None
310
+
311
+ # 获取历史资金流向数据
312
+ history_data = []
313
+
314
+ for period in ["3日排行", "5日排行", "10日排行", "20日排行"]:
315
+ period_data = self.get_industry_fund_flow(period)
316
+ industry_period_data = next((item for item in period_data if item["industry"] == industry), None)
317
+
318
+ if industry_period_data:
319
+ days = int(period.replace("日排行", ""))
320
+ date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
321
+
322
+ history_data.append({
323
+ "date": date,
324
+ "inflow": industry_period_data["inflow"],
325
+ "outflow": industry_period_data["outflow"],
326
+ "netFlow": industry_period_data["netFlow"],
327
+ "change": industry_period_data["change"]
328
+ })
329
+
330
+ # 添加即时数据
331
+ history_data.append({
332
+ "date": datetime.now().strftime("%Y-%m-%d"),
333
+ "inflow": industry_data["inflow"],
334
+ "outflow": industry_data["outflow"],
335
+ "netFlow": industry_data["netFlow"],
336
+ "change": industry_data["change"]
337
+ })
338
+
339
+ # 按日期排序
340
+ history_data.sort(key=lambda x: x["date"])
341
+
342
+ # 计算行��评分
343
+ score = self.calculate_industry_score(industry_data, history_data)
344
+
345
+ # 生成投资建议
346
+ recommendation = self.generate_industry_recommendation(score, industry_data, history_data)
347
+
348
+ # 构建结果
349
+ result = {
350
+ "industry": industry,
351
+ "index": industry_data["index"],
352
+ "change": industry_data["change"],
353
+ "companyCount": industry_data["companyCount"],
354
+ "inflow": industry_data["inflow"],
355
+ "outflow": industry_data["outflow"],
356
+ "netFlow": industry_data["netFlow"],
357
+ "leadingStock": industry_data.get("leadingStock", ""),
358
+ "leadingStockChange": industry_data.get("leadingStockChange", ""),
359
+ "leadingStockPrice": industry_data.get("leadingStockPrice", 0),
360
+ "score": score,
361
+ "recommendation": recommendation,
362
+ "flowHistory": history_data
363
+ }
364
+
365
+ return result
366
+
367
+ except Exception as e:
368
+ self.logger.error(f"获取行业详细信息失败: {str(e)}")
369
+ import traceback
370
+ self.logger.error(traceback.format_exc())
371
+ return None
372
+
373
+ def calculate_industry_score(self, industry_data, history_data):
374
+ """计算行业评分"""
375
+ try:
376
+ # 基础分数为50分
377
+ score = 50
378
+
379
+ # 根据涨跌幅增减分数(-10到+10)
380
+ change = float(industry_data["change"])
381
+ if change > 3:
382
+ score += 10
383
+ elif change > 1:
384
+ score += 5
385
+ elif change < -3:
386
+ score -= 10
387
+ elif change < -1:
388
+ score -= 5
389
+
390
+ # 根据资金流向增减分数(-20到+20)
391
+ netFlow = float(industry_data["netFlow"])
392
+
393
+ if netFlow > 5:
394
+ score += 20
395
+ elif netFlow > 2:
396
+ score += 15
397
+ elif netFlow > 0:
398
+ score += 10
399
+ elif netFlow < -5:
400
+ score -= 20
401
+ elif netFlow < -2:
402
+ score -= 15
403
+ elif netFlow < 0:
404
+ score -= 10
405
+
406
+ # 根据历史资金流向趋势增减分数(-10到+10)
407
+ if len(history_data) >= 2:
408
+ net_flow_trend = 0
409
+ for i in range(1, len(history_data)):
410
+ if float(history_data[i]["netFlow"]) > float(history_data[i - 1]["netFlow"]):
411
+ net_flow_trend += 1
412
+ else:
413
+ net_flow_trend -= 1
414
+
415
+ if net_flow_trend > 0:
416
+ score += 10
417
+ elif net_flow_trend < 0:
418
+ score -= 10
419
+
420
+ # 限制分数在0-100之间
421
+ score = max(0, min(100, score))
422
+
423
+ return round(score)
424
+
425
+ except Exception as e:
426
+ self.logger.error(f"计算行业评分时出错: {str(e)}")
427
+ return 50
428
+
429
+ def generate_industry_recommendation(self, score, industry_data, history_data):
430
+ """生成行业投资建议"""
431
+ try:
432
+ if score >= 80:
433
+ return "行业景气度高,资金持续流入,建议积极配置"
434
+ elif score >= 60:
435
+ return "行业表现良好,建议适当加仓"
436
+ elif score >= 40:
437
+ return "行业表现一般,建议谨慎参与"
438
+ else:
439
+ return "行业下行趋势明显,建议减持规避风险"
440
+
441
+ except Exception as e:
442
+ self.logger.error(f"生成行业投资建议时出错: {str(e)}")
443
+ return "无法生成投资建议"
444
+
445
+ def compare_industries(self, limit=10):
446
+ """比较不同行业的表现"""
447
+ try:
448
+ # 获取行业板块数据
449
+ industry_data = ak.stock_board_industry_name_em()
450
+
451
+ # 提取行业名称列表
452
+ industries = industry_data['板块名称'].tolist() if '板块名称' in industry_data.columns else []
453
+
454
+ if not industries:
455
+ return {"error": "获取行业列表失败"}
456
+
457
+ # 限制分析的行业数量
458
+ industries = industries[:limit] if limit else industries
459
+
460
+ # 分析各行业情况
461
+ industry_results = []
462
+
463
+ for industry in industries:
464
+ try:
465
+ # 尝试获取行业板块代码
466
+ industry_code = None
467
+ for _, row in industry_data.iterrows():
468
+ if row['板块名称'] == industry:
469
+ industry_code = row['板块代码']
470
+ break
471
+
472
+ if not industry_code:
473
+ self.logger.warning(f"未找到行业 {industry} 的板块代码")
474
+ continue
475
+
476
+ # 尝试使用不同的参数来获取行业数据 - 不使用"3m"
477
+ try:
478
+ # 尝试不使用period参数
479
+ industry_info = ak.stock_board_industry_hist_em(symbol=industry_code)
480
+ except Exception as e1:
481
+ try:
482
+ # 尝试使用daily参数
483
+ industry_info = ak.stock_board_industry_hist_em(symbol=industry_code, period="daily")
484
+ except Exception as e2:
485
+ self.logger.warning(f"分析行业 {industry} 历史数据失败: {str(e1)}, {str(e2)}")
486
+ continue
487
+
488
+ # 计算行业涨跌幅
489
+ if not industry_info.empty:
490
+ latest = industry_info.iloc[0]
491
+
492
+ # 尝试获取涨跌幅,列名可能有变化
493
+ change = 0.0
494
+ if '涨跌幅' in latest.index:
495
+ change = latest['涨跌幅']
496
+ elif '涨跌幅' in industry_info.columns:
497
+ change = latest['涨跌幅']
498
+
499
+ # 尝试获取成交量和成交额
500
+ volume = 0.0
501
+ turnover = 0.0
502
+ if '成交量' in latest.index:
503
+ volume = latest['成交量']
504
+ elif '成交量' in industry_info.columns:
505
+ volume = latest['成交量']
506
+
507
+ if '成交额' in latest.index:
508
+ turnover = latest['成交额']
509
+ elif '成交额' in industry_info.columns:
510
+ turnover = latest['成交额']
511
+
512
+ industry_results.append({
513
+ "industry": industry,
514
+ "change": float(change) if change else 0.0,
515
+ "volume": float(volume) if volume else 0.0,
516
+ "turnover": float(turnover) if turnover else 0.0
517
+ })
518
+ except Exception as e:
519
+ self.logger.error(f"分析行业 {industry} 时出错: {str(e)}")
520
+
521
+ # 按涨跌幅排序
522
+ industry_results.sort(key=lambda x: x.get('change', 0), reverse=True)
523
+
524
+ return {
525
+ "count": len(industry_results),
526
+ "top_industries": industry_results[:5] if len(industry_results) >= 5 else industry_results,
527
+ "bottom_industries": industry_results[-5:] if len(industry_results) >= 5 else [],
528
+ "results": industry_results
529
+ }
530
+
531
+ except Exception as e:
532
+ self.logger.error(f"比较行业表现时出错: {str(e)}")
533
+ return {"error": f"比较行业表现时出错: {str(e)}"}
industry_api_endpoints.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 开发者:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # industry_api_endpoints.py
9
+ # 预留接口
requirements.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.24.0
2
+ pandas==2.2.2
3
+ scipy>=1.13.0,<1.14.0
4
+ akshare==1.15.94
5
+ tqdm==4.67.1
6
+ openai==0.28.0
7
+ requests==2.32.3
8
+ python-dotenv==1.0.1
9
+ flask==3.1.0
10
+ loguru==0.7.2
11
+ matplotlib==3.9.2
12
+ seaborn==0.13.2
13
+ ipython>=7.34.0
14
+ beautifulsoup4==4.12.3
15
+ html5lib==1.1
16
+ lxml==4.9.4
17
+ jsonpath==0.82.2
18
+ openpyxl==3.1.5
19
+ flask_swagger_ui
20
+ sqlalchemy
21
+ flask-cors
22
+ flask-caching
23
+ # 新增依赖
24
+ gunicorn==20.1.0 # 生产环境WSGI服务器
25
+ PyYAML==6.0 # YAML支持
26
+ scikit-learn==1.2.2 # 机器学习库(用于预测模型)
27
+ statsmodels==0.13.5 # 统计模型(用于时间序列分析)
28
+ pytest==7.3.1 # 测试框架
29
+
30
+ # 部署工具
31
+ supervisor==4.2.5 # 进程管理
32
+ redis==4.5.4 # 可选的缓存后端
risk_monitor.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 开发者:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # risk_monitor.py
9
+ import pandas as pd
10
+ import numpy as np
11
+ from datetime import datetime, timedelta
12
+
13
+ class RiskMonitor:
14
+ def __init__(self, analyzer):
15
+ self.analyzer = analyzer
16
+
17
+ def analyze_stock_risk(self, stock_code, market_type='A'):
18
+ """分析单只股票的风险"""
19
+ try:
20
+ # 获取股票数据和技术指标
21
+ df = self.analyzer.get_stock_data(stock_code, market_type)
22
+ df = self.analyzer.calculate_indicators(df)
23
+
24
+ # 计算各类风险指标
25
+ volatility_risk = self._analyze_volatility_risk(df)
26
+ trend_risk = self._analyze_trend_risk(df)
27
+ reversal_risk = self._analyze_reversal_risk(df)
28
+ volume_risk = self._analyze_volume_risk(df)
29
+
30
+ # 综合评估总体风险
31
+ total_risk_score = (
32
+ volatility_risk['score'] * 0.3 +
33
+ trend_risk['score'] * 0.3 +
34
+ reversal_risk['score'] * 0.25 +
35
+ volume_risk['score'] * 0.15
36
+ )
37
+
38
+ # 确定风险等级
39
+ if total_risk_score >= 80:
40
+ risk_level = "极高"
41
+ elif total_risk_score >= 60:
42
+ risk_level = "高"
43
+ elif total_risk_score >= 40:
44
+ risk_level = "中等"
45
+ elif total_risk_score >= 20:
46
+ risk_level = "低"
47
+ else:
48
+ risk_level = "极低"
49
+
50
+ # 生成风险警报
51
+ alerts = []
52
+
53
+ if volatility_risk['score'] >= 70:
54
+ alerts.append({
55
+ "type": "volatility",
56
+ "level": "高",
57
+ "message": f"波动率风险较高 ({volatility_risk['value']:.2f}%),可能面临大幅波动"
58
+ })
59
+
60
+ if trend_risk['score'] >= 70:
61
+ alerts.append({
62
+ "type": "trend",
63
+ "level": "高",
64
+ "message": f"趋势风险较高,当前处于{trend_risk['trend']}趋势,可能面临加速下跌"
65
+ })
66
+
67
+ if reversal_risk['score'] >= 70:
68
+ alerts.append({
69
+ "type": "reversal",
70
+ "level": "高",
71
+ "message": f"趋势反转风险较高,技术指标显示可能{reversal_risk['direction']}反转"
72
+ })
73
+
74
+ if volume_risk['score'] >= 70:
75
+ alerts.append({
76
+ "type": "volume",
77
+ "level": "高",
78
+ "message": f"成交量异常,{volume_risk['pattern']},可能预示价格波动"
79
+ })
80
+
81
+ return {
82
+ "total_risk_score": total_risk_score,
83
+ "risk_level": risk_level,
84
+ "volatility_risk": volatility_risk,
85
+ "trend_risk": trend_risk,
86
+ "reversal_risk": reversal_risk,
87
+ "volume_risk": volume_risk,
88
+ "alerts": alerts
89
+ }
90
+
91
+ except Exception as e:
92
+ print(f"分析股票风险出错: {str(e)}")
93
+ return {
94
+ "error": f"分析风险时出错: {str(e)}"
95
+ }
96
+
97
+ def _analyze_volatility_risk(self, df):
98
+ """分析波动率风险"""
99
+ # 计算近期波动率
100
+ recent_volatility = df.iloc[-1]['Volatility']
101
+
102
+ # 计算波动率变化
103
+ avg_volatility = df['Volatility'].mean()
104
+ volatility_change = recent_volatility / avg_volatility - 1
105
+
106
+ # 评估风险分数
107
+ if recent_volatility > 5 and volatility_change > 0.5:
108
+ score = 90 # 极高风险
109
+ elif recent_volatility > 4 and volatility_change > 0.3:
110
+ score = 75 # 高风险
111
+ elif recent_volatility > 3 and volatility_change > 0.1:
112
+ score = 60 # 中高风险
113
+ elif recent_volatility > 2:
114
+ score = 40 # 中等风险
115
+ elif recent_volatility > 1:
116
+ score = 20 # 低风险
117
+ else:
118
+ score = 0 # 极低风险
119
+
120
+ return {
121
+ "score": score,
122
+ "value": recent_volatility,
123
+ "change": volatility_change * 100,
124
+ "risk_level": "高" if score >= 60 else "中" if score >= 30 else "低"
125
+ }
126
+
127
+ def _analyze_trend_risk(self, df):
128
+ """分析趋势风险"""
129
+ # 获取均线数据
130
+ ma5 = df.iloc[-1]['MA5']
131
+ ma20 = df.iloc[-1]['MA20']
132
+ ma60 = df.iloc[-1]['MA60']
133
+
134
+ # 判断当前趋势
135
+ if ma5 < ma20 < ma60:
136
+ trend = "下降"
137
+
138
+ # 判断下跌加速程度
139
+ ma5_ma20_gap = (ma20 - ma5) / ma20 * 100
140
+
141
+ if ma5_ma20_gap > 5:
142
+ score = 90 # 极高风险
143
+ elif ma5_ma20_gap > 3:
144
+ score = 75 # 高风险
145
+ elif ma5_ma20_gap > 1:
146
+ score = 60 # 中高风险
147
+ else:
148
+ score = 50 # 中等风险
149
+
150
+ elif ma5 > ma20 > ma60:
151
+ trend = "上升"
152
+ score = 20 # 低风险
153
+ else:
154
+ trend = "盘整"
155
+ score = 40 # 中等风险
156
+
157
+ return {
158
+ "score": score,
159
+ "trend": trend,
160
+ "risk_level": "高" if score >= 60 else "中" if score >= 30 else "低"
161
+ }
162
+
163
+ def _analyze_reversal_risk(self, df):
164
+ """分析趋势反转风险"""
165
+ # 获取最新指标
166
+ rsi = df.iloc[-1]['RSI']
167
+ macd = df.iloc[-1]['MACD']
168
+ signal = df.iloc[-1]['Signal']
169
+ price = df.iloc[-1]['close']
170
+ ma20 = df.iloc[-1]['MA20']
171
+
172
+ # 判断潜在趋势反转信号
173
+ reversal_signals = 0
174
+
175
+ # RSI超买/超卖
176
+ if rsi > 75:
177
+ reversal_signals += 1
178
+ direction = "向下"
179
+ elif rsi < 25:
180
+ reversal_signals += 1
181
+ direction = "向上"
182
+ else:
183
+ direction = "无明确方向"
184
+
185
+ # MACD死叉/金叉
186
+ if macd > signal and df.iloc[-2]['MACD'] <= df.iloc[-2]['Signal']:
187
+ reversal_signals += 1
188
+ direction = "向上"
189
+ elif macd < signal and df.iloc[-2]['MACD'] >= df.iloc[-2]['Signal']:
190
+ reversal_signals += 1
191
+ direction = "向下"
192
+
193
+ # 价格与均线关系
194
+ if price > ma20 * 1.1:
195
+ reversal_signals += 1
196
+ direction = "向下"
197
+ elif price < ma20 * 0.9:
198
+ reversal_signals += 1
199
+ direction = "向上"
200
+
201
+ # 评估风险分数
202
+ if reversal_signals >= 3:
203
+ score = 90 # 极高风险
204
+ elif reversal_signals == 2:
205
+ score = 70 # 高风险
206
+ elif reversal_signals == 1:
207
+ score = 40 # 中等风险
208
+ else:
209
+ score = 10 # 低风险
210
+
211
+ return {
212
+ "score": score,
213
+ "reversal_signals": reversal_signals,
214
+ "direction": direction,
215
+ "risk_level": "高" if score >= 60 else "中" if score >= 30 else "低"
216
+ }
217
+
218
+ def _analyze_volume_risk(self, df):
219
+ """分析成交量风险"""
220
+ # 计算成交量变化
221
+ recent_volume = df.iloc[-1]['volume']
222
+ avg_volume = df['volume'].rolling(window=20).mean().iloc[-1]
223
+ volume_ratio = recent_volume / avg_volume
224
+
225
+ # 判断成交量模式
226
+ if volume_ratio > 3:
227
+ pattern = "成交量暴增"
228
+ score = 90 # 极高风险
229
+ elif volume_ratio > 2:
230
+ pattern = "成交量显著放大"
231
+ score = 70 # 高风险
232
+ elif volume_ratio > 1.5:
233
+ pattern = "成交量温和放大"
234
+ score = 50 # 中等风险
235
+ elif volume_ratio < 0.5:
236
+ pattern = "成交量萎缩"
237
+ score = 40 # 中低风险
238
+ else:
239
+ pattern = "成交量正常"
240
+ score = 20 # 低风险
241
+
242
+ # 价格与成交量背离分析
243
+ price_change = (df.iloc[-1]['close'] - df.iloc[-5]['close']) / df.iloc[-5]['close']
244
+ volume_change = (recent_volume - df.iloc[-5]['volume']) / df.iloc[-5]['volume']
245
+
246
+ if price_change > 0.05 and volume_change < -0.3:
247
+ pattern = "价量背离(价格上涨但量能萎缩)"
248
+ score = max(score, 80) # 提高风险评分
249
+ elif price_change < -0.05 and volume_change < -0.3:
250
+ pattern = "价量同向(价格下跌且量能萎缩)"
251
+ score = max(score, 70) # 提高风险评分
252
+ elif price_change < -0.05 and volume_change > 0.5:
253
+ pattern = "价量同向(价格下跌且量能放大)"
254
+ score = max(score, 85) # 提高风险评分
255
+
256
+ return {
257
+ "score": score,
258
+ "volume_ratio": volume_ratio,
259
+ "pattern": pattern,
260
+ "risk_level": "高" if score >= 60 else "中" if score >= 30 else "低"
261
+ }
262
+
263
+ def analyze_portfolio_risk(self, portfolio):
264
+ """分析投资组合整体风险"""
265
+ try:
266
+ if not portfolio or len(portfolio) == 0:
267
+ return {"error": "投资组合为空"}
268
+
269
+ # 分析每只股票的风险
270
+ stock_risks = {}
271
+ total_weight = 0
272
+ weighted_risk_score = 0
273
+
274
+ for stock in portfolio:
275
+ stock_code = stock.get('stock_code')
276
+ weight = stock.get('weight', 1)
277
+ market_type = stock.get('market_type', 'A')
278
+
279
+ if not stock_code:
280
+ continue
281
+
282
+ # 分析股票风险
283
+ risk = self.analyze_stock_risk(stock_code, market_type)
284
+ stock_risks[stock_code] = risk
285
+
286
+ # 计算加权风险分数
287
+ total_weight += weight
288
+ weighted_risk_score += risk.get('total_risk_score', 50) * weight
289
+
290
+ # 计算组合总风险分数
291
+ if total_weight > 0:
292
+ portfolio_risk_score = weighted_risk_score / total_weight
293
+ else:
294
+ portfolio_risk_score = 0
295
+
296
+ # 确定风险等级
297
+ if portfolio_risk_score >= 80:
298
+ risk_level = "极高"
299
+ elif portfolio_risk_score >= 60:
300
+ risk_level = "高"
301
+ elif portfolio_risk_score >= 40:
302
+ risk_level = "中等"
303
+ elif portfolio_risk_score >= 20:
304
+ risk_level = "低"
305
+ else:
306
+ risk_level = "极低"
307
+
308
+ # 收集高风险股票
309
+ high_risk_stocks = [
310
+ {
311
+ "stock_code": code,
312
+ "risk_score": risk.get('total_risk_score', 0),
313
+ "risk_level": risk.get('risk_level', '未知')
314
+ }
315
+ for code, risk in stock_risks.items()
316
+ if risk.get('total_risk_score', 0) >= 60
317
+ ]
318
+
319
+ # 收集所有风险警报
320
+ all_alerts = []
321
+ for code, risk in stock_risks.items():
322
+ for alert in risk.get('alerts', []):
323
+ all_alerts.append({
324
+ "stock_code": code,
325
+ **alert
326
+ })
327
+
328
+ # 分析风险集中度
329
+ risk_concentration = self._analyze_risk_concentration(portfolio, stock_risks)
330
+
331
+ return {
332
+ "portfolio_risk_score": portfolio_risk_score,
333
+ "risk_level": risk_level,
334
+ "high_risk_stocks": high_risk_stocks,
335
+ "alerts": all_alerts,
336
+ "risk_concentration": risk_concentration,
337
+ "stock_risks": stock_risks
338
+ }
339
+
340
+ except Exception as e:
341
+ print(f"分析投资组合风险出错: {str(e)}")
342
+ return {
343
+ "error": f"分析投资组合风险时出错: {str(e)}"
344
+ }
345
+
346
+ def _analyze_risk_concentration(self, portfolio, stock_risks):
347
+ """分析风险集中度"""
348
+ # 分析行业集中度
349
+ industries = {}
350
+ for stock in portfolio:
351
+ stock_code = stock.get('stock_code')
352
+ stock_info = self.analyzer.get_stock_info(stock_code)
353
+ industry = stock_info.get('行业', '未知')
354
+ weight = stock.get('weight', 1)
355
+
356
+ if industry in industries:
357
+ industries[industry] += weight
358
+ else:
359
+ industries[industry] = weight
360
+
361
+ # 找出权重最大的行业
362
+ max_industry = max(industries.items(), key=lambda x: x[1]) if industries else ('未知', 0)
363
+
364
+ # 计算高风险股票总权重
365
+ high_risk_weight = 0
366
+ for stock in portfolio:
367
+ stock_code = stock.get('stock_code')
368
+ if stock_code in stock_risks and stock_risks[stock_code].get('total_risk_score', 0) >= 60:
369
+ high_risk_weight += stock.get('weight', 1)
370
+
371
+ return {
372
+ "max_industry": max_industry[0],
373
+ "max_industry_weight": max_industry[1],
374
+ "high_risk_weight": high_risk_weight
375
+ }
scenario_predictor.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 开发者:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # scenario_predictor.py
9
+ import os
10
+ import numpy as np
11
+ import pandas as pd
12
+ from datetime import datetime, timedelta
13
+ import openai
14
+ """
15
+
16
+ """
17
+
18
+ class ScenarioPredictor:
19
+ def __init__(self, analyzer, openai_api_key=None, openai_model=None):
20
+ self.analyzer = analyzer
21
+ self.openai_api_key = os.getenv('OPENAI_API_KEY', os.getenv('OPENAI_API_KEY'))
22
+ self.openai_api_url = os.getenv('OPENAI_API_URL')
23
+ self.openai_model = os.getenv('OPENAI_API_MODEL', 'gemini-2.0-pro-exp-02-05')
24
+
25
+
26
+ def generate_scenarios(self, stock_code, market_type='A', days=60):
27
+ """生成乐观、中性、悲观三种市场情景预测"""
28
+ try:
29
+ # 获取股票数据和技术指标
30
+ df = self.analyzer.get_stock_data(stock_code, market_type)
31
+ df = self.analyzer.calculate_indicators(df)
32
+
33
+ # 获取股票信息
34
+ stock_info = self.analyzer.get_stock_info(stock_code)
35
+
36
+ # 计算基础数据
37
+ current_price = df.iloc[-1]['close']
38
+ avg_volatility = df['Volatility'].mean()
39
+
40
+ # 根据历史波动率计算情景
41
+ scenarios = self._calculate_scenarios(df, days)
42
+
43
+ # 使用AI生成各情景的分析
44
+ if self.openai_api_key:
45
+ ai_analysis = self._generate_ai_analysis(stock_code, stock_info, df, scenarios)
46
+ scenarios.update(ai_analysis)
47
+
48
+ return scenarios
49
+ except Exception as e:
50
+ print(f"生成情景预测出错: {str(e)}")
51
+ return {}
52
+
53
+ def _calculate_scenarios(self, df, days):
54
+ """基于历史数据计算三种情景的价格预测"""
55
+ current_price = df.iloc[-1]['close']
56
+
57
+ # 计算历史波动率和移动均线
58
+ volatility = df['Volatility'].mean() / 100 # 转换为小数
59
+ daily_volatility = volatility / np.sqrt(252) # 转换为日波动率
60
+ ma20 = df.iloc[-1]['MA20']
61
+ ma60 = df.iloc[-1]['MA60']
62
+
63
+ # 计算乐观情景(上涨至压力位或突破)
64
+ optimistic_return = 0.15 # 15%上涨
65
+ if df.iloc[-1]['BB_upper'] > current_price:
66
+ optimistic_target = df.iloc[-1]['BB_upper'] * 1.05 # 突破上轨5%
67
+ else:
68
+ optimistic_target = current_price * (1 + optimistic_return)
69
+
70
+ # 计算中性情景(震荡,围绕当前价格或20日均线波动)
71
+ neutral_target = (current_price + ma20) / 2
72
+
73
+ # 计算悲观情景(下跌至支撑位或跌破)
74
+ pessimistic_return = -0.12 # 12%下跌
75
+ if df.iloc[-1]['BB_lower'] < current_price:
76
+ pessimistic_target = df.iloc[-1]['BB_lower'] * 0.95 # 跌破下轨5%
77
+ else:
78
+ pessimistic_target = current_price * (1 + pessimistic_return)
79
+
80
+ # 计算预期时间
81
+ time_periods = np.arange(1, days + 1)
82
+
83
+ # 生成乐观路径
84
+ opt_path = [current_price]
85
+ for _ in range(days):
86
+ daily_return = (optimistic_target / current_price) ** (1 / days) - 1
87
+ random_component = np.random.normal(0, daily_volatility)
88
+ new_price = opt_path[-1] * (1 + daily_return + random_component / 2)
89
+ opt_path.append(new_price)
90
+
91
+ # 生成中性路径
92
+ neu_path = [current_price]
93
+ for _ in range(days):
94
+ daily_return = (neutral_target / current_price) ** (1 / days) - 1
95
+ random_component = np.random.normal(0, daily_volatility)
96
+ new_price = neu_path[-1] * (1 + daily_return + random_component)
97
+ neu_path.append(new_price)
98
+
99
+ # 生成悲观路径
100
+ pes_path = [current_price]
101
+ for _ in range(days):
102
+ daily_return = (pessimistic_target / current_price) ** (1 / days) - 1
103
+ random_component = np.random.normal(0, daily_volatility)
104
+ new_price = pes_path[-1] * (1 + daily_return + random_component / 2)
105
+ pes_path.append(new_price)
106
+
107
+ # 生成日期序列
108
+ start_date = datetime.now()
109
+ dates = [(start_date + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(days + 1)]
110
+
111
+ # 组织结果
112
+ return {
113
+ 'current_price': current_price,
114
+ 'optimistic': {
115
+ 'target_price': optimistic_target,
116
+ 'change_percent': (optimistic_target / current_price - 1) * 100,
117
+ 'path': dict(zip(dates, opt_path))
118
+ },
119
+ 'neutral': {
120
+ 'target_price': neutral_target,
121
+ 'change_percent': (neutral_target / current_price - 1) * 100,
122
+ 'path': dict(zip(dates, neu_path))
123
+ },
124
+ 'pessimistic': {
125
+ 'target_price': pessimistic_target,
126
+ 'change_percent': (pessimistic_target / current_price - 1) * 100,
127
+ 'path': dict(zip(dates, pes_path))
128
+ }
129
+ }
130
+
131
+ def _generate_ai_analysis(self, stock_code, stock_info, df, scenarios):
132
+ """使用AI生成各情景的分析说明"""
133
+ try:
134
+ openai.api_key = self.openai_api_key
135
+ openai.api_base = self.openai_api_url
136
+
137
+ # 提取关键数据
138
+ current_price = df.iloc[-1]['close']
139
+ ma5 = df.iloc[-1]['MA5']
140
+ ma20 = df.iloc[-1]['MA20']
141
+ ma60 = df.iloc[-1]['MA60']
142
+ rsi = df.iloc[-1]['RSI']
143
+ macd = df.iloc[-1]['MACD']
144
+ signal = df.iloc[-1]['Signal']
145
+
146
+ # 构建提示词
147
+ prompt = f"""分析股票{stock_code}({stock_info.get('股票名称', '未知')})的三种市场情景:
148
+
149
+ 1. 当前数据:
150
+ - 当前价格: {current_price}
151
+ - 均线: MA5={ma5}, MA20={ma20}, MA60={ma60}
152
+ - RSI: {rsi}
153
+ - MACD: {macd}, Signal: {signal}
154
+
155
+ 2. 预测目标价:
156
+ - 乐观情景: {scenarios['optimistic']['target_price']:.2f} ({scenarios['optimistic']['change_percent']:.2f}%)
157
+ - 中性情景: {scenarios['neutral']['target_price']:.2f} ({scenarios['neutral']['change_percent']:.2f}%)
158
+ - 悲观情景: {scenarios['pessimistic']['target_price']:.2f} ({scenarios['pessimistic']['change_percent']:.2f}%)
159
+
160
+ 请为每种情景提供简短分析(每种情景100字以内),包括可能的触发条件和风险因素。格式为JSON:
161
+ {{
162
+ "optimistic_analysis": "乐观情景分析...",
163
+ "neutral_analysis": "中性情景分析...",
164
+ "pessimistic_analysis": "悲观情景分析..."
165
+ }}
166
+ """
167
+
168
+ # 调用AI API
169
+ response = openai.ChatCompletion.create(
170
+ model=self.openai_model,
171
+ messages=[
172
+ {"role": "system", "content": "你是专业的股票分析师,擅长技术分析和情景预测。"},
173
+ {"role": "user", "content": prompt}
174
+ ],
175
+ temperature=0.7
176
+ )
177
+
178
+ # 解析AI回复
179
+ import json
180
+ try:
181
+ analysis = json.loads(response.choices[0].message.content)
182
+ return analysis
183
+ except:
184
+ # 如果解析失败,尝试从文本中提取JSON
185
+ import re
186
+ json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response.choices[0].message.content)
187
+ if json_match:
188
+ json_str = json_match.group(1)
189
+ return json.loads(json_str)
190
+ else:
191
+ return {
192
+ "optimistic_analysis": "乐观情景分析暂无",
193
+ "neutral_analysis": "中性情景分析暂无",
194
+ "pessimistic_analysis": "悲观情景分析暂无"
195
+ }
196
+ except Exception as e:
197
+ print(f"生成AI分析出错: {str(e)}")
198
+ return {
199
+ "optimistic_analysis": "乐观情景分析暂无",
200
+ "neutral_analysis": "中性情景分析暂无",
201
+ "pessimistic_analysis": "悲观情景分析暂无"
202
+ }
start.sh ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 智能分析系统管理脚本
4
+ # 功能:启动、停止、重启和监控系统服务
5
+
6
+ # 配置
7
+ APP_NAME="智能分析系统"
8
+ APP_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9
+ PYTHON_CMD="python"
10
+ SERVER_SCRIPT="web_server.py"
11
+ PID_FILE="${APP_DIR}/.server.pid"
12
+ LOG_FILE="${APP_DIR}/server.log"
13
+ MONITOR_INTERVAL=30 # 监控检查间隔(秒)
14
+
15
+ # 颜色配置
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ YELLOW='\033[0;33m'
19
+ BLUE='\033[0;34m'
20
+ NC='\033[0m' # 无颜色
21
+
22
+ # 函数:显示帮助信息
23
+ show_help() {
24
+ echo -e "${BLUE}${APP_NAME}管理脚本${NC}"
25
+ echo "使用方法: $0 [命令]"
26
+ echo ""
27
+ echo "命令:"
28
+ echo " start 启动服务"
29
+ echo " stop 停止服务"
30
+ echo " restart 重启服务"
31
+ echo " status 查看服务状态"
32
+ echo " monitor 以守护进程方式监控服务"
33
+ echo " logs 查看日志"
34
+ echo " help 显示此帮助信息"
35
+ }
36
+
37
+ # 函数:检查前置条件
38
+ check_prerequisites() {
39
+ # 检查Python是否已安装
40
+ if ! command -v $PYTHON_CMD &> /dev/null; then
41
+ echo -e "${RED}错误: 未找到Python命令。请确保Python已安装且在PATH中。${NC}"
42
+ exit 1
43
+ fi
44
+
45
+ # 检查server脚本是否存在
46
+ if [ ! -f "${APP_DIR}/${SERVER_SCRIPT}" ]; then
47
+ echo -e "${RED}错误: 未找到服务器脚本 ${SERVER_SCRIPT}。${NC}"
48
+ echo -e "${YELLOW}当前目录: $(pwd)${NC}"
49
+ exit 1
50
+ fi
51
+ }
52
+
53
+ # 函数:获取进程ID
54
+ get_pid() {
55
+ if [ -f "$PID_FILE" ]; then
56
+ local pid=$(cat "$PID_FILE")
57
+ if ps -p $pid > /dev/null; then
58
+ echo $pid
59
+ return 0
60
+ fi
61
+ fi
62
+ # 尝试通过进程名查找
63
+ local pid=$(pgrep -f "python.*${SERVER_SCRIPT}" 2>/dev/null)
64
+ if [ -n "$pid" ]; then
65
+ echo $pid
66
+ return 0
67
+ fi
68
+ echo ""
69
+ return 1
70
+ }
71
+
72
+ # 函数:启动服务
73
+ start_server() {
74
+ local pid=$(get_pid)
75
+ if [ -n "$pid" ]; then
76
+ echo -e "${YELLOW}警告: 服务已在运行 (PID: $pid)${NC}"
77
+ return 0
78
+ fi
79
+
80
+ echo -e "${BLUE}正在启动${APP_NAME}...${NC}"
81
+ cd "$APP_DIR"
82
+
83
+ # 使用nohup在后台启动服务
84
+ nohup $PYTHON_CMD $SERVER_SCRIPT > "$LOG_FILE" 2>&1 &
85
+ local new_pid=$!
86
+
87
+ # 保存PID到文件
88
+ echo $new_pid > "$PID_FILE"
89
+
90
+ # 等待几秒检查服务是否成功启动
91
+ sleep 3
92
+ if ps -p $new_pid > /dev/null; then
93
+ echo -e "${GREEN}${APP_NAME}已成功启动 (PID: $new_pid)${NC}"
94
+ return 0
95
+ else
96
+ echo -e "${RED}启动${APP_NAME}失败。查看日志获取更多信息: ${LOG_FILE}${NC}"
97
+ return 1
98
+ fi
99
+ }
100
+
101
+ # 函数:停止服务
102
+ stop_server() {
103
+ local pid=$(get_pid)
104
+ if [ -z "$pid" ]; then
105
+ echo -e "${YELLOW}服务未运行${NC}"
106
+ return 0
107
+ fi
108
+
109
+ echo -e "${BLUE}正在停止${APP_NAME} (PID: $pid)...${NC}"
110
+
111
+ # 尝试优雅地停止服务
112
+ kill -15 $pid
113
+
114
+ # 等待服务停止
115
+ local max_wait=10
116
+ local waited=0
117
+ while ps -p $pid > /dev/null && [ $waited -lt $max_wait ]; do
118
+ sleep 1
119
+ waited=$((waited + 1))
120
+ echo -ne "${YELLOW}等待服务停止 $waited/$max_wait ${NC}\r"
121
+ done
122
+ echo ""
123
+
124
+ # 如果服务仍在运行,强制停止
125
+ if ps -p $pid > /dev/null; then
126
+ echo -e "${YELLOW}服务未响应优雅停止请求,正在强制终止...${NC}"
127
+ kill -9 $pid
128
+ sleep 1
129
+ fi
130
+
131
+ # 检查服务是否已停止
132
+ if ps -p $pid > /dev/null; then
133
+ echo -e "${RED}无法停止服务 (PID: $pid)${NC}"
134
+ return 1
135
+ else
136
+ echo -e "${GREEN}服务已成功停止${NC}"
137
+ rm -f "$PID_FILE"
138
+ return 0
139
+ fi
140
+ }
141
+
142
+ # 函数:重启服务
143
+ restart_server() {
144
+ echo -e "${BLUE}正在重启${APP_NAME}...${NC}"
145
+ stop_server
146
+ sleep 2
147
+ start_server
148
+ }
149
+
150
+ # 函数:检查服务状态
151
+ check_status() {
152
+ local pid=$(get_pid)
153
+ if [ -n "$pid" ]; then
154
+ local uptime=$(ps -o etime= -p $pid)
155
+ local mem=$(ps -o %mem= -p $pid)
156
+ local cpu=$(ps -o %cpu= -p $pid)
157
+
158
+ echo -e "${GREEN}${APP_NAME}正在运行${NC}"
159
+ echo -e " PID: ${BLUE}$pid${NC}"
160
+ echo -e " 运行时间: ${BLUE}$uptime${NC}"
161
+ echo -e " 内存使用: ${BLUE}$mem%${NC}"
162
+ echo -e " CPU使用: ${BLUE}$cpu%${NC}"
163
+ echo -e " 日志文件: ${BLUE}$LOG_FILE${NC}"
164
+ return 0
165
+ else
166
+ echo -e "${YELLOW}${APP_NAME}未运行${NC}"
167
+ return 1
168
+ fi
169
+ }
170
+
171
+ # 函数:监控服务
172
+ monitor_server() {
173
+ echo -e "${BLUE}开始监控${APP_NAME}...${NC}"
174
+ echo -e "${BLUE}监控日志将写入: ${LOG_FILE}.monitor${NC}"
175
+ echo -e "${YELLOW}按 Ctrl+C 停止监控${NC}"
176
+
177
+ # 在后台启动监控
178
+ (
179
+ while true; do
180
+ local pid=$(get_pid)
181
+ local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
182
+
183
+ if [ -z "$pid" ]; then
184
+ echo "$timestamp - 服务未运行,正在重启..." >> "${LOG_FILE}.monitor"
185
+ cd "$APP_DIR"
186
+ $PYTHON_CMD $SERVER_SCRIPT >> "$LOG_FILE" 2>&1 &
187
+ local new_pid=$!
188
+ echo $new_pid > "$PID_FILE"
189
+ echo "$timestamp - 服务已重启 (PID: $new_pid)" >> "${LOG_FILE}.monitor"
190
+ else
191
+ # 检查服务是否响应 (可以通过访问服务API实现)
192
+ local is_responsive=true
193
+
194
+ # 这里可以添加额外的健康检查逻辑
195
+ # 例如:使用curl检查API是否响应
196
+ # if ! curl -s http://localhost:8888/health > /dev/null; then
197
+ # is_responsive=false
198
+ # fi
199
+
200
+ if [ "$is_responsive" = false ]; then
201
+ echo "$timestamp - 服务无响应,正在重启..." >> "${LOG_FILE}.monitor"
202
+ kill -9 $pid
203
+ sleep 2
204
+ cd "$APP_DIR"
205
+ $PYTHON_CMD $SERVER_SCRIPT >> "$LOG_FILE" 2>&1 &
206
+ local new_pid=$!
207
+ echo $new_pid > "$PID_FILE"
208
+ echo "$timestamp - 服务已重启 (PID: $new_pid)" >> "${LOG_FILE}.monitor"
209
+ fi
210
+ fi
211
+
212
+ sleep $MONITOR_INTERVAL
213
+ done
214
+ ) &
215
+
216
+ # 保存监控进程PID
217
+ MONITOR_PID=$!
218
+ echo $MONITOR_PID > "${APP_DIR}/.monitor.pid"
219
+ echo -e "${GREEN}监控进程已启动 (PID: $MONITOR_PID)${NC}"
220
+
221
+ # 捕获Ctrl+C以停止监控
222
+ trap 'kill $MONITOR_PID; echo -e "${YELLOW}监控已停止${NC}"; rm -f "${APP_DIR}/.monitor.pid"' INT
223
+
224
+ # 等待监控进程
225
+ wait $MONITOR_PID
226
+ }
227
+
228
+ # 函数:查看日志
229
+ view_logs() {
230
+ if [ ! -f "$LOG_FILE" ]; then
231
+ echo -e "${YELLOW}日志文件不存在: ${LOG_FILE}${NC}"
232
+ return 1
233
+ fi
234
+
235
+ echo -e "${BLUE}显示最新的日志内容 (按Ctrl+C退出)${NC}"
236
+ tail -f "$LOG_FILE"
237
+ }
238
+
239
+ # 主函数
240
+ main() {
241
+ check_prerequisites
242
+
243
+ local command=${1:-"help"}
244
+
245
+ case $command in
246
+ start)
247
+ start_server
248
+ ;;
249
+ stop)
250
+ stop_server
251
+ ;;
252
+ restart)
253
+ restart_server
254
+ ;;
255
+ status)
256
+ check_status
257
+ ;;
258
+ monitor)
259
+ monitor_server
260
+ ;;
261
+ logs)
262
+ view_logs
263
+ ;;
264
+ *)
265
+ show_help
266
+ ;;
267
+ esac
268
+ }
269
+
270
+ # 执行主函数
271
+ main "$@"
static/favicon.ico ADDED

Git LFS Details

  • SHA256: cccb8db926822477a84ea0d6a8316b4c16c7348254d81911e87449afc3b41458
  • Pointer size: 131 Bytes
  • Size of remote file: 270 kB
static/swagger.json ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "swagger": "2.0",
3
+ "info": {
4
+ "title": "股票智能分析系统 API",
5
+ "description": "股票智能分析系统的REST API文档",
6
+ "version": "2.1.0"
7
+ },
8
+ "host": "localhost:8888",
9
+ "basePath": "/",
10
+ "schemes": ["http", "https"],
11
+ "paths": {
12
+ "/analyze": {
13
+ "post": {
14
+ "summary": "分析股票",
15
+ "description": "分析单只或多只股票",
16
+ "parameters": [
17
+ {
18
+ "name": "body",
19
+ "in": "body",
20
+ "required": true,
21
+ "schema": {
22
+ "type": "object",
23
+ "properties": {
24
+ "stock_codes": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "string"
28
+ },
29
+ "example": ["600519", "000858"]
30
+ },
31
+ "market_type": {
32
+ "type": "string",
33
+ "example": "A"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ],
39
+ "responses": {
40
+ "200": {
41
+ "description": "成功分析股票"
42
+ },
43
+ "400": {
44
+ "description": "请求参数错误"
45
+ },
46
+ "500": {
47
+ "description": "服务器内部错误"
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "/api/start_stock_analysis": {
53
+ "post": {
54
+ "summary": "启动股票分析任务",
55
+ "description": "启动异步股票分析任务",
56
+ "parameters": [
57
+ {
58
+ "name": "body",
59
+ "in": "body",
60
+ "required": true,
61
+ "schema": {
62
+ "type": "object",
63
+ "properties": {
64
+ "stock_code": {
65
+ "type": "string",
66
+ "example": "600519"
67
+ },
68
+ "market_type": {
69
+ "type": "string",
70
+ "example": "A"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ ],
76
+ "responses": {
77
+ "200": {
78
+ "description": "成功启动分析任务"
79
+ }
80
+ }
81
+ }
82
+ },
83
+ "/api/analysis_status/{task_id}": {
84
+ "get": {
85
+ "summary": "获取分析任务状态",
86
+ "description": "获取异步分析任务的状态和结果",
87
+ "parameters": [
88
+ {
89
+ "name": "task_id",
90
+ "in": "path",
91
+ "required": true,
92
+ "type": "string"
93
+ }
94
+ ],
95
+ "responses": {
96
+ "200": {
97
+ "description": "成功获取任务状态和结果"
98
+ },
99
+ "404": {
100
+ "description": "找不到指定的任务"
101
+ }
102
+ }
103
+ }
104
+ },
105
+ "/api/stock_data": {
106
+ "get": {
107
+ "summary": "获取股票数据",
108
+ "description": "获取股票历史数据和技术指标",
109
+ "parameters": [
110
+ {
111
+ "name": "stock_code",
112
+ "in": "query",
113
+ "required": true,
114
+ "type": "string"
115
+ },
116
+ {
117
+ "name": "market_type",
118
+ "in": "query",
119
+ "required": false,
120
+ "type": "string",
121
+ "default": "A"
122
+ },
123
+ {
124
+ "name": "period",
125
+ "in": "query",
126
+ "required": false,
127
+ "type": "string",
128
+ "enum": ["1m", "3m", "6m", "1y"],
129
+ "default": "1y"
130
+ }
131
+ ],
132
+ "responses": {
133
+ "200": {
134
+ "description": "成功获取股票数据"
135
+ },
136
+ "400": {
137
+ "description": "请求参数错误"
138
+ },
139
+ "500": {
140
+ "description": "服务器内部错误"
141
+ }
142
+ }
143
+ }
144
+ },
145
+ "/api/start_market_scan": {
146
+ "post": {
147
+ "summary": "启动市场扫描任务",
148
+ "description": "启动异步市场扫描任务",
149
+ "parameters": [
150
+ {
151
+ "name": "body",
152
+ "in": "body",
153
+ "required": true,
154
+ "schema": {
155
+ "type": "object",
156
+ "properties": {
157
+ "stock_list": {
158
+ "type": "array",
159
+ "items": {
160
+ "type": "string"
161
+ },
162
+ "example": ["600519", "000858"]
163
+ },
164
+ "min_score": {
165
+ "type": "integer",
166
+ "example": 60
167
+ },
168
+ "market_type": {
169
+ "type": "string",
170
+ "example": "A"
171
+ }
172
+ }
173
+ }
174
+ }
175
+ ],
176
+ "responses": {
177
+ "200": {
178
+ "description": "成功启动扫描任务"
179
+ }
180
+ }
181
+ }
182
+ },
183
+ "/api/scan_status/{task_id}": {
184
+ "get": {
185
+ "summary": "���取扫描任务状态",
186
+ "description": "获取异步扫描任务的状态和结果",
187
+ "parameters": [
188
+ {
189
+ "name": "task_id",
190
+ "in": "path",
191
+ "required": true,
192
+ "type": "string"
193
+ }
194
+ ],
195
+ "responses": {
196
+ "200": {
197
+ "description": "成功获取任务状态和结果"
198
+ },
199
+ "404": {
200
+ "description": "找不到指定的任务"
201
+ }
202
+ }
203
+ }
204
+ },
205
+ "/api/index_stocks": {
206
+ "get": {
207
+ "summary": "获取指数成分股",
208
+ "description": "获取指定指数的成分股列表",
209
+ "parameters": [
210
+ {
211
+ "name": "index_code",
212
+ "in": "query",
213
+ "required": true,
214
+ "type": "string",
215
+ "example": "000300"
216
+ }
217
+ ],
218
+ "responses": {
219
+ "200": {
220
+ "description": "成功获取指数成分股"
221
+ },
222
+ "400": {
223
+ "description": "请求参数错误"
224
+ },
225
+ "500": {
226
+ "description": "服务器内部错误"
227
+ }
228
+ }
229
+ }
230
+ },
231
+ "/api/industry_stocks": {
232
+ "get": {
233
+ "summary": "获取行业成分股",
234
+ "description": "获取指定行业的成分股列表",
235
+ "parameters": [
236
+ {
237
+ "name": "industry",
238
+ "in": "query",
239
+ "required": true,
240
+ "type": "string",
241
+ "example": "银行"
242
+ }
243
+ ],
244
+ "responses": {
245
+ "200": {
246
+ "description": "成功获取行业成分股"
247
+ },
248
+ "400": {
249
+ "description": "请求参数错误"
250
+ },
251
+ "500": {
252
+ "description": "服务器内部错误"
253
+ }
254
+ }
255
+ }
256
+ },
257
+ "/api/fundamental_analysis": {
258
+ "post": {
259
+ "summary": "基本面分析",
260
+ "description": "获取股票的基本面分析结果",
261
+ "parameters": [
262
+ {
263
+ "name": "body",
264
+ "in": "body",
265
+ "required": true,
266
+ "schema": {
267
+ "type": "object",
268
+ "properties": {
269
+ "stock_code": {
270
+ "type": "string",
271
+ "example": "600519"
272
+ }
273
+ }
274
+ }
275
+ }
276
+ ],
277
+ "responses": {
278
+ "200": {
279
+ "description": "成功获取基本面分析结果"
280
+ }
281
+ }
282
+ }
283
+ },
284
+ "/api/capital_flow": {
285
+ "post": {
286
+ "summary": "资金流向分析",
287
+ "description": "获取股票的资金流向分析结果",
288
+ "parameters": [
289
+ {
290
+ "name": "body",
291
+ "in": "body",
292
+ "required": true,
293
+ "schema": {
294
+ "type": "object",
295
+ "properties": {
296
+ "stock_code": {
297
+ "type": "string",
298
+ "example": "600519"
299
+ },
300
+ "days": {
301
+ "type": "integer",
302
+ "example": 10
303
+ }
304
+ }
305
+ }
306
+ }
307
+ ],
308
+ "responses": {
309
+ "200": {
310
+ "description": "成功获取资金流向分析结果"
311
+ }
312
+ }
313
+ }
314
+ },
315
+ "/api/scenario_predict": {
316
+ "post": {
317
+ "summary": "情景预测",
318
+ "description": "获取股票的多情景预测结果",
319
+ "parameters": [
320
+ {
321
+ "name": "body",
322
+ "in": "body",
323
+ "required": true,
324
+ "schema": {
325
+ "type": "object",
326
+ "properties": {
327
+ "stock_code": {
328
+ "type": "string",
329
+ "example": "600519"
330
+ },
331
+ "market_type": {
332
+ "type": "string",
333
+ "example": "A"
334
+ },
335
+ "days": {
336
+ "type": "integer",
337
+ "example": 60
338
+ }
339
+ }
340
+ }
341
+ }
342
+ ],
343
+ "responses": {
344
+ "200": {
345
+ "description": "成功获取情景预测结果"
346
+ }
347
+ }
348
+ }
349
+ },
350
+ "/api/qa": {
351
+ "post": {
352
+ "summary": "智能问答",
353
+ "description": "获取股票相关问题的智能回答",
354
+ "parameters": [
355
+ {
356
+ "name": "body",
357
+ "in": "body",
358
+ "required": true,
359
+ "schema": {
360
+ "type": "object",
361
+ "properties": {
362
+ "stock_code": {
363
+ "type": "string",
364
+ "example": "600519"
365
+ },
366
+ "question": {
367
+ "type": "string",
368
+ "example": "这只股票的主要支撑位是多少?"
369
+ },
370
+ "market_type": {
371
+ "type": "string",
372
+ "example": "A"
373
+ }
374
+ }
375
+ }
376
+ }
377
+ ],
378
+ "responses": {
379
+ "200": {
380
+ "description": "成功获取智能回答"
381
+ }
382
+ }
383
+ }
384
+ },
385
+ "/api/risk_analysis": {
386
+ "post": {
387
+ "summary": "风险分析",
388
+ "description": "获取股票的风险分析结果",
389
+ "parameters": [
390
+ {
391
+ "name": "body",
392
+ "in": "body",
393
+ "required": true,
394
+ "schema": {
395
+ "type": "object",
396
+ "properties": {
397
+ "stock_code": {
398
+ "type": "string",
399
+ "example": "600519"
400
+ },
401
+ "market_type": {
402
+ "type": "string",
403
+ "example": "A"
404
+ }
405
+ }
406
+ }
407
+ }
408
+ ],
409
+ "responses": {
410
+ "200": {
411
+ "description": "成功获取风险分析结果"
412
+ }
413
+ }
414
+ }
415
+ },
416
+ "/api/portfolio_risk": {
417
+ "post": {
418
+ "summary": "投资组合风险分析",
419
+ "description": "获取投资组合的整体风险分析结果",
420
+ "parameters": [
421
+ {
422
+ "name": "body",
423
+ "in": "body",
424
+ "required": true,
425
+ "schema": {
426
+ "type": "object",
427
+ "properties": {
428
+ "portfolio": {
429
+ "type": "array",
430
+ "items": {
431
+ "type": "object",
432
+ "properties": {
433
+ "stock_code": {
434
+ "type": "string"
435
+ },
436
+ "weight": {
437
+ "type": "number"
438
+ },
439
+ "market_type": {
440
+ "type": "string"
441
+ }
442
+ }
443
+ },
444
+ "example": [
445
+ {
446
+ "stock_code": "600519",
447
+ "weight": 30,
448
+ "market_type": "A"
449
+ },
450
+ {
451
+ "stock_code": "000858",
452
+ "weight": 20,
453
+ "market_type": "A"
454
+ }
455
+ ]
456
+ }
457
+ }
458
+ }
459
+ }
460
+ ],
461
+ "responses": {
462
+ "200": {
463
+ "description": "成功获取投资组合风险分析结果"
464
+ }
465
+ }
466
+ }
467
+ },
468
+ "/api/index_analysis": {
469
+ "get": {
470
+ "summary": "指数分析",
471
+ "description": "获取指数的整体分析结果",
472
+ "parameters": [
473
+ {
474
+ "name": "index_code",
475
+ "in": "query",
476
+ "required": true,
477
+ "type": "string",
478
+ "example": "000300"
479
+ },
480
+ {
481
+ "name": "limit",
482
+ "in": "query",
483
+ "required": false,
484
+ "type": "integer",
485
+ "example": 30
486
+ }
487
+ ],
488
+ "responses": {
489
+ "200": {
490
+ "description": "成功获取指数分析结果"
491
+ }
492
+ }
493
+ }
494
+ },
495
+ "/api/industry_analysis": {
496
+ "get": {
497
+ "summary": "行业分析",
498
+ "description": "获取行业的整体分析结果",
499
+ "parameters": [
500
+ {
501
+ "name": "industry",
502
+ "in": "query",
503
+ "required": true,
504
+ "type": "string",
505
+ "example": "银行"
506
+ },
507
+ {
508
+ "name": "limit",
509
+ "in": "query",
510
+ "required": false,
511
+ "type": "integer",
512
+ "example": 30
513
+ }
514
+ ],
515
+ "responses": {
516
+ "200": {
517
+ "description": "成功获取行业分析结果"
518
+ }
519
+ }
520
+ }
521
+ },
522
+ "/api/industry_compare": {
523
+ "get": {
524
+ "summary": "行业比较",
525
+ "description": "比较不同行业的表现",
526
+ "parameters": [
527
+ {
528
+ "name": "limit",
529
+ "in": "query",
530
+ "required": false,
531
+ "type": "integer",
532
+ "example": 10
533
+ }
534
+ ],
535
+ "responses": {
536
+ "200": {
537
+ "description": "成功获取行业比较结果"
538
+ }
539
+ }
540
+ }
541
+ }
542
+ },
543
+ "definitions": {
544
+ "Stock": {
545
+ "type": "object",
546
+ "properties": {
547
+ "stock_code": {
548
+ "type": "string"
549
+ },
550
+ "stock_name": {
551
+ "type": "string"
552
+ },
553
+ "price": {
554
+ "type": "number"
555
+ },
556
+ "price_change": {
557
+ "type": "number"
558
+ }
559
+ }
560
+ },
561
+ "AnalysisResult": {
562
+ "type": "object",
563
+ "properties": {
564
+ "score": {
565
+ "type": "number"
566
+ },
567
+ "recommendation": {
568
+ "type": "string"
569
+ }
570
+ }
571
+ }
572
+ }
573
+ }
stock_analyzer.py ADDED
@@ -0,0 +1,2131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 修改:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # stock_analyzer.py
9
+ import time
10
+ import traceback
11
+ import pandas as pd
12
+ import numpy as np
13
+ from datetime import datetime, timedelta
14
+ import os
15
+ import requests
16
+ from typing import Dict, List, Optional, Tuple
17
+ from dotenv import load_dotenv
18
+ import logging
19
+ import math
20
+ import json
21
+ import threading
22
+
23
+ # 线程局部存储
24
+ thread_local = threading.local()
25
+
26
+
27
+ class StockAnalyzer:
28
+ """
29
+ 股票分析器 - 原有API保持不变,内部实现增强
30
+ """
31
+
32
+ def __init__(self, initial_cash=1000000):
33
+ # 设置日志
34
+ logging.basicConfig(level=logging.INFO,
35
+ format='%(asctime)s - %(levelname)s - %(message)s')
36
+ self.logger = logging.getLogger(__name__)
37
+
38
+ # 加载环境变量
39
+ load_dotenv()
40
+
41
+ # 设置 OpenAI API (原 Gemini API)
42
+ self.openai_api_key = os.getenv('OPENAI_API_KEY', os.getenv('OPENAI_API_KEY'))
43
+ self.openai_api_url = os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1')
44
+ self.openai_model = os.getenv('OPENAI_API_MODEL', 'gemini-2.0-pro-exp-02-05')
45
+ self.news_model = os.getenv('NEWS_MODEL')
46
+
47
+ # 配置参数
48
+ self.params = {
49
+ 'ma_periods': {'short': 5, 'medium': 20, 'long': 60},
50
+ 'rsi_period': 14,
51
+ 'bollinger_period': 20,
52
+ 'bollinger_std': 2,
53
+ 'volume_ma_period': 20,
54
+ 'atr_period': 14
55
+ }
56
+
57
+ # 添加缓存初始化
58
+ self.data_cache = {}
59
+
60
+ # JSON匹配标志
61
+ self.json_match_flag = True
62
+ def get_stock_data(self, stock_code, market_type='A', start_date=None, end_date=None):
63
+ """获取股票数据"""
64
+ import akshare as ak
65
+
66
+ self.logger.info(f"开始获取股票 {stock_code} 数据,市场类型: {market_type}")
67
+
68
+ cache_key = f"{stock_code}_{market_type}_{start_date}_{end_date}_price"
69
+ if cache_key in self.data_cache:
70
+ cached_df = self.data_cache[cache_key]
71
+ # 创建一个副本以避免修改缓存数据
72
+ # 并确保副本的日期类型为datetime
73
+ result = cached_df.copy()
74
+ # If 'date' column exists but is not datetime, convert it
75
+ if 'date' in result.columns and not pd.api.types.is_datetime64_any_dtype(result['date']):
76
+ try:
77
+ result['date'] = pd.to_datetime(result['date'])
78
+ except Exception as e:
79
+ self.logger.warning(f"无法将日期列转换为datetime格式: {str(e)}")
80
+ return result
81
+
82
+ if start_date is None:
83
+ start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
84
+ if end_date is None:
85
+ end_date = datetime.now().strftime('%Y%m%d')
86
+
87
+ try:
88
+ # 根据市场类型获取数据
89
+ if market_type == 'A':
90
+ df = ak.stock_zh_a_hist(
91
+ symbol=stock_code,
92
+ start_date=start_date,
93
+ end_date=end_date,
94
+ adjust="qfq"
95
+ )
96
+ elif market_type == 'HK':
97
+ df = ak.stock_hk_daily(
98
+ symbol=stock_code,
99
+ adjust="qfq"
100
+ )
101
+ elif market_type == 'US':
102
+ df = ak.stock_us_hist(
103
+ symbol=stock_code,
104
+ start_date=start_date,
105
+ end_date=end_date,
106
+ adjust="qfq"
107
+ )
108
+ else:
109
+ raise ValueError(f"不支持的市场类型: {market_type}")
110
+
111
+ # 重命名列名以匹配分析需求
112
+ df = df.rename(columns={
113
+ "日期": "date",
114
+ "开盘": "open",
115
+ "收盘": "close",
116
+ "最高": "high",
117
+ "最低": "low",
118
+ "成交量": "volume",
119
+ "成交额": "amount"
120
+ })
121
+
122
+ # 确保日期格式正确
123
+ df['date'] = pd.to_datetime(df['date'])
124
+
125
+ # 数据类型转换
126
+ numeric_columns = ['open', 'close', 'high', 'low', 'volume']
127
+ for col in numeric_columns:
128
+ if col in df.columns:
129
+ df[col] = pd.to_numeric(df[col], errors='coerce')
130
+
131
+ # 删除空值
132
+ df = df.dropna()
133
+
134
+ result = df.sort_values('date')
135
+
136
+ # 缓存原始数据(包含datetime类型)
137
+ self.data_cache[cache_key] = result.copy()
138
+
139
+ return result
140
+
141
+ except Exception as e:
142
+ self.logger.error(f"获取股票数据失败: {e}")
143
+ raise Exception(f"获取股票数据失败: {e}")
144
+
145
+ def get_north_flow_history(self, stock_code, start_date=None, end_date=None):
146
+ """获取单个股票的北向资金历史持股数据"""
147
+ try:
148
+ import akshare as ak
149
+
150
+ # 获取历史持股数据
151
+ if start_date is None and end_date is None:
152
+ # 默认获取近90天数据
153
+ north_hist_data = ak.stock_hsgt_hist_em(symbol=stock_code)
154
+ else:
155
+ north_hist_data = ak.stock_hsgt_hist_em(symbol=stock_code, start_date=start_date, end_date=end_date)
156
+
157
+ if north_hist_data.empty:
158
+ return {"history": []}
159
+
160
+ # 转换为列表格式返回
161
+ history = []
162
+ for _, row in north_hist_data.iterrows():
163
+ history.append({
164
+ "date": row.get('日期', ''),
165
+ "holding": float(row.get('持股数', 0)) if '持股数' in row else 0,
166
+ "ratio": float(row.get('持股比例', 0)) if '持股比例' in row else 0,
167
+ "change": float(row.get('持股变动', 0)) if '持股变动' in row else 0,
168
+ "market_value": float(row.get('持股市值', 0)) if '持股市值' in row else 0
169
+ })
170
+
171
+ return {"history": history}
172
+ except Exception as e:
173
+ self.logger.error(f"获取北向资金历史数据出错: {str(e)}")
174
+ return {"history": []}
175
+
176
+ def calculate_ema(self, series, period):
177
+ """计算指数移动平均线"""
178
+ return series.ewm(span=period, adjust=False).mean()
179
+
180
+ def calculate_rsi(self, series, period):
181
+ """计算RSI指标"""
182
+ delta = series.diff()
183
+ gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
184
+ loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
185
+ rs = gain / loss
186
+ return 100 - (100 / (1 + rs))
187
+
188
+ def calculate_macd(self, series):
189
+ """计算MACD指标"""
190
+ exp1 = series.ewm(span=12, adjust=False).mean()
191
+ exp2 = series.ewm(span=26, adjust=False).mean()
192
+ macd = exp1 - exp2
193
+ signal = macd.ewm(span=9, adjust=False).mean()
194
+ hist = macd - signal
195
+ return macd, signal, hist
196
+
197
+ def calculate_bollinger_bands(self, series, period, std_dev):
198
+ """计算布林带"""
199
+ middle = series.rolling(window=period).mean()
200
+ std = series.rolling(window=period).std()
201
+ upper = middle + (std * std_dev)
202
+ lower = middle - (std * std_dev)
203
+ return upper, middle, lower
204
+
205
+ def calculate_atr(self, df, period):
206
+ """计算ATR指标"""
207
+ high = df['high']
208
+ low = df['low']
209
+ close = df['close'].shift(1)
210
+
211
+ tr1 = high - low
212
+ tr2 = abs(high - close)
213
+ tr3 = abs(low - close)
214
+
215
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
216
+ return tr.rolling(window=period).mean()
217
+
218
+ def format_indicator_data(self, df):
219
+ """格式化指标数据,控制小数位数"""
220
+
221
+ # 格式化价格数据 (2位小数)
222
+ price_columns = ['open', 'close', 'high', 'low', 'MA5', 'MA20', 'MA60', 'BB_upper', 'BB_middle', 'BB_lower']
223
+ for col in price_columns:
224
+ if col in df.columns:
225
+ df[col] = df[col].round(2)
226
+
227
+ # 格式化MACD相关指标 (3位小数)
228
+ macd_columns = ['MACD', 'Signal', 'MACD_hist']
229
+ for col in macd_columns:
230
+ if col in df.columns:
231
+ df[col] = df[col].round(3)
232
+
233
+ # 格式化其他技术指标 (2位小数)
234
+ other_columns = ['RSI', 'Volatility', 'ROC', 'Volume_Ratio']
235
+ for col in other_columns:
236
+ if col in df.columns:
237
+ df[col] = df[col].round(2)
238
+
239
+ return df
240
+
241
+ def calculate_indicators(self, df):
242
+ """计算技术指标"""
243
+
244
+ try:
245
+ # 计算移动平均线
246
+ df['MA5'] = self.calculate_ema(df['close'], self.params['ma_periods']['short'])
247
+ df['MA20'] = self.calculate_ema(df['close'], self.params['ma_periods']['medium'])
248
+ df['MA60'] = self.calculate_ema(df['close'], self.params['ma_periods']['long'])
249
+
250
+ # 计算RSI
251
+ df['RSI'] = self.calculate_rsi(df['close'], self.params['rsi_period'])
252
+
253
+ # 计算MACD
254
+ df['MACD'], df['Signal'], df['MACD_hist'] = self.calculate_macd(df['close'])
255
+
256
+ # 计算布林带
257
+ df['BB_upper'], df['BB_middle'], df['BB_lower'] = self.calculate_bollinger_bands(
258
+ df['close'],
259
+ self.params['bollinger_period'],
260
+ self.params['bollinger_std']
261
+ )
262
+
263
+ # 成交量分析
264
+ df['Volume_MA'] = df['volume'].rolling(window=self.params['volume_ma_period']).mean()
265
+ df['Volume_Ratio'] = df['volume'] / df['Volume_MA']
266
+
267
+ # 计算ATR和波动率
268
+ df['ATR'] = self.calculate_atr(df, self.params['atr_period'])
269
+ df['Volatility'] = df['ATR'] / df['close'] * 100
270
+
271
+ # 动量指标
272
+ df['ROC'] = df['close'].pct_change(periods=10) * 100
273
+
274
+ # 格式化数据
275
+ df = self.format_indicator_data(df)
276
+
277
+ return df
278
+
279
+ except Exception as e:
280
+ self.logger.error(f"计算技术指标时出错: {str(e)}")
281
+ raise
282
+
283
+ def calculate_score(self, df, market_type='A'):
284
+ """
285
+ 计算股票评分 - 使用时空共振交易系统增强
286
+ 根据不同的市场特征调整评分权重和标准
287
+ """
288
+ try:
289
+ score = 0
290
+ latest = df.iloc[-1]
291
+ prev_days = min(30, len(df) - 1) # Get the most recent 30 days or all available data
292
+
293
+ # 时空共振框架 - 维度1:多时间框架分析
294
+ # 基础权重配置
295
+ weights = {
296
+ 'trend': 0.30, # 趋势因子权重(日线级别)
297
+ 'volatility': 0.15, # 波动率因子权重
298
+ 'technical': 0.25, # 技术指标因子权重
299
+ 'volume': 0.20, # 成交量因子权重(能量守恒维度)
300
+ 'momentum': 0.10 # 动量因子权重(周线级别)
301
+ }
302
+
303
+ # 根据市场类型调整权重(维度1:时间框架嵌套)
304
+ if market_type == 'US':
305
+ # 美股优先考虑长期趋势
306
+ weights['trend'] = 0.35
307
+ weights['volatility'] = 0.10
308
+ weights['momentum'] = 0.15
309
+ elif market_type == 'HK':
310
+ # 港股调整波动率和成交量权重
311
+ weights['volatility'] = 0.20
312
+ weights['volume'] = 0.25
313
+
314
+ # 1. 趋势评分(最高30分)- 日线级别分析
315
+ trend_score = 0
316
+
317
+ # 均线评估 - "三线形态"分析
318
+ if latest['MA5'] > latest['MA20'] and latest['MA20'] > latest['MA60']:
319
+ # 完美多头排列(维度1:日线形态)
320
+ trend_score += 15
321
+ elif latest['MA5'] > latest['MA20']:
322
+ # 短期上升趋势(维度1:5分钟形态)
323
+ trend_score += 10
324
+ elif latest['MA20'] > latest['MA60']:
325
+ # 中期上升趋势
326
+ trend_score += 5
327
+
328
+ # 价格位置评估
329
+ if latest['close'] > latest['MA5']:
330
+ trend_score += 5
331
+ if latest['close'] > latest['MA20']:
332
+ trend_score += 5
333
+ if latest['close'] > latest['MA60']:
334
+ trend_score += 5
335
+
336
+ # 确保不超过最高分数限制
337
+ trend_score = min(30, trend_score)
338
+
339
+ # 2. 波动率评分(最高15分)- 维度2:过滤
340
+ volatility_score = 0
341
+
342
+ # 适度的波动率最理想
343
+ volatility = latest['Volatility']
344
+ if 1.0 <= volatility <= 2.5:
345
+ # 最佳波动率范围
346
+ volatility_score += 15
347
+ elif 2.5 < volatility <= 4.0:
348
+ # 较高波动率,次优选择
349
+ volatility_score += 10
350
+ elif volatility < 1.0:
351
+ # 波动率过低,缺乏能量
352
+ volatility_score += 5
353
+ else:
354
+ # 波动率过高,风险较大
355
+ volatility_score += 0
356
+
357
+ # 3. 技术指标评分(最高25分)- "峰值检测系统"
358
+ technical_score = 0
359
+
360
+ # RSI指标评估(10分)
361
+ rsi = latest['RSI']
362
+ if 40 <= rsi <= 60:
363
+ # 中性区域,趋势稳定
364
+ technical_score += 7
365
+ elif 30 <= rsi < 40 or 60 < rsi <= 70:
366
+ # 阈值区域,可能出现反转信号
367
+ technical_score += 10
368
+ elif rsi < 30:
369
+ # 超卖区域,可能出现买入机会
370
+ technical_score += 8
371
+ elif rsi > 70:
372
+ # 超买区域,可能存在卖出风险
373
+ technical_score += 2
374
+
375
+ # MACD指标评估(10分)- "峰值预警信号"
376
+ if latest['MACD'] > latest['Signal'] and latest['MACD_hist'] > 0:
377
+ # MACD金叉且柱状图为正
378
+ technical_score += 10
379
+ elif latest['MACD'] > latest['Signal']:
380
+ # MACD金叉
381
+ technical_score += 8
382
+ elif latest['MACD'] < latest['Signal'] and latest['MACD_hist'] < 0:
383
+ # MACD死叉且柱状图为负
384
+ technical_score += 0
385
+ elif latest['MACD_hist'] > df.iloc[-2]['MACD_hist']:
386
+ # MACD柱状图增长,可能出现反转信号
387
+ technical_score += 5
388
+
389
+ # 布林带位置评估(5分)
390
+ bb_position = (latest['close'] - latest['BB_lower']) / (latest['BB_upper'] - latest['BB_lower'])
391
+ if 0.3 <= bb_position <= 0.7:
392
+ # 价格在布林带中间区域,趋势稳定
393
+ technical_score += 3
394
+ elif bb_position < 0.2:
395
+ # 价格接近下轨,可能超卖
396
+ technical_score += 5
397
+ elif bb_position > 0.8:
398
+ # 价格接近上轨,可能超买
399
+ technical_score += 1
400
+
401
+ # 确保最大分数限制
402
+ technical_score = min(25, technical_score)
403
+
404
+ # 4. 成交量评分(最高20分)- "能量守恒维度"
405
+ volume_score = 0
406
+
407
+ # 成交量趋势分析
408
+ recent_vol_ratio = [df.iloc[-i]['Volume_Ratio'] for i in range(1, min(6, len(df)))]
409
+ avg_vol_ratio = sum(recent_vol_ratio) / len(recent_vol_ratio)
410
+
411
+ if avg_vol_ratio > 1.5 and latest['close'] > df.iloc[-2]['close']:
412
+ # 成交量放大且价格上涨 - "成交量能量阈值突破"
413
+ volume_score += 20
414
+ elif avg_vol_ratio > 1.2 and latest['close'] > df.iloc[-2]['close']:
415
+ # 成交量和价格同步上涨
416
+ volume_score += 15
417
+ elif avg_vol_ratio < 0.8 and latest['close'] < df.iloc[-2]['close']:
418
+ # 成交量和价格同步下跌,可能是健康回调
419
+ volume_score += 10
420
+ elif avg_vol_ratio > 1.2 and latest['close'] < df.iloc[-2]['close']:
421
+ # 成交量增加但价格下跌,可能存在较大卖压
422
+ volume_score += 0
423
+ else:
424
+ # 其他情况
425
+ volume_score += 8
426
+
427
+ # 5. 动量评分(最高10分)- 维度1:周线级别
428
+ momentum_score = 0
429
+
430
+ # ROC动量指标
431
+ roc = latest['ROC']
432
+ if roc > 5:
433
+ # Strong upward momentum
434
+ momentum_score += 10
435
+ elif 2 <= roc <= 5:
436
+ # Moderate upward momentum
437
+ momentum_score += 8
438
+ elif 0 <= roc < 2:
439
+ # Weak upward momentum
440
+ momentum_score += 5
441
+ elif -2 <= roc < 0:
442
+ # Weak downward momentum
443
+ momentum_score += 3
444
+ else:
445
+ # Strong downward momentum
446
+ momentum_score += 0
447
+
448
+ # 根据加权因子计算总分 - “共振公式”
449
+ final_score = (
450
+ trend_score * weights['trend'] / 0.30 +
451
+ volatility_score * weights['volatility'] / 0.15 +
452
+ technical_score * weights['technical'] / 0.25 +
453
+ volume_score * weights['volume'] / 0.20 +
454
+ momentum_score * weights['momentum'] / 0.10
455
+ )
456
+
457
+ # 特殊市场调整 - “市场适应机制”
458
+ if market_type == 'US':
459
+ # 美国市场额外调整因素
460
+ # 检查是否为财报季
461
+ is_earnings_season = self._is_earnings_season()
462
+ if is_earnings_season:
463
+ # Earnings season has higher volatility, adjust score certainty
464
+ final_score = 0.9 * final_score + 5 # Slight regression to the mean
465
+
466
+ elif market_type == 'HK':
467
+ # 港股特殊调整
468
+ # 检查A股联动效应
469
+ a_share_linkage = self._check_a_share_linkage(df)
470
+ if a_share_linkage > 0.7: # High linkage
471
+ # 根据大陆市场情绪调整
472
+ mainland_sentiment = self._get_mainland_market_sentiment()
473
+ if mainland_sentiment > 0:
474
+ final_score += 5
475
+ else:
476
+ final_score -= 5
477
+
478
+ # Ensure score remains within 0-100 range
479
+ final_score = max(0, min(100, round(final_score)))
480
+
481
+ # Store sub-scores for display
482
+ self.score_details = {
483
+ 'trend': trend_score,
484
+ 'volatility': volatility_score,
485
+ 'technical': technical_score,
486
+ 'volume': volume_score,
487
+ 'momentum': momentum_score,
488
+ 'total': final_score
489
+ }
490
+
491
+ return final_score
492
+
493
+ except Exception as e:
494
+ self.logger.error(f"Error calculating score: {str(e)}")
495
+ # Return neutral score on error
496
+ return 50
497
+
498
+ def calculate_position_size(self, stock_code, risk_percent=2.0, stop_loss_percent=5.0):
499
+ """
500
+ 根据风险管理原则计算最佳仓位大小
501
+ 实施时空共振系统的“仓位大小公式”
502
+
503
+ 参数:
504
+ stock_code: 要分析的股票代码
505
+ risk_percent: 在此交易中承担风险的总资本百分比(默认为2%)
506
+ stop_loss_percent: 从入场点的止损百分比(默认为5��)
507
+
508
+ 返回:
509
+ 仓位大小占总资本的百分比
510
+ """
511
+ try:
512
+ # Get stock data
513
+ df = self.get_stock_data(stock_code)
514
+ df = self.calculate_indicators(df)
515
+
516
+ # 获取波动率因子(来自维度3:能量守恒)
517
+ latest = df.iloc[-1]
518
+ volatility = latest['Volatility']
519
+
520
+ # 计算波动率调整因子(较高波动率=较小仓位)
521
+ volatility_factor = 1.0
522
+ if volatility > 4.0:
523
+ volatility_factor = 0.6 # Reduce position for high volatility stocks
524
+ elif volatility > 2.5:
525
+ volatility_factor = 0.8 # Slightly reduce position
526
+ elif volatility < 1.0:
527
+ volatility_factor = 1.2 # Can increase position for low volatility stocks
528
+
529
+ # Calculate position size using risk formula
530
+ # 公式:position_size = (风险金额) / (止损 * 波动率因子)
531
+ position_size = (risk_percent) / (stop_loss_percent * volatility_factor)
532
+
533
+ # 限制最大仓位为25%以实现多元化
534
+ position_size = min(position_size, 25.0)
535
+
536
+ return position_size
537
+
538
+ except Exception as e:
539
+ self.logger.error(f"Error calculating position size: {str(e)}")
540
+ # 返回保守的默认仓位大小(出错时)
541
+ return 5.0
542
+
543
+ def get_recommendation(self, score, market_type='A', technical_data=None, news_data=None):
544
+ """
545
+ 根据得分和附加信息生成投资建议
546
+ 使用时空共振交易系统策略增强
547
+ """
548
+ try:
549
+ # 1. Base recommendation logic - Dynamic threshold adjustment based on score
550
+ if score >= 85:
551
+ base_recommendation = '强烈建议买入'
552
+ confidence = 'high'
553
+ action = 'strong_buy'
554
+ elif score >= 70:
555
+ base_recommendation = '建议买入'
556
+ confidence = 'medium_high'
557
+ action = 'buy'
558
+ elif score >= 55:
559
+ base_recommendation = '谨慎买入'
560
+ confidence = 'medium'
561
+ action = 'cautious_buy'
562
+ elif score >= 45:
563
+ base_recommendation = '持观望态度'
564
+ confidence = 'medium'
565
+ action = 'hold'
566
+ elif score >= 30:
567
+ base_recommendation = '谨慎持有'
568
+ confidence = 'medium'
569
+ action = 'cautious_hold'
570
+ elif score >= 15:
571
+ base_recommendation = '建议减仓'
572
+ confidence = 'medium_high'
573
+ action = 'reduce'
574
+ else:
575
+ base_recommendation = '建议卖出'
576
+ confidence = 'high'
577
+ action = 'sell'
578
+
579
+ # 2. Consider market characteristics (Dimension 1: Timeframe Nesting)
580
+ market_adjustment = ""
581
+ if market_type == 'US':
582
+ # US market adjustment factors
583
+ if self._is_earnings_season():
584
+ if confidence == 'high' or confidence == 'medium_high':
585
+ confidence = 'medium'
586
+ market_adjustment = "(财报季临近,波动可能加大,建议适当控制仓位)"
587
+
588
+ elif market_type == 'HK':
589
+ # HK market adjustment factors
590
+ mainland_sentiment = self._get_mainland_market_sentiment()
591
+ if mainland_sentiment < -0.3 and (action == 'buy' or action == 'strong_buy'):
592
+ action = 'cautious_buy'
593
+ confidence = 'medium'
594
+ market_adjustment = "(受大陆市场情绪影响,建议控制风险)"
595
+
596
+ elif market_type == 'A':
597
+ # A-share specific adjustment factors
598
+ if technical_data and 'Volatility' in technical_data:
599
+ vol = technical_data.get('Volatility', 0)
600
+ if vol > 4.0 and (action == 'buy' or action == 'strong_buy'):
601
+ action = 'cautious_buy'
602
+ confidence = 'medium'
603
+ market_adjustment = "(市场波动较大,建议分批买入)"
604
+
605
+ # 3. Consider market sentiment (Dimension 2: Filtering)
606
+ sentiment_adjustment = ""
607
+ if news_data and 'market_sentiment' in news_data:
608
+ sentiment = news_data.get('market_sentiment', 'neutral')
609
+
610
+ if sentiment == 'bullish' and action in ['hold', 'cautious_hold']:
611
+ action = 'cautious_buy'
612
+ sentiment_adjustment = "(市场氛围积极,可适当提高仓位)"
613
+
614
+ elif sentiment == 'bearish' and action in ['buy', 'cautious_buy']:
615
+ action = 'hold'
616
+ sentiment_adjustment = "(市场氛围悲观,建议等���更好买点)"
617
+ elif self.json_match_flag==False:
618
+ import re
619
+
620
+ # 如果JSON解析失败,尝试从原始内容中匹配市场情绪
621
+ sentiment_pattern = r'(bullish|neutral|bearish)'
622
+ sentiment_match = re.search(sentiment_pattern, news_data.get('original_content', ''))
623
+
624
+ if sentiment_match:
625
+ sentiment_map = {
626
+ 'bullish': 'bullish',
627
+ 'neutral': 'neutral',
628
+ 'bearish': 'bearish'
629
+ }
630
+ sentiment = sentiment_map.get(sentiment_match.group(1), 'neutral')
631
+
632
+ if sentiment == 'bullish' and action in ['hold', 'cautious_hold']:
633
+ action = 'cautious_buy'
634
+ sentiment_adjustment = "(市场氛围积极,可适当提高仓位)"
635
+ elif sentiment == 'bearish' and action in ['buy', 'cautious_buy']:
636
+ action = 'hold'
637
+ sentiment_adjustment = "(市场氛围悲观,建议等待更好买点)"
638
+
639
+
640
+ # 4. Technical indicators adjustment (Dimension 2: "Peak Detection System")
641
+ technical_adjustment = ""
642
+ if technical_data:
643
+ rsi = technical_data.get('RSI', 50)
644
+ macd_signal = technical_data.get('MACD_signal', 'neutral')
645
+
646
+ # RSI overbought/oversold adjustment
647
+ if rsi > 80 and action in ['buy', 'strong_buy']:
648
+ action = 'hold'
649
+ technical_adjustment = "(RSI指标显示超买,建议等待回调)"
650
+ elif rsi < 20 and action in ['sell', 'reduce']:
651
+ action = 'hold'
652
+ technical_adjustment = "(RSI指标显示超卖,可能存在反弹机会)"
653
+
654
+ # MACD signal adjustment
655
+ if macd_signal == 'bullish' and action in ['hold', 'cautious_hold']:
656
+ action = 'cautious_buy'
657
+ if not technical_adjustment:
658
+ technical_adjustment = "(MACD显示买入信号)"
659
+ elif macd_signal == 'bearish' and action in ['cautious_buy', 'buy']:
660
+ action = 'hold'
661
+ if not technical_adjustment:
662
+ technical_adjustment = "(MACD显示卖出信号)"
663
+
664
+ # 5. Convert adjusted action to final recommendation
665
+ action_to_recommendation = {
666
+ 'strong_buy': '强烈建议买入',
667
+ 'buy': '建议买入',
668
+ 'cautious_buy': '谨慎买入',
669
+ 'hold': '持观望态度',
670
+ 'cautious_hold': '谨慎持有',
671
+ 'reduce': '建议减仓',
672
+ 'sell': '建议卖出'
673
+ }
674
+
675
+ final_recommendation = action_to_recommendation.get(action, base_recommendation)
676
+
677
+ # 6. Combine all adjustment factors
678
+ adjustments = " ".join(filter(None, [market_adjustment, sentiment_adjustment, technical_adjustment]))
679
+
680
+ if adjustments:
681
+ return f"{final_recommendation} {adjustments}"
682
+ else:
683
+ return final_recommendation
684
+
685
+ except Exception as e:
686
+ self.logger.error(f"Error generating investment recommendation: {str(e)}")
687
+ # Return safe default recommendation on error
688
+ return "无法提供明确建议,请结合多种因素谨慎决策"
689
+
690
+ def check_consecutive_losses(self, trade_history, max_consecutive_losses=3):
691
+ """
692
+ 实施“冷静期风险控制” - 连续亏损后停止交易
693
+
694
+ 参数:
695
+ trade_history: 最近交易结果列表 (True 表示盈利, False 表示亏损)
696
+ max_consecutive_losses: 允许的最大连续亏损次数
697
+
698
+ 返回:
699
+ Boolean: True 如果应该暂停交易, False 如果可以继续交易
700
+ """
701
+ consecutive_losses = 0
702
+
703
+ # Count consecutive losses from most recent trades
704
+ for trade in reversed(trade_history):
705
+ if not trade: # If trade is a loss
706
+ consecutive_losses += 1
707
+ else:
708
+ break # Break on first profitable trade
709
+
710
+ # Return True if we've hit max consecutive losses
711
+ return consecutive_losses >= max_consecutive_losses
712
+
713
+ def check_profit_taking(self, current_profit_percent, threshold=20.0):
714
+ """
715
+ 当回报超过阈值时,实施获利了结机制
716
+ 属于“能量守恒维度”的一部分
717
+
718
+ 参数:
719
+ current_profit_percent: 当前利润百分比
720
+ threshold: 用于获利了结的利润百分比阈值
721
+
722
+ 返回:
723
+ Float: 减少仓位的百分比 (0.0-1.0)
724
+ """
725
+ if current_profit_percent >= threshold:
726
+ # If profit exceeds threshold, suggest reducing position by 50%
727
+ return 0.5
728
+
729
+ return 0.0 # No position reduction recommended
730
+
731
+ def _is_earnings_season(self):
732
+ """检查当前是否处于财报季(辅助函数)"""
733
+ from datetime import datetime
734
+ current_month = datetime.now().month
735
+ # 美股财报季大致在1月、4月、7月和10月
736
+ return current_month in [1, 4, 7, 10]
737
+
738
+ def _check_a_share_linkage(self, df, window=20):
739
+ """检查港股与A股的联动性(辅助函数)"""
740
+ # 该函数需要获取对应的A股指数数据
741
+ # 简化版实现:
742
+ try:
743
+ # 获取恒生指数与上证指数的相关系数
744
+ # 实际实现中需要获取真实数据
745
+ correlation = 0.6 # 示例值
746
+ return correlation
747
+ except:
748
+ return 0.5 # 默认中等关联度
749
+
750
+ def _get_mainland_market_sentiment(self):
751
+ """获取中国大陆市场情绪(辅助函数)"""
752
+ # 实际实现中需要分析上证指数、北向资金等因素
753
+ try:
754
+ # 简化版实现,返回-1到1之间的值,1表示积极情绪
755
+ sentiment = 0.2 # 示例值
756
+ return sentiment
757
+ except:
758
+ return 0 # 默认中性情绪
759
+
760
+ def get_stock_news(self, stock_code, market_type='A', limit=5):
761
+ """
762
+ 获取股票相关新闻和实时信息,通过OpenAI API调用function calling方式获取
763
+ 参数:
764
+ stock_code: 股票代码
765
+ market_type: 市场类型 (A/HK/US)
766
+ limit: 返回的新闻条数上限
767
+ 返回:
768
+ 包含新闻和公告的字典
769
+ """
770
+ try:
771
+ self.logger.info(f"获取股票 {stock_code} 的相关新闻和信息")
772
+
773
+ # 缓存键
774
+ cache_key = f"{stock_code}_{market_type}_news"
775
+ if cache_key in self.data_cache and (
776
+ datetime.now() - self.data_cache[cache_key]['timestamp']).seconds < 3600:
777
+ # 缓存1小时内的数据
778
+ return self.data_cache[cache_key]['data']
779
+
780
+ # 获取股票基本信息
781
+ stock_info = self.get_stock_info(stock_code)
782
+ stock_name = stock_info.get('股票名称', '未知')
783
+ industry = stock_info.get('行业', '未知')
784
+
785
+ # 构建新闻查询的prompt
786
+ market_name = "A股" if market_type == 'A' else "港股" if market_type == 'HK' else "美股"
787
+ query = f"""请帮我搜索以下股票的最新相关新闻和信息:
788
+ 股票名称: {stock_name}
789
+ 股票代码: {stock_code}
790
+ 市场: {market_name}
791
+ 行业: {industry}
792
+
793
+ 请使用search_news工具搜索相关新闻,然后只需要返回JSON格式。
794
+ 按照以下格式的JSON数据返回:
795
+ {{
796
+ "news": [
797
+ {{"title": "新闻标题", "date": "YYYY-MM-DD", "source": "新闻来源", "summary": "新闻摘要"}},
798
+ ...
799
+ ],
800
+ "announcements": [
801
+ {{"title": "公告标题", "date": "YYYY-MM-DD", "type": "公告类型"}},
802
+ ...
803
+ ],
804
+ "industry_news": [
805
+ {{"title": "行业新闻标题", "date": "YYYY-MM-DD", "summary": "新闻摘要"}},
806
+ ...
807
+ ],
808
+ "market_sentiment": "市场情绪(bullish/slightly_bullish/neutral/slightly_bearish/bearish)"
809
+ }}
810
+ 注意只返回json数据,不要返回其他内容。
811
+ """
812
+
813
+ # 定义函数调用工具
814
+ tools = [
815
+ {
816
+ "type": "function",
817
+ "function": {
818
+ "name": "search_news",
819
+ "description": "搜索股票相关的新闻和信息",
820
+ "parameters": {
821
+ "type": "object",
822
+ "properties": {
823
+ "query": {
824
+ "type": "string",
825
+ "description": "搜索查询词,用于查找相关新闻"
826
+ }
827
+ },
828
+ "required": ["query"]
829
+ }
830
+ }
831
+ }
832
+ ]
833
+
834
+ # 使用线程和队列添加超时控制
835
+ import queue
836
+ import threading
837
+ import json
838
+ import openai
839
+ import requests
840
+
841
+ result_queue = queue.Queue()
842
+
843
+ def search_news(query):
844
+ """实际执行搜索的函数"""
845
+ try:
846
+ # 获取SERP API密钥
847
+ serp_api_key = os.getenv('SERP_API_KEY')
848
+ if not serp_api_key:
849
+ self.logger.error("未找到SERP_API_KEY环境变量")
850
+ return {"error": "未配置搜索API密钥"}
851
+
852
+ # 构建搜索查询
853
+ search_query = f"{stock_name} {stock_code} {market_name} 最新新闻 公告"
854
+
855
+ # 调用SERP API
856
+ url = "https://serpapi.com/search"
857
+ params = {
858
+ "engine": "google",
859
+ "q": search_query,
860
+ "api_key": serp_api_key,
861
+ "tbm": "nws", # 新闻搜索
862
+ "num": limit * 2 # 获取更多结果以便筛选
863
+ }
864
+
865
+ response = requests.get(url, params=params)
866
+ search_results = response.json()
867
+
868
+ # 提取新闻结果
869
+ news_results = []
870
+ if "news_results" in search_results:
871
+ for item in search_results["news_results"][:limit]:
872
+ news_results.append({
873
+ "title": item.get("title", ""),
874
+ "date": item.get("date", ""),
875
+ "source": item.get("source", ""),
876
+ "link": item.get("link", ""),
877
+ "snippet": item.get("snippet", "")
878
+ })
879
+
880
+ # 构建行业新闻查询
881
+ industry_query = f"{industry} {market_name} 行业动态 最新消息"
882
+ industry_params = {
883
+ "engine": "google",
884
+ "q": industry_query,
885
+ "api_key": serp_api_key,
886
+ "tbm": "nws",
887
+ "num": limit
888
+ }
889
+
890
+ industry_response = requests.get(url, params=industry_params)
891
+ industry_results = industry_response.json()
892
+
893
+ # 提取行业新闻
894
+ industry_news = []
895
+ if "news_results" in industry_results:
896
+ for item in industry_results["news_results"][:limit]:
897
+ industry_news.append({
898
+ "title": item.get("title", ""),
899
+ "date": item.get("date", ""),
900
+ "source": item.get("source", ""),
901
+ "summary": item.get("snippet", "")
902
+ })
903
+
904
+ # 获取公告信息 (可能需要专门的API或网站爬取)
905
+ # 这里简化处理,实际应用中可能需要更复杂的逻辑
906
+ announcements = []
907
+
908
+ # 分析市场情绪
909
+ # 简单实现:基于新闻标题和摘要的关键词分析
910
+ sentiment_keywords = {
911
+ 'bullish': ['上涨', '增长', '利好', '突破', '强势', '看好', '机会', '利润'],
912
+ 'slightly_bullish': ['回升', '改善', '企稳', '向好', '期待'],
913
+ 'neutral': ['稳定', '平稳', '持平', '不变'],
914
+ 'slightly_bearish': ['回调', '承压', '谨慎', '风险', '下滑'],
915
+ 'bearish': ['下跌', '亏损', '跌破', '利空', '警惕', '危机', '崩盘']
916
+ }
917
+
918
+ # 计算情绪得分
919
+ sentiment_scores = {k: 0 for k in sentiment_keywords.keys()}
920
+ all_text = " ".join([n.get("title", "") + " " + n.get("snippet", "") for n in news_results])
921
+
922
+ for sentiment, keywords in sentiment_keywords.items():
923
+ for keyword in keywords:
924
+ if keyword in all_text:
925
+ sentiment_scores[sentiment] += 1
926
+
927
+ # 确定主导情绪
928
+ if not sentiment_scores or all(score == 0 for score in sentiment_scores.values()):
929
+ market_sentiment = "neutral"
930
+ else:
931
+ market_sentiment = max(sentiment_scores.items(), key=lambda x: x[1])[0]
932
+
933
+ return {
934
+ "news": news_results,
935
+ "announcements": announcements,
936
+ "industry_news": industry_news,
937
+ "market_sentiment": market_sentiment
938
+ }
939
+
940
+ except Exception as e:
941
+ self.logger.error(f"搜索新闻时出错: {str(e)}")
942
+ return {"error": str(e)}
943
+
944
+ def call_api():
945
+ try:
946
+ messages = [{"role": "user", "content": query}]
947
+
948
+ # 第一步:调用模型,让它决定使用工具
949
+ response = openai.ChatCompletion.create(
950
+ model=self.news_model,
951
+ messages=messages,
952
+ tools=tools,
953
+ tool_choice="auto",
954
+ temperature=0.7,
955
+ max_tokens=1000,
956
+ stream=False,
957
+ timeout=120
958
+ )
959
+
960
+ # 检查是否有工具调用
961
+ message = response["choices"][0]["message"]
962
+
963
+ if "tool_calls" in message:
964
+ # 处理工具调用
965
+ tool_calls = message["tool_calls"]
966
+
967
+ # 准备新的消息列表,包含工具调用结果
968
+ messages.append(message) # 添加助手的消息
969
+
970
+ for tool_call in tool_calls:
971
+ function_name = tool_call["function"]["name"]
972
+ function_args = json.loads(tool_call["function"]["arguments"])
973
+
974
+ # 执行搜索
975
+ if function_name == "search_news":
976
+ search_query = function_args.get("query", f"{stock_name} {stock_code} 新闻")
977
+ function_response = search_news(search_query)
978
+
979
+ # 添加工具响应到消息
980
+ messages.append({
981
+ "tool_call_id": tool_call["id"],
982
+ "role": "tool",
983
+ "name": function_name,
984
+ "content": json.dumps(function_response, ensure_ascii=False)
985
+ })
986
+
987
+ # 第二步:让模型处理搜索结果并生成最终响应
988
+ second_response = openai.ChatCompletion.create(
989
+ model=self.news_model,
990
+ messages=messages,
991
+ temperature=0.7,
992
+ max_tokens=4000,
993
+ stream=False,
994
+ timeout=120
995
+ )
996
+
997
+ result_queue.put(second_response)
998
+ else:
999
+ # 如果模型没有选择使用工具,直接使用第一次响应
1000
+ result_queue.put(response)
1001
+
1002
+ except Exception as e:
1003
+ result_queue.put(e)
1004
+
1005
+ # 启动API调用线程
1006
+ api_thread = threading.Thread(target=call_api)
1007
+ api_thread.daemon = True
1008
+ api_thread.start()
1009
+
1010
+ # 等待结果,最多等待240秒
1011
+ try:
1012
+ result = result_queue.get(timeout=240)
1013
+
1014
+ # 检查结果是否为异常
1015
+ if isinstance(result, Exception):
1016
+ self.logger.error(f"获取新闻API调用失败: {str(result)}")
1017
+ raise result
1018
+
1019
+ # 提取回复内容
1020
+ content = result["choices"][0]["message"]["content"].strip()
1021
+
1022
+ # 尝试解析JSON,但如果失败则保留原始内容
1023
+ try:
1024
+ # 尝试直接解析JSON
1025
+ news_data = json.loads(content)
1026
+ except json.JSONDecodeError:
1027
+ # 如果直接解析失败,尝试提取JSON部分
1028
+ import re
1029
+ json_match = re.search(r'```json\s*([\s\S]*?)\s*```', content)
1030
+ if json_match:
1031
+ json_str = json_match.group(1)
1032
+ news_data = json.loads(json_str)
1033
+ self.json_match_flag = True
1034
+ else:
1035
+ # 如果仍然无法提取JSON,尝试直接返回响应
1036
+ self.logger.info(f"无法提取JSON,直接返回响应{content}")
1037
+ self.json_match_flag = False
1038
+ news_data = {}
1039
+ news_data['original_content'] = content
1040
+
1041
+ # 确保数据结构完整
1042
+ if not isinstance(news_data, dict):
1043
+ news_data = {}
1044
+
1045
+ for key in ['news', 'announcements', 'industry_news']:
1046
+ if key not in news_data:
1047
+ news_data[key] = []
1048
+
1049
+ if 'market_sentiment' not in news_data:
1050
+ news_data['market_sentiment'] = 'neutral'
1051
+
1052
+ # 添加时间戳
1053
+ news_data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1054
+
1055
+ # 缓存结果
1056
+ self.data_cache[cache_key] = {
1057
+ 'data': news_data,
1058
+ 'timestamp': datetime.now()
1059
+ }
1060
+
1061
+ return news_data
1062
+
1063
+ except queue.Empty:
1064
+ self.logger.warning("获取新闻API调用超时")
1065
+ return {
1066
+ 'news': [],
1067
+ 'announcements': [],
1068
+ 'industry_news': [],
1069
+ 'market_sentiment': 'neutral',
1070
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1071
+ }
1072
+ except Exception as e:
1073
+ self.logger.error(f"处理新闻数据时出错: {str(e)}")
1074
+ return {
1075
+ 'news': [],
1076
+ 'announcements': [],
1077
+ 'industry_news': [],
1078
+ 'market_sentiment': 'neutral',
1079
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1080
+ }
1081
+
1082
+ except Exception as e:
1083
+ self.logger.error(f"获取股票新闻时出错: {str(e)}")
1084
+ # 出错时返回空结果
1085
+ return {
1086
+ 'news': [],
1087
+ 'announcements': [],
1088
+ 'industry_news': [],
1089
+ 'market_sentiment': 'neutral',
1090
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1091
+ }
1092
+
1093
+ # def get_recommendation(self, score, market_type='A', technical_data=None, news_data=None):
1094
+ # """
1095
+ # 根据得分和附加信息给出平滑的投资建议
1096
+ #
1097
+ # 参数:
1098
+ # score: 股票综合评分 (0-100)
1099
+ # market_type: 市场类型 (A/HK/US)
1100
+ # technical_data: 技术指标数据 (可选)
1101
+ # news_data: 新闻和市场情绪数据 (可选)
1102
+ #
1103
+ # 返回:
1104
+ # 投资建议字符串
1105
+ # """
1106
+ # try:
1107
+ # # 1. 基础建议逻辑 - 基于分数的平滑建议
1108
+ # if score >= 85:
1109
+ # base_recommendation = '强烈建议买入'
1110
+ # confidence = 'high'
1111
+ # action = 'strong_buy'
1112
+ # elif score >= 70:
1113
+ # base_recommendation = '建议买入'
1114
+ # confidence = 'medium_high'
1115
+ # action = 'buy'
1116
+ # elif score >= 55:
1117
+ # base_recommendation = '谨慎买入'
1118
+ # confidence = 'medium'
1119
+ # action = 'cautious_buy'
1120
+ # elif score >= 45:
1121
+ # base_recommendation = '持观望态度'
1122
+ # confidence = 'medium'
1123
+ # action = 'hold'
1124
+ # elif score >= 30:
1125
+ # base_recommendation = '谨慎持有'
1126
+ # confidence = 'medium'
1127
+ # action = 'cautious_hold'
1128
+ # elif score >= 15:
1129
+ # base_recommendation = '建议减仓'
1130
+ # confidence = 'medium_high'
1131
+ # action = 'reduce'
1132
+ # else:
1133
+ # base_recommendation = '建议卖出'
1134
+ # confidence = 'high'
1135
+ # action = 'sell'
1136
+ #
1137
+ # # 2. 考虑市场特性
1138
+ # market_adjustment = ""
1139
+ # if market_type == 'US':
1140
+ # # 美股调整因素
1141
+ # if self._is_earnings_season():
1142
+ # if confidence == 'high' or confidence == 'medium_high':
1143
+ # confidence = 'medium'
1144
+ # market_adjustment = "(财报季临近,波动可能加大,建议适当控制仓位)"
1145
+ #
1146
+ # elif market_type == 'HK':
1147
+ # # 港股调整因素
1148
+ # mainland_sentiment = self._get_mainland_market_sentiment()
1149
+ # if mainland_sentiment < -0.3 and (action == 'buy' or action == 'strong_buy'):
1150
+ # action = 'cautious_buy'
1151
+ # confidence = 'medium'
1152
+ # market_adjustment = "(受大陆市场情绪影响,建议控制风险)"
1153
+ #
1154
+ # elif market_type == 'A':
1155
+ # # A股特有调整因素
1156
+ # if technical_data and 'Volatility' in technical_data:
1157
+ # vol = technical_data.get('Volatility', 0)
1158
+ # if vol > 4.0 and (action == 'buy' or action == 'strong_buy'):
1159
+ # action = 'cautious_buy'
1160
+ # confidence = 'medium'
1161
+ # market_adjustment = "(市场波动较大,建议分批买入)"
1162
+ #
1163
+ # # 3. 考虑市场���绪
1164
+ # sentiment_adjustment = ""
1165
+ # if news_data and 'market_sentiment' in news_data:
1166
+ # sentiment = news_data.get('market_sentiment', 'neutral')
1167
+ #
1168
+ # if sentiment == 'bullish' and action in ['hold', 'cautious_hold']:
1169
+ # action = 'cautious_buy'
1170
+ # sentiment_adjustment = "(市场氛围积极,可适当提高仓位)"
1171
+ #
1172
+ # elif sentiment == 'bearish' and action in ['buy', 'cautious_buy']:
1173
+ # action = 'hold'
1174
+ # sentiment_adjustment = "(市场氛围悲观,建议等待更好买点)"
1175
+ #
1176
+ # # 4. 技术指标微调
1177
+ # technical_adjustment = ""
1178
+ # if technical_data:
1179
+ # rsi = technical_data.get('RSI', 50)
1180
+ # macd_signal = technical_data.get('MACD_signal', 'neutral')
1181
+ #
1182
+ # # RSI超买超卖调整
1183
+ # if rsi > 80 and action in ['buy', 'strong_buy']:
1184
+ # action = 'hold'
1185
+ # technical_adjustment = "(RSI指标显示超买,建议等待回调)"
1186
+ # elif rsi < 20 and action in ['sell', 'reduce']:
1187
+ # action = 'hold'
1188
+ # technical_adjustment = "(RSI指标显示超卖,可能存在反弹机会)"
1189
+ #
1190
+ # # MACD信号调整
1191
+ # if macd_signal == 'bullish' and action in ['hold', 'cautious_hold']:
1192
+ # action = 'cautious_buy'
1193
+ # if not technical_adjustment:
1194
+ # technical_adjustment = "(MACD显示买入信号)"
1195
+ # elif macd_signal == 'bearish' and action in ['cautious_buy', 'buy']:
1196
+ # action = 'hold'
1197
+ # if not technical_adjustment:
1198
+ # technical_adjustment = "(MACD显示卖出信号)"
1199
+ #
1200
+ # # 5. 根据调整后的action转换为最终建议
1201
+ # action_to_recommendation = {
1202
+ # 'strong_buy': '强烈建议买入',
1203
+ # 'buy': '建议买入',
1204
+ # 'cautious_buy': '谨慎买入',
1205
+ # 'hold': '持观望态度',
1206
+ # 'cautious_hold': '谨慎持有',
1207
+ # 'reduce': '建议减仓',
1208
+ # 'sell': '建议卖出'
1209
+ # }
1210
+ #
1211
+ # final_recommendation = action_to_recommendation.get(action, base_recommendation)
1212
+ #
1213
+ # # 6. 组合所有调整因素
1214
+ # adjustments = " ".join(filter(None, [market_adjustment, sentiment_adjustment, technical_adjustment]))
1215
+ #
1216
+ # if adjustments:
1217
+ # return f"{final_recommendation} {adjustments}"
1218
+ # else:
1219
+ # return final_recommendation
1220
+ #
1221
+ # except Exception as e:
1222
+ # self.logger.error(f"生成投资建议时出错: {str(e)}")
1223
+ # # 出错时返回安全的默认建议
1224
+ # return "无法提供明确建议,请结合多种因素谨慎决策"
1225
+
1226
+ # 原有API:使用 OpenAI 替代 Gemini
1227
+ # def get_ai_analysis(self, df, stock_code):
1228
+ # """使用AI进行分析"""
1229
+ # try:
1230
+ # import openai
1231
+ # import threading
1232
+ # import queue
1233
+
1234
+ # # 设置API密钥和基础URL
1235
+ # openai.api_key = self.openai_api_key
1236
+ # openai.api_base = self.openai_api_url
1237
+
1238
+ # recent_data = df.tail(14).to_dict('records')
1239
+ # technical_summary = {
1240
+ # 'trend': 'upward' if df.iloc[-1]['MA5'] > df.iloc[-1]['MA20'] else 'downward',
1241
+ # 'volatility': f"{df.iloc[-1]['Volatility']:.2f}%",
1242
+ # 'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing',
1243
+ # 'rsi_level': df.iloc[-1]['RSI']
1244
+ # }
1245
+
1246
+ # prompt = f"""分析股票{stock_code}:
1247
+ # 技术指标概要:{technical_summary}
1248
+ # 近14日交易数据:{recent_data}
1249
+
1250
+ # 请提供:
1251
+ # 1.趋势分析(包含支撑位和压力位)
1252
+ # 2.成交量分析及其含义
1253
+ # 3.风险评估(包含波动率分析)
1254
+ # 4.短期和中期目标价位
1255
+ # 5.关键技术位分析
1256
+ # 6.具体交易建议(包含止损位)
1257
+
1258
+ # 请基于技术指标和市场动态进行分析,给出具体数据支持。"""
1259
+
1260
+ # messages = [{"role": "user", "content": prompt}]
1261
+
1262
+ # # 使用线程和队列添加超时控制
1263
+ # result_queue = queue.Queue()
1264
+
1265
+ # def call_api():
1266
+ # try:
1267
+ # response = openai.ChatCompletion.create(
1268
+ # model=self.openai_model,
1269
+ # messages=messages,
1270
+ # temperature=1,
1271
+ # max_tokens=4000,
1272
+ # stream = False
1273
+ # )
1274
+ # result_queue.put(response)
1275
+ # except Exception as e:
1276
+ # result_queue.put(e)
1277
+
1278
+ # # 启动API调用线程
1279
+ # api_thread = threading.Thread(target=call_api)
1280
+ # api_thread.daemon = True
1281
+ # api_thread.start()
1282
+
1283
+ # # 等待结果,最多等待20秒
1284
+ # try:
1285
+ # result = result_queue.get(timeout=20)
1286
+
1287
+ # # 检查结果是否为异常
1288
+ # if isinstance(result, Exception):
1289
+ # raise result
1290
+
1291
+ # # 提取助理回复
1292
+ # assistant_reply = result["choices"][0]["message"]["content"].strip()
1293
+ # return assistant_reply
1294
+
1295
+ # except queue.Empty:
1296
+ # return "AI分析超时,无法获取分析结果。请稍后再试。"
1297
+ # except Exception as e:
1298
+ # return f"AI分析过程中发生错误: {str(e)}"
1299
+
1300
+ # except Exception as e:
1301
+ # self.logger.error(f"AI分析发生错误: {str(e)}")
1302
+ # return "AI分析过程中发生错误,请稍后再试"
1303
+
1304
+ def get_ai_analysis(self, df, stock_code, market_type='A'):
1305
+ """
1306
+ 使用AI进行增强分析
1307
+ 结合技术指标、实时新闻和行业信息
1308
+
1309
+ 参数:
1310
+ df: 股票历史数据DataFrame
1311
+ stock_code: 股票代码
1312
+ market_type: 市场类型(A/HK/US)
1313
+
1314
+ 返回:
1315
+ AI生成的分析报告文本
1316
+ """
1317
+ try:
1318
+ import openai
1319
+ import threading
1320
+ import queue
1321
+
1322
+ # 设置API密钥和基础URL
1323
+ openai.api_key = self.openai_api_key
1324
+ openai.api_base = self.openai_api_url
1325
+
1326
+ # 1. 获取最近K线数据
1327
+ recent_data = df.tail(20).to_dict('records')
1328
+
1329
+ # 2. 计算技术指标摘要
1330
+ technical_summary = {
1331
+ 'trend': 'upward' if df.iloc[-1]['MA5'] > df.iloc[-1]['MA20'] else 'downward',
1332
+ 'volatility': f"{df.iloc[-1]['Volatility']:.2f}%",
1333
+ 'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing',
1334
+ 'rsi_level': df.iloc[-1]['RSI'],
1335
+ 'macd_signal': 'bullish' if df.iloc[-1]['MACD'] > df.iloc[-1]['Signal'] else 'bearish',
1336
+ 'bb_position': self._calculate_bb_position(df)
1337
+ }
1338
+
1339
+ # 3. 获取支撑压力位
1340
+ sr_levels = self.identify_support_resistance(df)
1341
+
1342
+ # 4. 获取股票基本信息
1343
+ stock_info = self.get_stock_info(stock_code)
1344
+ stock_name = stock_info.get('股票名称', '未知')
1345
+ industry = stock_info.get('行业', '未知')
1346
+
1347
+ # 5. 获取相关新闻和实时信息 - 整合get_stock_news
1348
+ self.logger.info(f"获取 {stock_code} 的相关新闻和市场信息")
1349
+ news_data = self.get_stock_news(stock_code, market_type)
1350
+
1351
+ # 6. 评分分解
1352
+ score = self.calculate_score(df, market_type)
1353
+ score_details = getattr(self, 'score_details', {'total': score})
1354
+
1355
+ # 7. 获取投资建议
1356
+ # 传递技术指标和新闻数据给get_recommendation函数
1357
+ tech_data = {
1358
+ 'RSI': technical_summary['rsi_level'],
1359
+ 'MACD_signal': technical_summary['macd_signal'],
1360
+ 'Volatility': df.iloc[-1]['Volatility']
1361
+ }
1362
+ recommendation = self.get_recommendation(score, market_type, tech_data, news_data)
1363
+
1364
+ # 8. 构建更全面的prompt
1365
+ prompt = f"""作为专业的股票分析师,请对{stock_name}({stock_code})进行全面分析:
1366
+
1367
+ 1. 基本信息:
1368
+ - 股票名称: {stock_name}
1369
+ - 股票代码: {stock_code}
1370
+ - 行业: {industry}
1371
+ - 市场类型: {"A股" if market_type == 'A' else "港股" if market_type == 'HK' else "美股"}
1372
+
1373
+ 2. 技术指标摘要:
1374
+ - 趋势: {technical_summary['trend']}
1375
+ - 波动率: {technical_summary['volatility']}
1376
+ - 成交量趋势: {technical_summary['volume_trend']}
1377
+ - RSI: {technical_summary['rsi_level']:.2f}
1378
+ - MACD信号: {technical_summary['macd_signal']}
1379
+ - 布林带位置: {technical_summary['bb_position']}
1380
+
1381
+ 3. 支撑与压力位:
1382
+ - 短期支撑位: {', '.join([str(level) for level in sr_levels['support_levels']['short_term']])}
1383
+ - 中期支撑位: {', '.join([str(level) for level in sr_levels['support_levels']['medium_term']])}
1384
+ - 短期压力位: {', '.join([str(level) for level in sr_levels['resistance_levels']['short_term']])}
1385
+ - 中期压力位: {', '.join([str(level) for level in sr_levels['resistance_levels']['medium_term']])}
1386
+
1387
+ 4. 综合评分: {score_details['total']}分
1388
+ - 趋势评分: {score_details.get('trend', 0)}
1389
+ - 波动率评分: {score_details.get('volatility', 0)}
1390
+ - 技术指标评分: {score_details.get('technical', 0)}
1391
+ - 成交量评分: {score_details.get('volume', 0)}
1392
+ - 动量评分: {score_details.get('momentum', 0)}
1393
+
1394
+ 5. 投资建议: {recommendation}"""
1395
+
1396
+ # 检查是否有JSON解析失败的情况
1397
+ if hasattr(self, 'json_match_flag') and not self.json_match_flag and 'original_content' in news_data:
1398
+ # 如果JSON解析失败,直接使用原始内容
1399
+ prompt += f"""
1400
+
1401
+ 6. 相关新闻和市场信息:
1402
+ {news_data.get('original_content', '无法获取相关新闻')}
1403
+ """
1404
+ else:
1405
+ # 正常情况下使用格式化的新闻数据
1406
+ prompt += f"""
1407
+
1408
+ 6. 近期相关新闻:
1409
+ {self._format_news_for_prompt(news_data.get('news', []))}
1410
+
1411
+ 7. 公司公告:
1412
+ {self._format_announcements_for_prompt(news_data.get('announcements', []))}
1413
+
1414
+ 8. 行业动态:
1415
+ {self._format_news_for_prompt(news_data.get('industry_news', []))}
1416
+
1417
+ 9. 市场情绪: {news_data.get('market_sentiment', 'neutral')}
1418
+
1419
+ 请提供以下内容:
1420
+ 1. 技术面分析 - 详细分析价格走势、支撑压力位、主要技术指标的信号
1421
+ 2. 行业和市场环境 - 结合新闻和行业动态分析公司所处环境
1422
+ 3. 风险因素 - 识别潜在风险点
1423
+ 4. 具体交易策略 - 给出明确的买入/卖出建议,包括入场点、止损位和目标价位
1424
+ 5. 短期(1周)、中期(1-3个月)和长期(半年)展望
1425
+
1426
+ 请基于数据给出客观分析,不要过度乐观或悲观。分析应该包含具体数据和百分比,避免模糊表述。
1427
+ """
1428
+
1429
+ messages = [{"role": "user", "content": prompt}]
1430
+
1431
+ # 使用线程和队列添加超时控制
1432
+ result_queue = queue.Queue()
1433
+
1434
+ def call_api():
1435
+ try:
1436
+ response = openai.ChatCompletion.create(
1437
+ model=self.openai_model,
1438
+ messages=messages,
1439
+ temperature=0.8,
1440
+ max_tokens=4000,
1441
+ stream=False,
1442
+ timeout=180
1443
+ )
1444
+ result_queue.put(response)
1445
+ except Exception as e:
1446
+ result_queue.put(e)
1447
+
1448
+ # 启动API调用线程
1449
+ api_thread = threading.Thread(target=call_api)
1450
+ api_thread.daemon = True
1451
+ api_thread.start()
1452
+
1453
+ # 等待结果,最多等待240秒
1454
+ try:
1455
+ result = result_queue.get(timeout=240)
1456
+
1457
+ # 检查结果是否为异常
1458
+ if isinstance(result, Exception):
1459
+ raise result
1460
+
1461
+ # 提取助理回复
1462
+ assistant_reply = result["choices"][0]["message"]["content"].strip()
1463
+ return assistant_reply
1464
+
1465
+ except queue.Empty:
1466
+ return "AI分析超时,无法获取分析结果。请稍后再试。"
1467
+ except Exception as e:
1468
+ return f"AI分析过程中发生错误: {str(e)}"
1469
+
1470
+ except Exception as e:
1471
+ self.logger.error(f"AI分析发生错误: {str(e)}")
1472
+ return f"AI分析过程中发生错误,请稍后再试。错误信息: {str(e)}"
1473
+
1474
+ def _calculate_bb_position(self, df):
1475
+ """计算价格在布林带中的位置"""
1476
+ latest = df.iloc[-1]
1477
+ bb_width = latest['BB_upper'] - latest['BB_lower']
1478
+ if bb_width == 0:
1479
+ return "middle"
1480
+
1481
+ position = (latest['close'] - latest['BB_lower']) / bb_width
1482
+
1483
+ if position < 0.2:
1484
+ return "near lower band (potential oversold)"
1485
+ elif position < 0.4:
1486
+ return "below middle band"
1487
+ elif position < 0.6:
1488
+ return "near middle band"
1489
+ elif position < 0.8:
1490
+ return "above middle band"
1491
+ else:
1492
+ return "near upper band (potential overbought)"
1493
+
1494
+ def _format_news_for_prompt(self, news_list):
1495
+ """格式化新闻列表为prompt字符串"""
1496
+ if not news_list:
1497
+ return " 无最新相关新闻"
1498
+
1499
+ formatted = ""
1500
+ for i, news in enumerate(news_list[:3]): # 最多显示3条
1501
+ date = news.get('date', '')
1502
+ title = news.get('title', '')
1503
+ source = news.get('source', '')
1504
+ formatted += f" {i + 1}. [{date}] {title} (来源: {source})\n"
1505
+
1506
+ return formatted
1507
+
1508
+ def _format_announcements_for_prompt(self, announcements):
1509
+ """格式化公告列表为prompt字符串"""
1510
+ if not announcements:
1511
+ return " 无最新公告"
1512
+
1513
+ formatted = ""
1514
+ for i, ann in enumerate(announcements[:3]): # 最多显示3条
1515
+ date = ann.get('date', '')
1516
+ title = ann.get('title', '')
1517
+ type_ = ann.get('type', '')
1518
+ formatted += f" {i + 1}. [{date}] {title} (类型: {type_})\n"
1519
+
1520
+ return formatted
1521
+
1522
+ # 原有API:保持接口不变
1523
+ def analyze_stock(self, stock_code, market_type='A'):
1524
+ """分析单个股票"""
1525
+ try:
1526
+ # self.clear_cache(stock_code, market_type)
1527
+ # 获取股票数据
1528
+ df = self.get_stock_data(stock_code, market_type)
1529
+ self.logger.info(f"获取股票数据完成")
1530
+ # 计算技术指标
1531
+ df = self.calculate_indicators(df)
1532
+ self.logger.info(f"计算技术指标完成")
1533
+ # 评分系统
1534
+ score = self.calculate_score(df)
1535
+ self.logger.info(f"评分系统完成")
1536
+ # 获取最新数据
1537
+ latest = df.iloc[-1]
1538
+ prev = df.iloc[-2]
1539
+
1540
+ # 获取基本信息
1541
+ stock_info = self.get_stock_info(stock_code)
1542
+ stock_name = stock_info.get('股票名称', '未知')
1543
+ industry = stock_info.get('行业', '未知')
1544
+
1545
+ # 生成报告(保持原有格式)
1546
+ report = {
1547
+ 'stock_code': stock_code,
1548
+ 'stock_name': stock_name,
1549
+ 'industry': industry,
1550
+ 'analysis_date': datetime.now().strftime('%Y-%m-%d'),
1551
+ 'score': score,
1552
+ 'price': latest['close'],
1553
+ 'price_change': (latest['close'] - prev['close']) / prev['close'] * 100,
1554
+ 'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
1555
+ 'rsi': latest['RSI'],
1556
+ 'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
1557
+ 'volume_status': '放量' if latest['Volume_Ratio'] > 1.5 else '平量',
1558
+ 'recommendation': self.get_recommendation(score),
1559
+ 'ai_analysis': self.get_ai_analysis(df, stock_code)
1560
+ }
1561
+
1562
+ return report
1563
+
1564
+ except Exception as e:
1565
+ self.logger.error(f"分析股票时出错: {str(e)}")
1566
+ raise
1567
+
1568
+ # 原有API:保持接口不变
1569
+ def scan_market(self, stock_list, min_score=60, market_type='A'):
1570
+ """扫描市场,寻找符合条件的股票"""
1571
+ recommendations = []
1572
+ total_stocks = len(stock_list)
1573
+
1574
+ self.logger.info(f"开始市场扫描,共 {total_stocks} 只股票")
1575
+ start_time = time.time()
1576
+ processed = 0
1577
+
1578
+ # 批量处理,减少日志输出
1579
+ batch_size = 10
1580
+ for i in range(0, total_stocks, batch_size):
1581
+ batch = stock_list[i:i + batch_size]
1582
+ batch_results = []
1583
+
1584
+ for stock_code in batch:
1585
+ try:
1586
+ # 使用简化版分析以加快速度
1587
+ report = self.quick_analyze_stock(stock_code, market_type)
1588
+ if report['score'] >= min_score:
1589
+ batch_results.append(report)
1590
+ except Exception as e:
1591
+ self.logger.error(f"分析股票 {stock_code} 时出错: {str(e)}")
1592
+ continue
1593
+
1594
+ # 添加批处理结果
1595
+ recommendations.extend(batch_results)
1596
+
1597
+ # 更新处理进度
1598
+ processed += len(batch)
1599
+ elapsed = time.time() - start_time
1600
+ remaining = (elapsed / processed) * (total_stocks - processed) if processed > 0 else 0
1601
+
1602
+ self.logger.info(
1603
+ f"已处理 {processed}/{total_stocks} 只股票,耗时 {elapsed:.1f}秒,预计剩余 {remaining:.1f}秒")
1604
+
1605
+ # 按得分排序
1606
+ recommendations.sort(key=lambda x: x['score'], reverse=True)
1607
+
1608
+ total_time = time.time() - start_time
1609
+ self.logger.info(
1610
+ f"市场扫描完成,共分析 {total_stocks} 只股票,找到 {len(recommendations)} 只符合条件的股票,总耗时 {total_time:.1f}秒")
1611
+
1612
+ return recommendations
1613
+
1614
+ # def quick_analyze_stock(self, stock_code, market_type='A'):
1615
+ # """快速分析股票,用于市场扫描"""
1616
+ # try:
1617
+ # # 获取股票数据
1618
+ # df = self.get_stock_data(stock_code, market_type)
1619
+
1620
+ # # 计算技术指标
1621
+ # df = self.calculate_indicators(df)
1622
+
1623
+ # # 简化评分计算
1624
+ # score = self.calculate_score(df)
1625
+
1626
+ # # 获取最新数据
1627
+ # latest = df.iloc[-1]
1628
+ # prev = df.iloc[-2] if len(df) > 1 else latest
1629
+
1630
+ # # 尝试获取股票名称和行业
1631
+ # try:
1632
+ # stock_info = self.get_stock_info(stock_code)
1633
+ # stock_name = stock_info.get('股票名称', '未知')
1634
+ # industry = stock_info.get('行业', '未知')
1635
+ # except:
1636
+ # stock_name = '未知'
1637
+ # industry = '未知'
1638
+
1639
+ # # 生成简化报告
1640
+ # report = {
1641
+ # 'stock_code': stock_code,
1642
+ # 'stock_name': stock_name,
1643
+ # 'industry': industry,
1644
+ # 'analysis_date': datetime.now().strftime('%Y-%m-%d'),
1645
+ # 'score': score,
1646
+ # 'price': float(latest['close']),
1647
+ # 'price_change': float((latest['close'] - prev['close']) / prev['close'] * 100),
1648
+ # 'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
1649
+ # 'rsi': float(latest['RSI']),
1650
+ # 'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
1651
+ # 'volume_status': '放量' if latest['Volume_Ratio'] > 1.5 else '平量',
1652
+ # 'recommendation': self.get_recommendation(score)
1653
+ # }
1654
+
1655
+ # return report
1656
+ # except Exception as e:
1657
+ # self.logger.error(f"快速分析股票 {stock_code} 时出错: {str(e)}")
1658
+ # raise
1659
+
1660
+ def quick_analyze_stock(self, stock_code, market_type='A'):
1661
+ """快速分析股票,用于市场扫描"""
1662
+ try:
1663
+ # 获取股票数据
1664
+ df = self.get_stock_data(stock_code, market_type)
1665
+
1666
+ # 计算技术指标
1667
+ df = self.calculate_indicators(df)
1668
+
1669
+ # 简化评分计算
1670
+ score = self.calculate_score(df)
1671
+
1672
+ # 获取最新数据
1673
+ latest = df.iloc[-1]
1674
+ prev = df.iloc[-2] if len(df) > 1 else latest
1675
+
1676
+ # 先获取股票信息再生成报告
1677
+ try:
1678
+ stock_info = self.get_stock_info(stock_code)
1679
+ stock_name = stock_info.get('股票名称', '未知')
1680
+ industry = stock_info.get('行业', '未知')
1681
+
1682
+ # 添加日志
1683
+ self.logger.info(f"股票 {stock_code} 信息: 名称={stock_name}, 行业={industry}")
1684
+ except Exception as e:
1685
+ self.logger.error(f"获取股票 {stock_code} 信息时出错: {str(e)}")
1686
+ stock_name = '未知'
1687
+ industry = '未知'
1688
+
1689
+ # 生成简化报告
1690
+ report = {
1691
+ 'stock_code': stock_code,
1692
+ 'stock_name': stock_name,
1693
+ 'industry': industry,
1694
+ 'analysis_date': datetime.now().strftime('%Y-%m-%d'),
1695
+ 'score': score,
1696
+ 'price': float(latest['close']),
1697
+ 'price_change': float((latest['close'] - prev['close']) / prev['close'] * 100),
1698
+ 'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
1699
+ 'rsi': float(latest['RSI']),
1700
+ 'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
1701
+ 'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
1702
+ 'recommendation': self.get_recommendation(score)
1703
+ }
1704
+
1705
+ return report
1706
+ except Exception as e:
1707
+ self.logger.error(f"快速分析股票 {stock_code} 时出错: {str(e)}")
1708
+ raise
1709
+
1710
+ # ======================== 新增功能 ========================#
1711
+
1712
+ def get_stock_info(self, stock_code):
1713
+ """获取股票基本信息"""
1714
+ import akshare as ak
1715
+
1716
+ cache_key = f"{stock_code}_info"
1717
+ if cache_key in self.data_cache:
1718
+ return self.data_cache[cache_key]
1719
+
1720
+ try:
1721
+ # 获取A股股票基本信息
1722
+ stock_info = ak.stock_individual_info_em(symbol=stock_code)
1723
+
1724
+ # 修改:使用列名而不是索引访问数据
1725
+ info_dict = {}
1726
+ for _, row in stock_info.iterrows():
1727
+ # 使用iloc安全地获取数据
1728
+ if len(row) >= 2: # 确保有至少两列
1729
+ info_dict[row.iloc[0]] = row.iloc[1]
1730
+
1731
+ # 获取股票名称
1732
+ try:
1733
+ stock_name = ak.stock_info_a_code_name()
1734
+
1735
+ # 检查数据框是否包含预期的列
1736
+ if '代码' in stock_name.columns and '名称' in stock_name.columns:
1737
+ # 尝试找到匹配的股票代码
1738
+ matched_stocks = stock_name[stock_name['代码'] == stock_code]
1739
+ if not matched_stocks.empty:
1740
+ name = matched_stocks['名称'].values[0]
1741
+ else:
1742
+ self.logger.warning(f"未找到股票代码 {stock_code} 的名称信息")
1743
+ name = "未知"
1744
+ else:
1745
+ # 尝试使用不同的列名
1746
+ possible_code_columns = ['代码', 'code', 'symbol', '股票代码', 'stock_code']
1747
+ possible_name_columns = ['名称', 'name', '股票名称', 'stock_name']
1748
+
1749
+ code_col = next((col for col in possible_code_columns if col in stock_name.columns), None)
1750
+ name_col = next((col for col in possible_name_columns if col in stock_name.columns), None)
1751
+
1752
+ if code_col and name_col:
1753
+ matched_stocks = stock_name[stock_name[code_col] == stock_code]
1754
+ if not matched_stocks.empty:
1755
+ name = matched_stocks[name_col].values[0]
1756
+ else:
1757
+ name = "未知"
1758
+ else:
1759
+ self.logger.warning(f"股票信息DataFrame结构不符合预期: {stock_name.columns.tolist()}")
1760
+ name = "未知"
1761
+ except Exception as e:
1762
+ self.logger.error(f"获取股票名称时出错: {str(e)}")
1763
+ name = "未知"
1764
+
1765
+ info_dict['股票名称'] = name
1766
+
1767
+ # 确保基本字段存在
1768
+ if '行业' not in info_dict:
1769
+ info_dict['行业'] = "未知"
1770
+ if '地区' not in info_dict:
1771
+ info_dict['地区'] = "未知"
1772
+
1773
+ # 增加更多日志来调试问题
1774
+ self.logger.info(f"获取到股票信息: 名称={name}, 行业={info_dict.get('行业', '未知')}")
1775
+
1776
+ self.data_cache[cache_key] = info_dict
1777
+ return info_dict
1778
+ except Exception as e:
1779
+ self.logger.error(f"获取股票信息失败: {str(e)}")
1780
+ return {"股票名称": "未知", "行业": "未知", "地区": "未知"}
1781
+
1782
+ def identify_support_resistance(self, df):
1783
+ """识别支撑位和压力位"""
1784
+ latest_price = df['close'].iloc[-1]
1785
+
1786
+ # 使用布林带作为支撑压力参考
1787
+ support_levels = [df['BB_lower'].iloc[-1]]
1788
+ resistance_levels = [df['BB_upper'].iloc[-1]]
1789
+
1790
+ # 添加主要均线作为支撑压力
1791
+ if latest_price < df['MA5'].iloc[-1]:
1792
+ resistance_levels.append(df['MA5'].iloc[-1])
1793
+ else:
1794
+ support_levels.append(df['MA5'].iloc[-1])
1795
+
1796
+ if latest_price < df['MA20'].iloc[-1]:
1797
+ resistance_levels.append(df['MA20'].iloc[-1])
1798
+ else:
1799
+ support_levels.append(df['MA20'].iloc[-1])
1800
+
1801
+ # 添加整数关口
1802
+ price_digits = len(str(int(latest_price)))
1803
+ base = 10 ** (price_digits - 1)
1804
+
1805
+ lower_integer = math.floor(latest_price / base) * base
1806
+ upper_integer = math.ceil(latest_price / base) * base
1807
+
1808
+ if lower_integer < latest_price:
1809
+ support_levels.append(lower_integer)
1810
+ if upper_integer > latest_price:
1811
+ resistance_levels.append(upper_integer)
1812
+
1813
+ # 排序并格式化
1814
+ support_levels = sorted(set([round(x, 2) for x in support_levels if x < latest_price]), reverse=True)
1815
+ resistance_levels = sorted(set([round(x, 2) for x in resistance_levels if x > latest_price]))
1816
+
1817
+ # 分类为短期和中期
1818
+ short_term_support = support_levels[:1] if support_levels else []
1819
+ medium_term_support = support_levels[1:2] if len(support_levels) > 1 else []
1820
+ short_term_resistance = resistance_levels[:1] if resistance_levels else []
1821
+ medium_term_resistance = resistance_levels[1:2] if len(resistance_levels) > 1 else []
1822
+
1823
+ return {
1824
+ 'support_levels': {
1825
+ 'short_term': short_term_support,
1826
+ 'medium_term': medium_term_support
1827
+ },
1828
+ 'resistance_levels': {
1829
+ 'short_term': short_term_resistance,
1830
+ 'medium_term': medium_term_resistance
1831
+ }
1832
+ }
1833
+
1834
+ def calculate_technical_score(self, df):
1835
+ """计算技术面评分 (0-40分)"""
1836
+ try:
1837
+ score = 0
1838
+ # 确保有足够的数据
1839
+ if len(df) < 2:
1840
+ self.logger.warning("数据不足,无法计算技术面评分")
1841
+ return {'total': 0, 'trend': 0, 'indicators': 0, 'support_resistance': 0, 'volatility_volume': 0}
1842
+
1843
+ latest = df.iloc[-1]
1844
+ prev = df.iloc[-2] # 获取前一个时间点的数据
1845
+ prev_close = prev['close']
1846
+
1847
+ # 1. 趋势分析 (0-10分)
1848
+ trend_score = 0
1849
+
1850
+ # 均线排列情况
1851
+ if latest['MA5'] > latest['MA20'] > latest['MA60']: # 多头排列
1852
+ trend_score += 5
1853
+ elif latest['MA5'] < latest['MA20'] < latest['MA60']: # 空头排列
1854
+ trend_score = 0
1855
+ else: # 交叉状态
1856
+ if latest['MA5'] > latest['MA20']:
1857
+ trend_score += 3
1858
+ if latest['MA20'] > latest['MA60']:
1859
+ trend_score += 2
1860
+
1861
+ # 价格与均线关系
1862
+ if latest['close'] > latest['MA5']:
1863
+ trend_score += 3
1864
+ elif latest['close'] > latest['MA20']:
1865
+ trend_score += 2
1866
+
1867
+ # 限制最大值
1868
+ trend_score = min(trend_score, 10)
1869
+ score += trend_score
1870
+
1871
+ # 2. 技术指标分析 (0-10分)
1872
+ indicator_score = 0
1873
+
1874
+ # RSI
1875
+ if 40 <= latest['RSI'] <= 60: # 中性
1876
+ indicator_score += 2
1877
+ elif 30 <= latest['RSI'] < 40 or 60 < latest['RSI'] <= 70: # 边缘区域
1878
+ indicator_score += 4
1879
+ elif latest['RSI'] < 30: # 超卖
1880
+ indicator_score += 5
1881
+ elif latest['RSI'] > 70: # 超买
1882
+ indicator_score += 0
1883
+
1884
+ # MACD
1885
+ if latest['MACD'] > latest['Signal']: # MACD金叉或在零轴上方
1886
+ indicator_score += 3
1887
+ else:
1888
+ # 修复:比较当前和前一个时间点的MACD柱状图值
1889
+ if latest['MACD_hist'] > prev['MACD_hist']: # 柱状图上升
1890
+ indicator_score += 1
1891
+
1892
+ # 限制最大值和最小值
1893
+ indicator_score = max(0, min(indicator_score, 10))
1894
+ score += indicator_score
1895
+
1896
+ # 3. 支撑压力位分析 (0-10分)
1897
+ sr_score = 0
1898
+
1899
+ # 识别支撑位和压力位
1900
+ middle_price = latest['close']
1901
+ upper_band = latest['BB_upper']
1902
+ lower_band = latest['BB_lower']
1903
+
1904
+ # 距离布林带上下轨的距离
1905
+ upper_distance = (upper_band - middle_price) / middle_price * 100
1906
+ lower_distance = (middle_price - lower_band) / middle_price * 100
1907
+
1908
+ if lower_distance < 2: # 接近下轨
1909
+ sr_score += 5
1910
+ elif lower_distance < 5:
1911
+ sr_score += 3
1912
+
1913
+ if upper_distance > 5: # 距上轨较远
1914
+ sr_score += 5
1915
+ elif upper_distance > 2:
1916
+ sr_score += 2
1917
+
1918
+ # 限制最大值
1919
+ sr_score = min(sr_score, 10)
1920
+ score += sr_score
1921
+
1922
+ # 4. 波动性和成交量分析 (0-10分)
1923
+ vol_score = 0
1924
+
1925
+ # 波动率分析
1926
+ if latest['Volatility'] < 2: # 低波动率
1927
+ vol_score += 3
1928
+ elif latest['Volatility'] < 4: # 中等波动率
1929
+ vol_score += 2
1930
+
1931
+ # 成交量分析
1932
+ if 'Volume_Ratio' in df.columns:
1933
+ if latest['Volume_Ratio'] > 1.5 and latest['close'] > prev_close: # 放量上涨
1934
+ vol_score += 4
1935
+ elif latest['Volume_Ratio'] < 0.8 and latest['close'] < prev_close: # 缩量下跌
1936
+ vol_score += 3
1937
+ elif latest['Volume_Ratio'] > 1 and latest['close'] > prev_close: # 普通放量上涨
1938
+ vol_score += 2
1939
+
1940
+ # 限制最大值
1941
+ vol_score = min(vol_score, 10)
1942
+ score += vol_score
1943
+
1944
+ # 保存各个维度的分数
1945
+ technical_scores = {
1946
+ 'total': score,
1947
+ 'trend': trend_score,
1948
+ 'indicators': indicator_score,
1949
+ 'support_resistance': sr_score,
1950
+ 'volatility_volume': vol_score
1951
+ }
1952
+
1953
+ return technical_scores
1954
+
1955
+ except Exception as e:
1956
+ self.logger.error(f"计算技术面评分时出错: {str(e)}")
1957
+ self.logger.error(f"错误详情: {traceback.format_exc()}")
1958
+ return {'total': 0, 'trend': 0, 'indicators': 0, 'support_resistance': 0, 'volatility_volume': 0}
1959
+
1960
+ def perform_enhanced_analysis(self, stock_code, market_type='A'):
1961
+ """执行增强版分析"""
1962
+ try:
1963
+ # 记录开始时间,便于性能分析
1964
+ start_time = time.time()
1965
+ self.logger.info(f"开始执行股票 {stock_code} 的增强分析")
1966
+
1967
+ # 获取股票数据
1968
+ df = self.get_stock_data(stock_code, market_type)
1969
+ data_time = time.time()
1970
+ self.logger.info(f"获取股票数据耗时: {data_time - start_time:.2f}秒")
1971
+
1972
+ # 计算技术指标
1973
+ df = self.calculate_indicators(df)
1974
+ indicator_time = time.time()
1975
+ self.logger.info(f"计算技术指标耗时: {indicator_time - data_time:.2f}秒")
1976
+
1977
+ # 获取最新数据
1978
+ latest = df.iloc[-1]
1979
+ prev = df.iloc[-2] if len(df) > 1 else latest
1980
+
1981
+ # 获取支撑压力位
1982
+ sr_levels = self.identify_support_resistance(df)
1983
+
1984
+ # 计算技术面评分
1985
+ technical_score = self.calculate_technical_score(df)
1986
+
1987
+ # 获取股票信息
1988
+ stock_info = self.get_stock_info(stock_code)
1989
+
1990
+ # 确保technical_score包含必要的字段
1991
+ if 'total' not in technical_score:
1992
+ technical_score['total'] = 0
1993
+
1994
+ # 生成增强版报告
1995
+ enhanced_report = {
1996
+ 'basic_info': {
1997
+ 'stock_code': stock_code,
1998
+ 'stock_name': stock_info.get('股票名称', '未知'),
1999
+ 'industry': stock_info.get('行业', '未知'),
2000
+ 'analysis_date': datetime.now().strftime('%Y-%m-%d')
2001
+ },
2002
+ 'price_data': {
2003
+ 'current_price': float(latest['close']), # 确保是Python原生类型
2004
+ 'price_change': float((latest['close'] - prev['close']) / prev['close'] * 100),
2005
+ 'price_change_value': float(latest['close'] - prev['close'])
2006
+ },
2007
+ 'technical_analysis': {
2008
+ 'trend': {
2009
+ 'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
2010
+ 'ma_status': "多头排列" if latest['MA5'] > latest['MA20'] > latest['MA60'] else
2011
+ "空头排列" if latest['MA5'] < latest['MA20'] < latest['MA60'] else
2012
+ "交叉状态",
2013
+ 'ma_values': {
2014
+ 'ma5': float(latest['MA5']),
2015
+ 'ma20': float(latest['MA20']),
2016
+ 'ma60': float(latest['MA60'])
2017
+ }
2018
+ },
2019
+ 'indicators': {
2020
+ # 确保所有指标都存在并是原生类型
2021
+ 'rsi': float(latest['RSI']) if 'RSI' in latest else 50.0,
2022
+ 'macd': float(latest['MACD']) if 'MACD' in latest else 0.0,
2023
+ 'macd_signal': float(latest['Signal']) if 'Signal' in latest else 0.0,
2024
+ 'macd_histogram': float(latest['MACD_hist']) if 'MACD_hist' in latest else 0.0,
2025
+ 'volatility': float(latest['Volatility']) if 'Volatility' in latest else 0.0
2026
+ },
2027
+ 'volume': {
2028
+ 'current_volume': float(latest['volume']) if 'volume' in latest else 0.0,
2029
+ 'volume_ratio': float(latest['Volume_Ratio']) if 'Volume_Ratio' in latest else 1.0,
2030
+ 'volume_status': '放量' if 'Volume_Ratio' in latest and latest['Volume_Ratio'] > 1.5 else '平量'
2031
+ },
2032
+ 'support_resistance': sr_levels
2033
+ },
2034
+ 'scores': technical_score,
2035
+ 'recommendation': {
2036
+ 'action': self.get_recommendation(technical_score['total']),
2037
+ 'key_points': []
2038
+ },
2039
+ 'ai_analysis': self.get_ai_analysis(df, stock_code)
2040
+ }
2041
+
2042
+ # 最后检查并修复报告结构
2043
+ self._validate_and_fix_report(enhanced_report)
2044
+
2045
+ # 在函数结束时记录总耗时
2046
+ end_time = time.time()
2047
+ self.logger.info(f"执行增强分析总耗时: {end_time - start_time:.2f}秒")
2048
+
2049
+ return enhanced_report
2050
+
2051
+ except Exception as e:
2052
+ self.logger.error(f"执行增强版分析时出错: {str(e)}")
2053
+ self.logger.error(traceback.format_exc())
2054
+
2055
+ # 返回基础错误报告
2056
+ return {
2057
+ 'basic_info': {
2058
+ 'stock_code': stock_code,
2059
+ 'stock_name': '分析失败',
2060
+ 'industry': '未知',
2061
+ 'analysis_date': datetime.now().strftime('%Y-%m-%d')
2062
+ },
2063
+ 'price_data': {
2064
+ 'current_price': 0.0,
2065
+ 'price_change': 0.0,
2066
+ 'price_change_value': 0.0
2067
+ },
2068
+ 'technical_analysis': {
2069
+ 'trend': {
2070
+ 'ma_trend': 'UNKNOWN',
2071
+ 'ma_status': '未知',
2072
+ 'ma_values': {'ma5': 0.0, 'ma20': 0.0, 'ma60': 0.0}
2073
+ },
2074
+ 'indicators': {
2075
+ 'rsi': 50.0,
2076
+ 'macd': 0.0,
2077
+ 'macd_signal': 0.0,
2078
+ 'macd_histogram': 0.0,
2079
+ 'volatility': 0.0
2080
+ },
2081
+ 'volume': {
2082
+ 'current_volume': 0.0,
2083
+ 'volume_ratio': 0.0,
2084
+ 'volume_status': 'NORMAL'
2085
+ },
2086
+ 'support_resistance': {
2087
+ 'support_levels': {'short_term': [], 'medium_term': []},
2088
+ 'resistance_levels': {'short_term': [], 'medium_term': []}
2089
+ }
2090
+ },
2091
+ 'scores': {'total': 0},
2092
+ 'recommendation': {'action': '分析出错,无法提供建议'},
2093
+ 'ai_analysis': f"分析过程中出错: {str(e)}"
2094
+ }
2095
+
2096
+ return error_report
2097
+
2098
+ # 添加一个辅助方法确保报告结构完整
2099
+ def _validate_and_fix_report(self, report):
2100
+ """确保分析报告结构完整"""
2101
+ # 检查必要的顶级字段
2102
+ required_sections = ['basic_info', 'price_data', 'technical_analysis', 'scores', 'recommendation',
2103
+ 'ai_analysis']
2104
+ for section in required_sections:
2105
+ if section not in report:
2106
+ self.logger.warning(f"报告缺少 {section} 部分,添加空对象")
2107
+ report[section] = {}
2108
+
2109
+ # 检查technical_analysis的结构
2110
+ if 'technical_analysis' in report:
2111
+ tech = report['technical_analysis']
2112
+ if not isinstance(tech, dict):
2113
+ report['technical_analysis'] = {}
2114
+ tech = report['technical_analysis']
2115
+
2116
+ # 检查indicators部分
2117
+ if 'indicators' not in tech or not isinstance(tech['indicators'], dict):
2118
+ tech['indicators'] = {
2119
+ 'rsi': 50.0,
2120
+ 'macd': 0.0,
2121
+ 'macd_signal': 0.0,
2122
+ 'macd_histogram': 0.0,
2123
+ 'volatility': 0.0
2124
+ }
2125
+
2126
+ # 转换所有指标为原生Python类型
2127
+ for key, value in tech['indicators'].items():
2128
+ try:
2129
+ tech['indicators'][key] = float(value)
2130
+ except (TypeError, ValueError):
2131
+ tech['indicators'][key] = 0.0
stock_qa.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 开发者:熊猫大侠
5
+ 版本:v2.1.0
6
+ 许可证:MIT License
7
+ """
8
+ # stock_qa.py
9
+ import os
10
+ import openai
11
+
12
+ class StockQA:
13
+ def __init__(self, analyzer, openai_api_key=None, openai_model=None):
14
+ self.analyzer = analyzer
15
+ self.openai_api_key = os.getenv('OPENAI_API_KEY', os.getenv('OPENAI_API_KEY'))
16
+ self.openai_api_url = os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1')
17
+ self.openai_model = os.getenv('OPENAI_API_MODEL', 'gemini-2.0-pro-exp-02-05')
18
+
19
+ def answer_question(self, stock_code, question, market_type='A'):
20
+ """回答关于股票的问题"""
21
+ try:
22
+ if not self.openai_api_key:
23
+ return {"error": "未配置API密钥,无法使用智能问答功能"}
24
+
25
+ # 获取股票信息
26
+ stock_info = self.analyzer.get_stock_info(stock_code)
27
+
28
+ # 获取技术指标数据
29
+ df = self.analyzer.get_stock_data(stock_code, market_type)
30
+ df = self.analyzer.calculate_indicators(df)
31
+
32
+ # 提取最新数据
33
+ latest = df.iloc[-1]
34
+
35
+ # 计算评分
36
+ score = self.analyzer.calculate_score(df)
37
+
38
+ # 获取支撑压力位
39
+ sr_levels = self.analyzer.identify_support_resistance(df)
40
+
41
+ # 构建上下文
42
+ context = f"""股票信息:
43
+ - 代码: {stock_code}
44
+ - 名称: {stock_info.get('股票名称', '未知')}
45
+ - 行业: {stock_info.get('行业', '未知')}
46
+
47
+ 技术指标(最新数据):
48
+ - 价格: {latest['close']}
49
+ - 5日均线: {latest['MA5']}
50
+ - 20日均线: {latest['MA20']}
51
+ - 60日均线: {latest['MA60']}
52
+ - RSI: {latest['RSI']}
53
+ - MACD: {latest['MACD']}
54
+ - MACD信号线: {latest['Signal']}
55
+ - 布林带上轨: {latest['BB_upper']}
56
+ - 布林带中轨: {latest['BB_middle']}
57
+ - 布林带下轨: {latest['BB_lower']}
58
+ - 波动率: {latest['Volatility']}%
59
+
60
+ 技术评分: {score}分
61
+
62
+ 支撑位:
63
+ - 短期: {', '.join([str(level) for level in sr_levels['support_levels']['short_term']])}
64
+ - 中期: {', '.join([str(level) for level in sr_levels['support_levels']['medium_term']])}
65
+
66
+ 压力位:
67
+ - 短期: {', '.join([str(level) for level in sr_levels['resistance_levels']['short_term']])}
68
+ - 中期: {', '.join([str(level) for level in sr_levels['resistance_levels']['medium_term']])}"""
69
+
70
+ # 特定问题类型的补充信息
71
+ if '基本面' in question or '财务' in question or '估值' in question:
72
+ try:
73
+ # 导入基本面分析器
74
+ from fundamental_analyzer import FundamentalAnalyzer
75
+ fundamental = FundamentalAnalyzer()
76
+
77
+ # 获取基本面数据
78
+ indicators = fundamental.get_financial_indicators(stock_code)
79
+
80
+ # 添加到上下文
81
+ context += f"""
82
+
83
+ 基本面指标:
84
+ - PE(TTM): {indicators.get('pe_ttm', '未知')}
85
+ - PB: {indicators.get('pb', '未知')}
86
+ - ROE: {indicators.get('roe', '未知')}%
87
+ - 毛利率: {indicators.get('gross_margin', '未知')}%
88
+ - 净利率: {indicators.get('net_profit_margin', '未知')}%"""
89
+ except:
90
+ context += "\n\n注意:未能获取基本面数据"
91
+
92
+ # 调用AI API回答问题
93
+ openai.api_key = self.openai_api_key
94
+ openai.api_base = self.openai_api_url
95
+
96
+ system_content = """你是专业的股票分析师助手,基于'时空共振交易体系'提供分析。
97
+ 请基于技术指标和市场数据进行客观分析。
98
+ """
99
+
100
+ response = openai.ChatCompletion.create(
101
+ model=self.openai_model,
102
+ messages=[
103
+ {"role": "system", "content": system_content},
104
+ {"role": "user",
105
+ "content": f"请回答关于股票的问题,并参考以下股票数据:\n\n{context}\n\n问题:{question}"}
106
+ ],
107
+ temperature=0.7
108
+ )
109
+
110
+ answer = response.choices[0].message.content
111
+
112
+ return {
113
+ "question": question,
114
+ "answer": answer,
115
+ "stock_code": stock_code,
116
+ "stock_name": stock_info.get('股票名称', '未知')
117
+ }
118
+
119
+ except Exception as e:
120
+ print(f"智能问答出错: {str(e)}")
121
+ return {
122
+ "question": question,
123
+ "answer": f"抱歉,回答问题时出错: {str(e)}",
124
+ "stock_code": stock_code
125
+ }
templates/capital_flow.html ADDED
@@ -0,0 +1,775 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}资金流向 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-3">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header py-2">
13
+ <h5 class="mb-0">资金流向分析</h5>
14
+ </div>
15
+ <div class="card-body py-2">
16
+ <form id="capital-flow-form" class="row g-2">
17
+ <div class="col-md-3">
18
+ <div class="input-group input-group-sm">
19
+ <span class="input-group-text">数据类型</span>
20
+ <select class="form-select" id="data-type">
21
+ <option value="concept" selected>概念资金流</option>
22
+ <option value="individual" >个股资金流</option>
23
+ </select>
24
+ </div>
25
+ </div>
26
+ <div class="col-md-3">
27
+ <div class="input-group input-group-sm">
28
+ <span class="input-group-text">周期</span>
29
+ <select class="form-select" id="period-select">
30
+ <option value="10日排行" selected>10日排行</option>
31
+ <option value="5日排行">5日排行</option>
32
+ <option value="3日排行">3日排行</option>
33
+ </select>
34
+ </div>
35
+ </div>
36
+ <div class="col-md-4 stock-input" style="display: none;">
37
+ <div class="input-group input-group-sm">
38
+ <span class="input-group-text">股票代码</span>
39
+ <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519">
40
+ </div>
41
+ </div>
42
+ <div class="col-md-2">
43
+ <button type="submit" class="btn btn-primary btn-sm w-100">
44
+ <i class="fas fa-search"></i> 查询
45
+ </button>
46
+ </div>
47
+ </form>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Loading Panel -->
54
+ <div id="loading-panel" class="text-center py-5" style="display: none;">
55
+ <div class="spinner-border text-primary" role="status">
56
+ <span class="visually-hidden">Loading...</span>
57
+ </div>
58
+ <p class="mt-3 mb-0">正在获取资金流向数据...</p>
59
+ </div>
60
+
61
+ <!-- Concept Fund Flow Panel -->
62
+ <div id="concept-flow-panel" class="row g-3 mb-3" style="display: none;">
63
+ <div class="col-12">
64
+ <div class="card">
65
+ <div class="card-header py-2 d-flex justify-content-between align-items-center">
66
+ <h5 class="mb-0">概念资金流向</h5>
67
+ <span id="concept-period-badge" class="badge bg-primary">10日排行</span>
68
+ </div>
69
+ <div class="card-body">
70
+ <div class="table-responsive">
71
+ <table class="table table-sm table-striped table-hover">
72
+ <thead>
73
+ <tr>
74
+ <th>序号</th>
75
+ <th>概念/行业</th>
76
+ <th>行业指数</th>
77
+ <th>涨跌幅</th>
78
+ <th>流入资金(亿)</th>
79
+ <th>流出资金(亿)</th>
80
+ <th>净额(亿)</th>
81
+ <th>公司家数</th>
82
+ <th>操作</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody id="concept-flow-table">
86
+ <!-- 资金流向数据将在JS中填充 -->
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Concept Stocks Panel -->
96
+ <div id="concept-stocks-panel" class="row g-3 mb-3" style="display: none;">
97
+ <div class="col-12">
98
+ <div class="card">
99
+ <div class="card-header py-2">
100
+ <h5 id="concept-stocks-title" class="mb-0">概念成分股</h5>
101
+ </div>
102
+ <div class="card-body">
103
+ <div class="table-responsive">
104
+ <table class="table table-sm table-striped table-hover">
105
+ <thead>
106
+ <tr>
107
+ <th>代码</th>
108
+ <th>名称</th>
109
+ <th>最新价</th>
110
+ <th>涨跌幅</th>
111
+ <th>主力净流入</th>
112
+ <th>主力净流入占比</th>
113
+ <th>操作</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody id="concept-stocks-table">
117
+ <!-- 概念成分股数据将在JS中填充 -->
118
+ </tbody>
119
+ </table>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Individual Fund Flow Panel -->
127
+ <div id="individual-flow-panel" class="row g-3 mb-3" style="display: none;">
128
+ <div class="col-12">
129
+ <div class="card">
130
+ <div class="card-header py-2 d-flex justify-content-between align-items-center">
131
+ <h5 id="individual-flow-title" class="mb-0">个股资金流向</h5>
132
+ <span id="individual-period-badge" class="badge bg-primary">10日排行</span>
133
+ </div>
134
+ <div class="card-body">
135
+ <div class="row">
136
+ <div class="col-md-6">
137
+ <h6>资金流向概览</h6>
138
+ <table class="table table-sm">
139
+ <tbody id="individual-flow-summary">
140
+ <!-- 个股资金流向概览将在JS中填充 -->
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+ <div class="col-md-6">
145
+ <h6>资金流入占比</h6>
146
+ <div id="fund-flow-pie-chart" style="height: 200px;"></div>
147
+ </div>
148
+ </div>
149
+ <div class="row mt-3">
150
+ <div class="col-12">
151
+ <h6>资金流向历史</h6>
152
+ <div id="fund-flow-history-chart" style="height: 300px;"></div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Individual Fund Flow Rank Panel -->
161
+ <div id="individual-rank-panel" class="row g-3 mb-3" style="display: none;">
162
+ <div class="col-12">
163
+ <div class="card">
164
+ <div class="card-header py-2 d-flex justify-content-between align-items-center">
165
+ <h5 class="mb-0">个股资金流向排名</h5>
166
+ <span id="individual-rank-period-badge" class="badge bg-primary">10日排行</span>
167
+ </div>
168
+ <div class="card-body">
169
+ <div class="table-responsive">
170
+ <table class="table table-sm table-striped table-hover">
171
+ <thead>
172
+ <tr>
173
+ <th>序号</th>
174
+ <th>代码</th>
175
+ <th>名称</th>
176
+ <th>最新价</th>
177
+ <th>涨跌幅</th>
178
+ <th>主力净流入</th>
179
+ <th>主力净流入占比</th>
180
+ <th>超大单净流入</th>
181
+ <th>大单净流入</th>
182
+ <th>中单净流入</th>
183
+ <th>小单净流入</th>
184
+ <th>操作</th>
185
+ </tr>
186
+ </thead>
187
+ <tbody id="individual-rank-table">
188
+ <!-- 个股资金流向排名数据将在JS中填充 -->
189
+ </tbody>
190
+ </table>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ {% endblock %}
198
+
199
+ {% block scripts %}
200
+ <script>
201
+ $(document).ready(function() {
202
+ // 默认加载概念资金流向
203
+ loadConceptFundFlow('90日排行');
204
+
205
+ // 表单提交事件
206
+ $('#capital-flow-form').submit(function(e) {
207
+ e.preventDefault();
208
+ const dataType = $('#data-type').val();
209
+ const period = $('#period-select').val();
210
+ const stockCode = $('#stock-code').val().trim();
211
+
212
+ if (dataType === 'concept') {
213
+ loadConceptFundFlow(period);
214
+ } else if (dataType === 'individual') {
215
+ if (stockCode) {
216
+ loadIndividualFundFlow(stockCode);
217
+ } else {
218
+ loadIndividualFundFlowRank(period);
219
+ }
220
+ }
221
+ });
222
+
223
+ // 数据类型切换事件
224
+ $('#data-type').change(function() {
225
+ const dataType = $(this).val();
226
+ if (dataType === 'individual') {
227
+ $('.stock-input').show();
228
+ } else {
229
+ $('.stock-input').hide();
230
+ }
231
+ });
232
+ });
233
+
234
+ // 加载概念资金流向
235
+ function loadConceptFundFlow(period) {
236
+ $('#loading-panel').show();
237
+ $('#concept-flow-panel, #concept-stocks-panel, #individual-flow-panel, #individual-rank-panel').hide();
238
+
239
+ $.ajax({
240
+ url: `/api/concept_fund_flow?period=${period}`,
241
+ type: 'GET',
242
+ success: function(response) {
243
+ renderConceptFundFlow(response, period);
244
+ $('#loading-panel').hide();
245
+ $('#concept-flow-panel').show();
246
+ },
247
+ error: function(xhr, status, error) {
248
+ $('#loading-panel').hide();
249
+ showError('获取概念资金流向数据失败: ' + error);
250
+ }
251
+ });
252
+ }
253
+
254
+ // 加载概念成分股
255
+ function loadConceptStocks(sector) {
256
+ $('#loading-panel').show();
257
+ $('#concept-stocks-panel').hide();
258
+
259
+ $.ajax({
260
+ url: `/api/sector_stocks?sector=${encodeURIComponent(sector)}`,
261
+ type: 'GET',
262
+ success: function(response) {
263
+ renderConceptStocks(response, sector);
264
+ $('#loading-panel').hide();
265
+ $('#concept-stocks-panel').show();
266
+ },
267
+ error: function(xhr, status, error) {
268
+ $('#loading-panel').hide();
269
+ showError('获取概念成分股数据失败: ' + error);
270
+ }
271
+ });
272
+ }
273
+
274
+ // 加载个股资金流向
275
+ function loadIndividualFundFlow(stockCode) {
276
+ $('#loading-panel').show();
277
+ $('#concept-flow-panel, #concept-stocks-panel, #individual-flow-panel, #individual-rank-panel').hide();
278
+
279
+ $.ajax({
280
+ url: `/api/individual_fund_flow?stock_code=${stockCode}`,
281
+ type: 'GET',
282
+ success: function(response) {
283
+ renderIndividualFundFlow(response);
284
+ $('#loading-panel').hide();
285
+ $('#individual-flow-panel').show();
286
+ },
287
+ error: function(xhr, status, error) {
288
+ $('#loading-panel').hide();
289
+ showError('获取个股资金流向数据失败: ' + error);
290
+ }
291
+ });
292
+ }
293
+
294
+ // 加载个股资金流向排名
295
+ function loadIndividualFundFlowRank(period) {
296
+ $('#loading-panel').show();
297
+ $('#concept-flow-panel, #concept-stocks-panel, #individual-flow-panel, #individual-rank-panel').hide();
298
+
299
+ $.ajax({
300
+ url: `/api/individual_fund_flow_rank?period=${period}`,
301
+ type: 'GET',
302
+ success: function(response) {
303
+ renderIndividualFundFlowRank(response, period);
304
+ $('#loading-panel').hide();
305
+ $('#individual-rank-panel').show();
306
+ },
307
+ error: function(xhr, status, error) {
308
+ $('#loading-panel').hide();
309
+ showError('获取个股资金流向排名数据失败: ' + error);
310
+ }
311
+ });
312
+ }
313
+
314
+ // 渲染概念资金流向
315
+ function renderConceptFundFlow(data, period) {
316
+ $('#concept-period-badge').text(period);
317
+
318
+ let html = '';
319
+ if (!data || data.length === 0) {
320
+ html = '<tr><td colspan="9" class="text-center">暂无数据</td></tr>';
321
+ } else {
322
+ data.forEach((item, index) => {
323
+ const changeClass = parseFloat(item.change_percent) >= 0 ? 'trend-up' : 'trend-down';
324
+ const changeIcon = parseFloat(item.change_percent) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
325
+
326
+ const netFlowClass = parseFloat(item.net_flow) >= 0 ? 'trend-up' : 'trend-down';
327
+ const netFlowIcon = parseFloat(item.net_flow) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
328
+
329
+ html += `
330
+ <tr>
331
+ <td>${item.rank}</td>
332
+ <td><a href="javascript:void(0)" onclick="loadConceptStocks('${item.sector}')">${item.sector}</a></td>
333
+ <td>${formatNumber(item.sector_index, 2)}</td>
334
+ <td class="${changeClass}">${changeIcon} ${formatPercent(item.change_percent)}</td>
335
+ <td>${formatNumber(item.inflow, 2)}</td>
336
+ <td>${formatNumber(item.outflow, 2)}</td>
337
+ <td class="${netFlowClass}">${netFlowIcon} ${formatNumber(item.net_flow, 2)}</td>
338
+ <td>${item.company_count}</td>
339
+ <td>
340
+ <button class="btn btn-sm btn-outline-primary" onclick="loadConceptStocks('${item.sector}')">
341
+ <i class="fas fa-search"></i>
342
+ </button>
343
+ </td>
344
+ </tr>
345
+ `;
346
+ });
347
+ }
348
+
349
+ $('#concept-flow-table').html(html);
350
+ }
351
+
352
+ // 渲染概念成分股
353
+ function renderConceptStocks(data, sector) {
354
+ $('#concept-stocks-title').text(`${sector} 成分股`);
355
+
356
+ let html = '';
357
+ if (!data || data.length === 0) {
358
+ html = '<tr><td colspan="7" class="text-center">暂无数据</td></tr>';
359
+ } else {
360
+ data.forEach((item) => {
361
+ const changeClass = parseFloat(item.change_percent) >= 0 ? 'trend-up' : 'trend-down';
362
+ const changeIcon = parseFloat(item.change_percent) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
363
+
364
+ const netFlowClass = parseFloat(item.main_net_inflow) >= 0 ? 'trend-up' : 'trend-down';
365
+ const netFlowIcon = parseFloat(item.main_net_inflow) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
366
+
367
+ html += `
368
+ <tr>
369
+ <td>${item.code}</td>
370
+ <td>${item.name}</td>
371
+ <td>${formatNumber(item.price, 2)}</td>
372
+ <td class="${changeClass}">${changeIcon} ${formatPercent(item.change_percent)}</td>
373
+ <td class="${netFlowClass}">${netFlowIcon} ${formatMoney(item.main_net_inflow)}</td>
374
+ <td class="${netFlowClass}">${formatPercent(item.main_net_inflow_percent)}</td>
375
+ <td>
376
+ <a href="/stock_detail/${item.code}" class="btn btn-sm btn-outline-primary">
377
+ <i class="fas fa-chart-line"></i>
378
+ </a>
379
+ <button class="btn btn-sm btn-outline-info" onclick="loadIndividualFundFlow('${item.code}')">
380
+ <i class="fas fa-money-bill-wave"></i>
381
+ </button>
382
+ </td>
383
+ </tr>
384
+ `;
385
+ });
386
+ }
387
+
388
+ $('#concept-stocks-table').html(html);
389
+ }
390
+
391
+ // 渲染个股资金流向
392
+ function renderIndividualFundFlow(data) {
393
+ if (!data || !data.data || data.data.length === 0) {
394
+ showError('未获取到有效的个股资金流向数据');
395
+ return;
396
+ }
397
+
398
+ // Sort data by date (descending - newest first)
399
+ data.data.sort((a, b) => {
400
+ // Parse dates to ensure proper comparison
401
+ let dateA = new Date(a.date);
402
+ let dateB = new Date(b.date);
403
+ return dateB - dateA;
404
+ });
405
+
406
+ // Re-calculate summary for 90 days instead of relying on backend calculation
407
+ recalculateSummary(data, 90);
408
+
409
+ // 设置标题
410
+ $('#individual-flow-title').text(`${data.stock_code} 资金流向`);
411
+
412
+ // 渲染概览
413
+ renderIndividualFlowSummary(data);
414
+
415
+ // 渲染资金流入占比饼图
416
+ renderFundFlowPieChart(data);
417
+
418
+ // 渲染资金流向历史图表
419
+ renderFundFlowHistoryChart(data);
420
+ }
421
+
422
+ function recalculateSummary(data, days) {
423
+ // Get recent data (up to the specified number of days)
424
+ const recent_data = data.data.slice(0, Math.min(days, data.data.length));
425
+
426
+ // Calculate summary statistics
427
+ const total_main_net_inflow = recent_data.reduce((sum, item) => sum + item.main_net_inflow, 0);
428
+ const avg_main_net_inflow_percent = recent_data.reduce((sum, item) => sum + item.main_net_inflow_percent, 0) / recent_data.length;
429
+ const positive_days = recent_data.filter(item => item.main_net_inflow > 0).length;
430
+ const negative_days = recent_data.length - positive_days;
431
+
432
+ // Create or update summary object
433
+ data.summary = {
434
+ recent_days: recent_data.length,
435
+ total_main_net_inflow: total_main_net_inflow,
436
+ avg_main_net_inflow_percent: avg_main_net_inflow_percent,
437
+ positive_days: positive_days,
438
+ negative_days: negative_days
439
+ };
440
+ }
441
+
442
+ // 渲染个股资金流向概览
443
+ function renderIndividualFlowSummary(data) {
444
+ if (!data.summary) return;
445
+
446
+ const summary = data.summary;
447
+ // Now using the first item after sorting
448
+ const recent = data.data[0]; // 最近一天的数据
449
+
450
+ let html = `
451
+ <tr>
452
+ <td>最新日期:</td>
453
+ <td>${recent.date}</td>
454
+ <td>最新价:</td>
455
+ <td>${formatNumber(recent.price, 2)}</td>
456
+ </tr>
457
+ <tr>
458
+ <td>涨跌幅:</td>
459
+ <td class="${recent.change_percent >= 0 ? 'trend-up' : 'trend-down'}">
460
+ ${recent.change_percent >= 0 ? '↑' : '↓'} ${formatPercent(recent.change_percent)}
461
+ </td>
462
+ <td>分析周期:</td>
463
+ <td>${summary.recent_days}天</td>
464
+ </tr>
465
+ <tr>
466
+ <td>主力净流入:</td>
467
+ <td class="${summary.total_main_net_inflow >= 0 ? 'trend-up' : 'trend-down'}">
468
+ ${summary.total_main_net_inflow >= 0 ? '↑' : '↓'} ${formatMoney(summary.total_main_net_inflow)}
469
+ </td>
470
+ <td>净流入占比:</td>
471
+ <td class="${summary.avg_main_net_inflow_percent >= 0 ? 'trend-up' : 'trend-down'}">
472
+ ${summary.avg_main_net_inflow_percent >= 0 ? '↑' : '↓'} ${formatPercent(summary.avg_main_net_inflow_percent)}
473
+ </td>
474
+ </tr>
475
+ <tr>
476
+ <td>资金流入天数:</td>
477
+ <td>${summary.positive_days}天</td>
478
+ <td>资金流出天数:</td>
479
+ <td>${summary.negative_days}天</td>
480
+ </tr>
481
+ `;
482
+
483
+ $('#individual-flow-summary').html(html);
484
+ }
485
+
486
+ // 渲染资金流入占比饼图
487
+ function renderFundFlowPieChart(data) {
488
+ if (!data.data || data.data.length === 0) return;
489
+
490
+ // Using the first item after sorting
491
+ const recent = data.data[0]; // 最近一天的数据
492
+
493
+ // 计算资金流入总额(绝对值)
494
+ const totalInflow = Math.abs(recent.super_large_net_inflow) +
495
+ Math.abs(recent.large_net_inflow) +
496
+ Math.abs(recent.medium_net_inflow) +
497
+ Math.abs(recent.small_net_inflow);
498
+
499
+ // 计算各类型占比
500
+ const superLargePct = Math.abs(recent.super_large_net_inflow) / totalInflow * 100;
501
+ const largePct = Math.abs(recent.large_net_inflow) / totalInflow * 100;
502
+ const mediumPct = Math.abs(recent.medium_net_inflow) / totalInflow * 100;
503
+ const smallPct = Math.abs(recent.small_net_inflow) / totalInflow * 100;
504
+
505
+ const options = {
506
+ series: [superLargePct, largePct, mediumPct, smallPct],
507
+ chart: {
508
+ type: 'pie',
509
+ height: 200
510
+ },
511
+ labels: ['超大单', '大单', '中单', '小单'],
512
+ colors: ['#0d6efd', '#198754', '#ffc107', '#dc3545'],
513
+ legend: {
514
+ position: 'bottom'
515
+ },
516
+ tooltip: {
517
+ y: {
518
+ formatter: function(value) {
519
+ return value.toFixed(2) + '%';
520
+ }
521
+ }
522
+ }
523
+ };
524
+
525
+ // 清除旧图表
526
+ $('#fund-flow-pie-chart').empty();
527
+
528
+ const chart = new ApexCharts(document.querySelector("#fund-flow-pie-chart"), options);
529
+ chart.render();
530
+ }
531
+
532
+ // 渲染资金流向历史图表
533
+ function renderFundFlowHistoryChart(data) {
534
+ if (!data.data || data.data.length === 0) return;
535
+
536
+ // 最近90天的数据
537
+ // Since we've already sorted the data, just get the first 90 and reverse for chronological display
538
+ const historyData = data.data.slice(0, 90).reverse();
539
+
540
+ const dates = historyData.map(item => item.date);
541
+ const mainNetInflow = historyData.map(item => item.main_net_inflow);
542
+ const superLargeInflow = historyData.map(item => item.super_large_net_inflow);
543
+ const largeInflow = historyData.map(item => item.large_net_inflow);
544
+ const mediumInflow = historyData.map(item => item.medium_net_inflow);
545
+ const smallInflow = historyData.map(item => item.small_net_inflow);
546
+ const priceChanges = historyData.map(item => item.change_percent);
547
+
548
+ const options = {
549
+ series: [
550
+ {
551
+ name: '主力净流入',
552
+ type: 'column',
553
+ data: mainNetInflow
554
+ },
555
+ {
556
+ name: '超大单',
557
+ type: 'line',
558
+ data: superLargeInflow
559
+ },
560
+ {
561
+ name: '大单',
562
+ type: 'line',
563
+ data: largeInflow
564
+ },
565
+ {
566
+ name: '价格涨跌幅',
567
+ type: 'line',
568
+ data: priceChanges
569
+ }
570
+ ],
571
+ chart: {
572
+ height: 300,
573
+ type: 'line',
574
+ toolbar: {
575
+ show: false
576
+ }
577
+ },
578
+ stroke: {
579
+ width: [0, 2, 2, 2],
580
+ curve: 'smooth'
581
+ },
582
+ plotOptions: {
583
+ bar: {
584
+ columnWidth: '50%'
585
+ }
586
+ },
587
+ colors: ['#0d6efd', '#198754', '#ffc107', '#dc3545'],
588
+ dataLabels: {
589
+ enabled: false
590
+ },
591
+ labels: dates,
592
+ xaxis: {
593
+ type: 'category'
594
+ },
595
+ yaxis: [
596
+ {
597
+ title: {
598
+ text: '资金流入(亿)',
599
+ style: {
600
+ fontSize: '12px'
601
+ }
602
+ },
603
+ labels: {
604
+ formatter: function(val) {
605
+ // Convert to 亿 for display (divide by 100 million)
606
+ return (val / 100000000).toFixed(2);
607
+ }
608
+ }
609
+ },
610
+ {
611
+ opposite: true,
612
+ title: {
613
+ text: '价格涨跌幅(%)',
614
+ style: {
615
+ fontSize: '12px'
616
+ }
617
+ },
618
+ labels: {
619
+ formatter: function(val) {
620
+ return val.toFixed(2);
621
+ }
622
+ },
623
+ min: -10,
624
+ max: 10,
625
+ tickAmount: 5
626
+ }
627
+ ],
628
+ tooltip: {
629
+ shared: true,
630
+ intersect: false,
631
+ y: {
632
+ formatter: function(y, { seriesIndex }) {
633
+ if (seriesIndex === 3) {
634
+ return y.toFixed(2) + '%';
635
+ }
636
+ // Display money values in 亿 (hundred million) units
637
+ return (y / 100000000).toFixed(2) + ' 亿';
638
+ }
639
+ }
640
+ },
641
+ legend: {
642
+ position: 'top'
643
+ }
644
+ };
645
+
646
+ // 清除旧图表
647
+ $('#fund-flow-history-chart').empty();
648
+
649
+ const chart = new ApexCharts(document.querySelector("#fund-flow-history-chart"), options);
650
+ chart.render();
651
+ }
652
+
653
+ // 渲染个股资金流向排名
654
+ function renderIndividualFundFlowRank(data, period) {
655
+ $('#individual-rank-period-badge').text(period);
656
+
657
+ let html = '';
658
+ if (!data || data.length === 0) {
659
+ html = '<tr><td colspan="12" class="text-center">暂无数据</td></tr>';
660
+ } else {
661
+ data.forEach((item) => {
662
+ const changeClass = parseFloat(item.change_percent) >= 0 ? 'trend-up' : 'trend-down';
663
+ const changeIcon = parseFloat(item.change_percent) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
664
+
665
+ const mainNetClass = parseFloat(item.main_net_inflow) >= 0 ? 'trend-up' : 'trend-down';
666
+ const mainNetIcon = parseFloat(item.main_net_inflow) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
667
+
668
+ html += `
669
+ <tr>
670
+ <td>${item.rank}</td>
671
+ <td>${item.code}</td>
672
+ <td>${item.name}</td>
673
+ <td>${formatNumber(item.price, 2)}</td>
674
+ <td class="${changeClass}">${changeIcon} ${formatPercent(item.change_percent)}</td>
675
+ <td class="${mainNetClass}">${mainNetIcon} ${formatMoney(item.main_net_inflow)}</td>
676
+ <td class="${mainNetClass}">${formatPercent(item.main_net_inflow_percent)}</td>
677
+ <td>${formatMoney(item.super_large_net_inflow)}</td>
678
+ <td>${formatMoney(item.large_net_inflow)}</td>
679
+ <td>${formatMoney(item.medium_net_inflow)}</td>
680
+ <td>${formatMoney(item.small_net_inflow)}</td>
681
+ <td>
682
+ <a href="/stock_detail/${item.code}" class="btn btn-sm btn-outline-primary">
683
+ <i class="fas fa-chart-line"></i>
684
+ </a>
685
+ <button class="btn btn-sm btn-outline-info" onclick="loadIndividualFundFlow('${item.code}')">
686
+ <i class="fas fa-money-bill-wave"></i>
687
+ </button>
688
+ </td>
689
+ </tr>
690
+ `;
691
+ });
692
+ }
693
+
694
+ $('#individual-rank-table').html(html);
695
+ }
696
+
697
+ // 格式化资金数字(支持大数字缩写)
698
+ function formatCompactNumber(num) {
699
+ if (Math.abs(num) >= 1.0e9) {
700
+ return (num / 1.0e9).toFixed(2) + "B";
701
+ } else if (Math.abs(num) >= 1.0e6) {
702
+ return (num / 1.0e6).toFixed(2) + "M";
703
+ } else if (Math.abs(num) >= 1.0e3) {
704
+ return (num / 1.0e3).toFixed(2) + "K";
705
+ } else {
706
+ return num.toFixed(2);
707
+ }
708
+ }
709
+
710
+ // 格式化资金
711
+ function formatMoney(value) {
712
+ if (value === null || value === undefined) {
713
+ return '--';
714
+ }
715
+
716
+ value = parseFloat(value);
717
+ if (isNaN(value)) {
718
+ return '--';
719
+ }
720
+
721
+ if (Math.abs(value) >= 1e8) {
722
+ return (value / 1e8).toFixed(2) + ' 亿';
723
+ } else if (Math.abs(value) >= 1e4) {
724
+ return (value / 1e4).toFixed(2) + ' 万';
725
+ } else {
726
+ return value.toFixed(2) + ' 元';
727
+ }
728
+ }
729
+
730
+ // 格式化百分比
731
+ function formatPercent(value) {
732
+ if (value === null || value === undefined) {
733
+ return '--';
734
+ }
735
+
736
+ value = parseFloat(value);
737
+ if (isNaN(value)) {
738
+ return '--';
739
+ }
740
+
741
+ return value.toFixed(2) + '%';
742
+ }
743
+
744
+ document.addEventListener('DOMContentLoaded', function() {
745
+ const dataType = document.getElementById('data-type');
746
+ const periodSelect = document.getElementById('period-select');
747
+ const stockInput = document.querySelector('.stock-input');
748
+
749
+ // 初始加载时检查默认值
750
+ toggleOptions();
751
+
752
+ dataType.addEventListener('change', toggleOptions);
753
+
754
+ function toggleOptions() {
755
+ if (dataType.value === 'individual') {
756
+ // 个股资金流选项
757
+ periodSelect.innerHTML = `
758
+ <option value="3日">3日</option>
759
+ <option value="5日">5日</option>
760
+ <option value="10日">10日</option>
761
+ `;
762
+ stockInput.style.display = 'block';
763
+ } else {
764
+ // 概念资金流选项
765
+ periodSelect.innerHTML = `
766
+ <option value="10日排行" selected>10日排行</option>
767
+ <option value="5日排行">5日排行</option>
768
+ <option value="3日排行">3日排行</option>
769
+ `;
770
+ stockInput.style.display = 'none';
771
+ }
772
+ }
773
+ });
774
+ </script>
775
+ {% endblock %}
templates/dashboard.html ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}智能仪表盘 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+ <div class="row g-3 mb-3">
9
+ <div class="col-12">
10
+ <div class="card">
11
+ <div class="card-header py-1"> <!-- 减少padding-top和padding-bottom -->
12
+ <h5 class="mb-0">智能股票分析</h5>
13
+ </div>
14
+ <div class="card-body py-2"> <!-- 减少padding-top和padding-bottom -->
15
+ <form id="analysis-form" class="row g-2"> <!-- 减少间距g-3到g-2 -->
16
+ <div class="col-md-4">
17
+ <div class="input-group input-group-sm"> <!-- 添加input-group-sm使输入框更小 -->
18
+ <span class="input-group-text">股票代码</span>
19
+ <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519" required>
20
+ </div>
21
+ </div>
22
+ <div class="col-md-3">
23
+ <div class="input-group input-group-sm"> <!-- 添加input-group-sm使下拉框更小 -->
24
+ <span class="input-group-text">市场</span>
25
+ <select class="form-select" id="market-type">
26
+ <option value="A" selected>A股</option>
27
+ <option value="HK">港股</option>
28
+ <option value="US">美股</option>
29
+ </select>
30
+ </div>
31
+ </div>
32
+ <div class="col-md-3">
33
+ <div class="input-group input-group-sm"> <!-- 添加input-group-sm使下拉框更小 -->
34
+ <span class="input-group-text">周期</span>
35
+ <select class="form-select" id="analysis-period">
36
+ <option value="1m">1个月</option>
37
+ <option value="3m">3个月</option>
38
+ <option value="6m">6个月</option>
39
+ <option value="1y" selected>1年</option>
40
+ </select>
41
+ </div>
42
+ </div>
43
+ <div class="col-md-2">
44
+ <button type="submit" class="btn btn-primary btn-sm w-100"> <!-- 使用btn-sm减小按钮尺寸 -->
45
+ <i class="fas fa-chart-line"></i> 分析
46
+ </button>
47
+ </div>
48
+ </form>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div id="analysis-result" style="display: none;">
55
+ <div class="row g-3 mb-3">
56
+ <div class="col-md-6">
57
+ <div class="card h-100">
58
+ <div class="card-header py-2">
59
+ <h5 class="mb-0">股票概要</h5>
60
+ </div>
61
+ <div class="card-body">
62
+ <div class="row mb-3">
63
+ <div class="col-md-7">
64
+ <h2 id="stock-name" class="mb-0 fs-4"></h2>
65
+ <p id="stock-info" class="text-muted mb-0 small"></p>
66
+ </div>
67
+ <div class="col-md-5 text-end">
68
+ <h3 id="stock-price" class="mb-0 fs-4"></h3>
69
+ <p id="price-change" class="mb-0"></p>
70
+ </div>
71
+ </div>
72
+ <div class="row">
73
+ <div class="col-md-6">
74
+ <div class="mb-2">
75
+ <span class="text-muted small">综合评分:</span>
76
+ <div class="mt-1">
77
+ <span id="total-score" class="badge rounded-pill score-pill"></span>
78
+ </div>
79
+ </div>
80
+ <div class="mb-2">
81
+ <span class="text-muted small">投资建议:</span>
82
+ <p id="recommendation" class="mb-0 text-strong"></p>
83
+ </div>
84
+ </div>
85
+ <div class="col-md-6">
86
+ <div class="mb-2">
87
+ <span class="text-muted small">技术面指标:</span>
88
+ <ul class="list-unstyled mt-1 mb-0 small">
89
+ <li><span class="text-muted">RSI:</span> <span id="rsi-value"></span></li>
90
+ <li><span class="text-muted">MA趋势:</span> <span id="ma-trend"></span></li>
91
+ <li><span class="text-muted">MACD信号:</span> <span id="macd-signal"></span></li>
92
+ <li><span class="text-muted">波动率:</span> <span id="volatility"></span></li>
93
+ </ul>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div class="col-md-6">
101
+ <div class="card h-100">
102
+ <div class="card-header py-2">
103
+ <h5 class="mb-0">多维度评分</h5>
104
+ </div>
105
+ <div class="card-body">
106
+ <div id="radar-chart" style="height: 200px;"></div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="row g-3 mb-3">
113
+ <div class="col-12">
114
+ <div class="card">
115
+ <div class="card-header py-2">
116
+ <h5 class="mb-0">价格与技术指标</h5>
117
+ </div>
118
+ <div class="card-body p-0">
119
+ <div id="price-chart" style="height: 400px;"></div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="row g-3 mb-3">
126
+ <div class="col-md-4">
127
+ <div class="card h-100">
128
+ <div class="card-header py-2">
129
+ <h5 class="mb-0">支撑与压力位</h5>
130
+ </div>
131
+ <div class="card-body">
132
+ <table class="table table-sm">
133
+ <thead>
134
+ <tr>
135
+ <th>类型</th>
136
+ <th>价格</th>
137
+ <th>距离</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody id="support-resistance-table">
141
+ <!-- 支撑压力位数据将在JS中动态填充 -->
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ <div class="col-md-8">
148
+ <div class="card h-100">
149
+ <div class="card-header py-2">
150
+ <h5 class="mb-0">AI分析建议</h5>
151
+ </div>
152
+ <div class="card-body">
153
+ <div id="ai-analysis" class="analysis-section">
154
+ <!-- AI分析结果将在JS中动态填充 -->
155
+ <div class="loading">
156
+ <div class="spinner-border text-primary" role="status">
157
+ <span class="visually-hidden">Loading...</span>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ {% endblock %}
168
+
169
+ {% block scripts %}
170
+ <script>
171
+ let stockData = [];
172
+ let analysisResult = null;
173
+
174
+ // 提交表单进行分析
175
+ $('#analysis-form').submit(function(e) {
176
+ e.preventDefault();
177
+ const stockCode = $('#stock-code').val().trim();
178
+ const marketType = $('#market-type').val();
179
+ const period = $('#analysis-period').val();
180
+
181
+ if (!stockCode) {
182
+ showError('请输入股票代码!');
183
+ return;
184
+ }
185
+
186
+ // 重定向到股票详情页
187
+ window.location.href = `/stock_detail/${stockCode}?market_type=${marketType}&period=${period}`;
188
+ });
189
+
190
+ // Format AI analysis text
191
+ function formatAIAnalysis(text) {
192
+ if (!text) return '';
193
+
194
+ // First, make the text safe for HTML
195
+ const safeText = text
196
+ .replace(/&/g, '&amp;')
197
+ .replace(/</g, '&lt;')
198
+ .replace(/>/g, '&gt;');
199
+
200
+ // Replace basic Markdown elements
201
+ let formatted = safeText
202
+ // Bold text with ** or __
203
+ .replace(/\*\*(.*?)\*\*/g, '<strong class="keyword">$1</strong>')
204
+ .replace(/__(.*?)__/g, '<strong>$1</strong>')
205
+
206
+ // Italic text with * or _
207
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
208
+ .replace(/_(.*?)_/g, '<em>$1</em>')
209
+
210
+ // Headers
211
+ .replace(/^# (.*?)$/gm, '<h4 class="mt-3 mb-2">$1</h4>')
212
+ .replace(/^## (.*?)$/gm, '<h5 class="mt-2 mb-2">$1</h5>')
213
+
214
+ // Apply special styling to financial terms
215
+ .replace(/支撑位/g, '<span class="keyword">支撑位</span>')
216
+ .replace(/压力位/g, '<span class="keyword">压力位</span>')
217
+ .replace(/趋势/g, '<span class="keyword">趋势</span>')
218
+ .replace(/均线/g, '<span class="keyword">均线</span>')
219
+ .replace(/MACD/g, '<span class="term">MACD</span>')
220
+ .replace(/RSI/g, '<span class="term">RSI</span>')
221
+ .replace(/KDJ/g, '<span class="term">KDJ</span>')
222
+
223
+ // Highlight price patterns and movements
224
+ .replace(/([上涨升])/g, '<span class="trend-up">$1</span>')
225
+ .replace(/([下跌降])/g, '<span class="trend-down">$1</span>')
226
+ .replace(/(买入|做多|多头|突破)/g, '<span class="trend-up">$1</span>')
227
+ .replace(/(卖出|做空|空头|跌破)/g, '<span class="trend-down">$1</span>')
228
+
229
+ // Highlight price values (matches patterns like 31.25, 120.50)
230
+ .replace(/(\d+\.\d{2})/g, '<span class="price">$1</span>')
231
+
232
+ // Convert line breaks to paragraph tags
233
+ .replace(/\n\n+/g, '</p><p class="analysis-para">')
234
+ .replace(/\n/g, '<br>');
235
+
236
+ // Wrap in paragraph tags for consistent styling
237
+ return '<p class="analysis-para">' + formatted + '</p>';
238
+ }
239
+
240
+ // 获取股票数据
241
+ function fetchStockData(stockCode, marketType, period) {
242
+ showLoading();
243
+
244
+ $.ajax({
245
+ url: `/api/stock_data?stock_code=${stockCode}&market_type=${marketType}&period=${period}`,
246
+ type: 'GET',
247
+ dataType: 'json',
248
+ success: function(response) {
249
+
250
+ // 检查response是否有data属性
251
+ if (!response.data) {
252
+ hideLoading();
253
+ showError('响应格式不正确: 缺少data字段');
254
+ return;
255
+ }
256
+
257
+ if (response.data.length === 0) {
258
+ hideLoading();
259
+ showError('未找到股票数据');
260
+ return;
261
+ }
262
+
263
+ stockData = response.data;
264
+
265
+ // 获取增强分析数据
266
+ fetchEnhancedAnalysis(stockCode, marketType);
267
+ },
268
+ error: function(xhr, status, error) {
269
+ hideLoading();
270
+
271
+ let errorMsg = '获取股票数据失败';
272
+ if (xhr.responseJSON && xhr.responseJSON.error) {
273
+ errorMsg += ': ' + xhr.responseJSON.error;
274
+ } else if (error) {
275
+ errorMsg += ': ' + error;
276
+ }
277
+ showError(errorMsg);
278
+ }
279
+ });
280
+ }
281
+
282
+ // 获取增强分析数据
283
+ function fetchEnhancedAnalysis(stockCode, marketType) {
284
+
285
+ $.ajax({
286
+ url: '/api/enhanced_analysis?_=' + new Date().getTime(),
287
+ type: 'POST',
288
+ contentType: 'application/json',
289
+ data: JSON.stringify({
290
+ stock_code: stockCode,
291
+ market_type: marketType
292
+ }),
293
+ success: function(response) {
294
+
295
+ if (!response.result) {
296
+ hideLoading();
297
+ showError('增强分析响应格式不正确');
298
+ return;
299
+ }
300
+
301
+ analysisResult = response.result;
302
+ renderAnalysisResult();
303
+ hideLoading();
304
+ $('#analysis-result').show();
305
+ },
306
+ error: function(xhr, status, error) {
307
+ hideLoading();
308
+
309
+ let errorMsg = '获取分析数据失败';
310
+ if (xhr.responseJSON && xhr.responseJSON.error) {
311
+ errorMsg += ': ' + xhr.responseJSON.error;
312
+ } else if (error) {
313
+ errorMsg += ': ' + error;
314
+ }
315
+ showError(errorMsg);
316
+ }
317
+ });
318
+ }
319
+
320
+ // 渲染分析结果
321
+ function renderAnalysisResult() {
322
+ if (!analysisResult) return;
323
+
324
+ // 渲染股票基本信息
325
+ $('#stock-name').text(analysisResult.basic_info.stock_name + ' (' + analysisResult.basic_info.stock_code + ')');
326
+ $('#stock-info').text(analysisResult.basic_info.industry + ' | ' + analysisResult.basic_info.analysis_date);
327
+
328
+ // 渲染价格信息
329
+ $('#stock-price').text('¥' + formatNumber(analysisResult.price_data.current_price, 2));
330
+ const priceChangeClass = analysisResult.price_data.price_change >= 0 ? 'trend-up' : 'trend-down';
331
+ const priceChangeIcon = analysisResult.price_data.price_change >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
332
+ $('#price-change').html(`<span class="${priceChangeClass}">${priceChangeIcon} ${formatNumber(analysisResult.price_data.price_change_value, 2)} (${formatPercent(analysisResult.price_data.price_change, 2)})</span>`);
333
+
334
+ // 渲染评分和建议
335
+ const scoreClass = getScoreColorClass(analysisResult.scores.total_score);
336
+ $('#total-score').text(analysisResult.scores.total_score).addClass(scoreClass);
337
+ $('#recommendation').text(analysisResult.recommendation.action);
338
+
339
+ // 渲染技术指标
340
+ $('#rsi-value').text(formatNumber(analysisResult.technical_analysis.indicators.rsi, 2));
341
+
342
+ const maTrendClass = getTrendColorClass(analysisResult.technical_analysis.trend.ma_trend);
343
+ const maTrendIcon = getTrendIcon(analysisResult.technical_analysis.trend.ma_trend);
344
+ $('#ma-trend').html(`<span class="${maTrendClass}">${maTrendIcon} ${analysisResult.technical_analysis.trend.ma_status}</span>`);
345
+
346
+ const macdSignal = analysisResult.technical_analysis.indicators.macd > analysisResult.technical_analysis.indicators.macd_signal ? 'BUY' : 'SELL';
347
+ const macdClass = macdSignal === 'BUY' ? 'trend-up' : 'trend-down';
348
+ const macdIcon = macdSignal === 'BUY' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
349
+ $('#macd-signal').html(`<span class="${macdClass}">${macdIcon} ${macdSignal}</span>`);
350
+
351
+ $('#volatility').text(formatPercent(analysisResult.technical_analysis.indicators.volatility, 2));
352
+
353
+ // 渲染支撑压力位
354
+ let supportResistanceHtml = '';
355
+
356
+ // 渲染压力位
357
+ if (analysisResult.technical_analysis.support_resistance.resistance &&
358
+ analysisResult.technical_analysis.support_resistance.resistance.short_term &&
359
+ analysisResult.technical_analysis.support_resistance.resistance.short_term.length > 0) {
360
+ const resistance = analysisResult.technical_analysis.support_resistance.resistance.short_term[0];
361
+ const distance = ((resistance - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
362
+ supportResistanceHtml += `
363
+ <tr>
364
+ <td><span class="badge bg-danger">短期压力</span></td>
365
+ <td>${formatNumber(resistance, 2)}</td>
366
+ <td>+${distance}%</td>
367
+ </tr>
368
+ `;
369
+ }
370
+
371
+ if (analysisResult.technical_analysis.support_resistance.resistance &&
372
+ analysisResult.technical_analysis.support_resistance.resistance.medium_term &&
373
+ analysisResult.technical_analysis.support_resistance.resistance.medium_term.length > 0) {
374
+ const resistance = analysisResult.technical_analysis.support_resistance.resistance.medium_term[0];
375
+ const distance = ((resistance - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
376
+ supportResistanceHtml += `
377
+ <tr>
378
+ <td><span class="badge bg-warning text-dark">中期压力</span></td>
379
+ <td>${formatNumber(resistance, 2)}</td>
380
+ <td>+${distance}%</td>
381
+ </tr>
382
+ `;
383
+ }
384
+
385
+ // 渲染支撑位
386
+ if (analysisResult.technical_analysis.support_resistance.support &&
387
+ analysisResult.technical_analysis.support_resistance.support.short_term &&
388
+ analysisResult.technical_analysis.support_resistance.support.short_term.length > 0) {
389
+ const support = analysisResult.technical_analysis.support_resistance.support.short_term[0];
390
+ const distance = ((support - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
391
+ supportResistanceHtml += `
392
+ <tr>
393
+ <td><span class="badge bg-success">短期支撑</span></td>
394
+ <td>${formatNumber(support, 2)}</td>
395
+ <td>${distance}%</td>
396
+ </tr>
397
+ `;
398
+ }
399
+
400
+ if (analysisResult.technical_analysis.support_resistance.support &&
401
+ analysisResult.technical_analysis.support_resistance.support.medium_term &&
402
+ analysisResult.technical_analysis.support_resistance.support.medium_term.length > 0) {
403
+ const support = analysisResult.technical_analysis.support_resistance.support.medium_term[0];
404
+ const distance = ((support - analysisResult.price_data.current_price) / analysisResult.price_data.current_price * 100).toFixed(2);
405
+ supportResistanceHtml += `
406
+ <tr>
407
+ <td><span class="badge bg-info">中期支撑</span></td>
408
+ <td>${formatNumber(support, 2)}</td>
409
+ <td>${distance}%</td>
410
+ </tr>
411
+ `;
412
+ }
413
+
414
+ $('#support-resistance-table').html(supportResistanceHtml);
415
+
416
+ // 渲染AI分析
417
+ $('#ai-analysis').html(formatAIAnalysis(analysisResult.ai_analysis));
418
+
419
+ // 绘制雷达图
420
+ renderRadarChart();
421
+
422
+ // 绘制价格图表
423
+ renderPriceChart();
424
+ }
425
+
426
+ // 绘制雷达图
427
+ function renderRadarChart() {
428
+ if (!analysisResult) return;
429
+
430
+ const options = {
431
+ series: [{
432
+ name: '评分',
433
+ data: [
434
+ analysisResult.scores.trend_score || 0,
435
+ analysisResult.scores.indicators_score || 0,
436
+ analysisResult.scores.support_resistance_score || 0,
437
+ analysisResult.scores.volatility_volume_score || 0
438
+ ]
439
+ }],
440
+ chart: {
441
+ height: 200,
442
+ type: 'radar',
443
+ toolbar: {
444
+ show: false
445
+ }
446
+ },
447
+ title: {
448
+ text: '多维度技术分析评分',
449
+ style: {
450
+ fontSize: '14px'
451
+ }
452
+ },
453
+ xaxis: {
454
+ categories: ['趋势分析', '技术指标', '支撑压力位', '波动与成交量']
455
+ },
456
+ yaxis: {
457
+ max: 10,
458
+ min: 0
459
+ },
460
+ fill: {
461
+ opacity: 0.5,
462
+ colors: ['#4e73df']
463
+ },
464
+ markers: {
465
+ size: 4
466
+ }
467
+ };
468
+
469
+ // 清除旧图表
470
+ $('#radar-chart').empty();
471
+
472
+ const chart = new ApexCharts(document.querySelector("#radar-chart"), options);
473
+ chart.render();
474
+ }
475
+
476
+ // 绘制价格图表
477
+ function renderPriceChart() {
478
+ if (!stockData || stockData.length === 0) return;
479
+
480
+ // 准备价格数据
481
+ const seriesData = [];
482
+
483
+ // 添加蜡烛图数据
484
+ const candleData = stockData.map(item => ({
485
+ x: new Date(item.date),
486
+ y: [item.open, item.high, item.low, item.close]
487
+ }));
488
+ seriesData.push({
489
+ name: '价格',
490
+ type: 'candlestick',
491
+ data: candleData
492
+ });
493
+
494
+ // 添加均线数据
495
+ const ma5Data = stockData.map(item => ({
496
+ x: new Date(item.date),
497
+ y: item.MA5
498
+ }));
499
+ seriesData.push({
500
+ name: 'MA5',
501
+ type: 'line',
502
+ data: ma5Data
503
+ });
504
+
505
+ const ma20Data = stockData.map(item => ({
506
+ x: new Date(item.date),
507
+ y: item.MA20
508
+ }));
509
+ seriesData.push({
510
+ name: 'MA20',
511
+ type: 'line',
512
+ data: ma20Data
513
+ });
514
+
515
+ const ma60Data = stockData.map(item => ({
516
+ x: new Date(item.date),
517
+ y: item.MA60
518
+ }));
519
+ seriesData.push({
520
+ name: 'MA60',
521
+ type: 'line',
522
+ data: ma60Data
523
+ });
524
+
525
+ // 创建图表
526
+ const options = {
527
+ series: seriesData,
528
+ chart: {
529
+ height: 400,
530
+ type: 'candlestick',
531
+ toolbar: {
532
+ show: true,
533
+ tools: {
534
+ download: true,
535
+ selection: true,
536
+ zoom: true,
537
+ zoomin: true,
538
+ zoomout: true,
539
+ pan: true,
540
+ reset: true
541
+ }
542
+ }
543
+ },
544
+ title: {
545
+ text: `${analysisResult.basic_info.stock_name} (${analysisResult.basic_info.stock_code}) 价格走势`,
546
+ align: 'left',
547
+ style: {
548
+ fontSize: '14px'
549
+ }
550
+ },
551
+ xaxis: {
552
+ type: 'datetime'
553
+ },
554
+ yaxis: {
555
+ tooltip: {
556
+ enabled: true
557
+ },
558
+ labels: {
559
+ formatter: function(value) {
560
+ return formatNumber(value, 2); // 统一使用2位小数
561
+ }
562
+ }
563
+ },
564
+ tooltip: {
565
+ shared: true,
566
+ custom: [
567
+ function({ seriesIndex, dataPointIndex, w }) {
568
+ if (seriesIndex === 0) {
569
+ const o = w.globals.seriesCandleO[seriesIndex][dataPointIndex];
570
+ const h = w.globals.seriesCandleH[seriesIndex][dataPointIndex];
571
+ const l = w.globals.seriesCandleL[seriesIndex][dataPointIndex];
572
+ const c = w.globals.seriesCandleC[seriesIndex][dataPointIndex];
573
+
574
+ return `
575
+ <div class="apexcharts-tooltip-candlestick">
576
+ <div>开盘: <span>${formatNumber(o, 2)}</span></div>
577
+ <div>最高: <span>${formatNumber(h, 2)}</span></div>
578
+ <div>最低: <span>${formatNumber(l, 2)}</span></div>
579
+ <div>收盘: <span>${formatNumber(c, 2)}</span></div>
580
+ </div>
581
+ `;
582
+ }
583
+ return '';
584
+ }
585
+ ]
586
+ },
587
+ plotOptions: {
588
+ candlestick: {
589
+ colors: {
590
+ upward: '#3C90EB',
591
+ downward: '#DF7D46'
592
+ }
593
+ }
594
+ }
595
+ };
596
+
597
+ // 清除旧图表
598
+ $('#price-chart').empty();
599
+
600
+ const chart = new ApexCharts(document.querySelector("#price-chart"), options);
601
+ chart.render();
602
+ }
603
+ </script>
604
+ {% endblock %}
templates/error.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}错误 {{ error_code }} - 股票智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container py-5">
7
+ <div class="row justify-content-center">
8
+ <div class="col-md-8">
9
+ <div class="card">
10
+ <div class="card-header bg-danger text-white">
11
+ <h4 class="mb-0">错误 {{ error_code }}</h4>
12
+ </div>
13
+ <div class="card-body text-center py-5">
14
+ <i class="fas fa-exclamation-triangle fa-5x text-danger mb-4"></i>
15
+ <h2>出现错误</h2>
16
+ <p class="lead">{{ message }}</p>
17
+ <div class="mt-4">
18
+ <a href="/" class="btn btn-primary me-2">
19
+ <i class="fas fa-home"></i> 返回首页
20
+ </a>
21
+ <button class="btn btn-outline-secondary" onclick="history.back()">
22
+ <i class="fas fa-arrow-left"></i> 返回上一页
23
+ </button>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ {% endblock %}
templates/fundamental.html ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}基本面分析 - {{ stock_code }} - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-3">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header py-2">
13
+ <h5 class="mb-0">基本面分析</h5>
14
+ </div>
15
+ <div class="card-body py-2">
16
+ <form id="fundamental-form" class="row g-2">
17
+ <div class="col-md-4">
18
+ <div class="input-group input-group-sm">
19
+ <span class="input-group-text">股票代码</span>
20
+ <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519" required>
21
+ </div>
22
+ </div>
23
+ <div class="col-md-3">
24
+ <div class="input-group input-group-sm">
25
+ <span class="input-group-text">市场</span>
26
+ <select class="form-select" id="market-type">
27
+ <option value="A" selected>A股</option>
28
+ <option value="HK">港股</option>
29
+ <option value="US">美股</option>
30
+ </select>
31
+ </div>
32
+ </div>
33
+ <div class="col-md-3">
34
+ <button type="submit" class="btn btn-primary btn-sm w-100">
35
+ <i class="fas fa-search"></i> 分析
36
+ </button>
37
+ </div>
38
+ </form>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div id="fundamental-result" style="display: none;">
45
+ <div class="row g-3 mb-3">
46
+ <div class="col-md-6">
47
+ <div class="card h-100">
48
+ <div class="card-header py-2">
49
+ <h5 class="mb-0">财务概况</h5>
50
+ </div>
51
+ <div class="card-body">
52
+ <div class="row mb-3">
53
+ <div class="col-md-7">
54
+ <h3 id="stock-name" class="mb-0 fs-4"></h3>
55
+ <p id="stock-info" class="text-muted mb-0 small"></p>
56
+ </div>
57
+ <div class="col-md-5 text-end">
58
+ <span id="fundamental-score" class="badge rounded-pill score-pill"></span>
59
+ </div>
60
+ </div>
61
+ <div class="row">
62
+ <div class="col-md-6">
63
+ <h6>估值指标</h6>
64
+ <ul class="list-unstyled mt-1 mb-0 small">
65
+ <li><span class="text-muted">PE(TTM):</span> <span id="pe-ttm"></span></li>
66
+ <li><span class="text-muted">PB:</span> <span id="pb"></span></li>
67
+ <li><span class="text-muted">PS(TTM):</span> <span id="ps-ttm"></span></li>
68
+ </ul>
69
+ </div>
70
+ <div class="col-md-6">
71
+ <h6>盈利能力</h6>
72
+ <ul class="list-unstyled mt-1 mb-0 small">
73
+ <li><span class="text-muted">ROE:</span> <span id="roe"></span></li>
74
+ <li><span class="text-muted">毛利率:</span> <span id="gross-margin"></span></li>
75
+ <li><span class="text-muted">净利润率:</span> <span id="net-profit-margin"></span></li>
76
+ </ul>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ <div class="col-md-6">
83
+ <div class="card h-100">
84
+ <div class="card-header py-2">
85
+ <h5 class="mb-0">成长性分析</h5>
86
+ </div>
87
+ <div class="card-body">
88
+ <div class="row">
89
+ <div class="col-md-6">
90
+ <h6>营收增长</h6>
91
+ <ul class="list-unstyled mt-1 mb-0 small">
92
+ <li><span class="text-muted">3年CAGR:</span> <span id="revenue-growth-3y"></span></li>
93
+ <li><span class="text-muted">5年CAGR:</span> <span id="revenue-growth-5y"></span></li>
94
+ </ul>
95
+ </div>
96
+ <div class="col-md-6">
97
+ <h6>利润增长</h6>
98
+ <ul class="list-unstyled mt-1 mb-0 small">
99
+ <li><span class="text-muted">3年CAGR:</span> <span id="profit-growth-3y"></span></li>
100
+ <li><span class="text-muted">5年CAGR:</span> <span id="profit-growth-5y"></span></li>
101
+ </ul>
102
+ </div>
103
+ </div>
104
+ <div class="mt-3">
105
+ <div id="growth-chart" style="height: 150px;"></div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="row g-3 mb-3">
113
+ <div class="col-md-4">
114
+ <div class="card h-100">
115
+ <div class="card-header py-2">
116
+ <h5 class="mb-0">估值评分</h5>
117
+ </div>
118
+ <div class="card-body">
119
+ <div id="valuation-chart" style="height: 200px;"></div>
120
+ <p id="valuation-comment" class="small text-muted mt-2"></p>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ <div class="col-md-4">
125
+ <div class="card h-100">
126
+ <div class="card-header py-2">
127
+ <h5 class="mb-0">财务健康评分</h5>
128
+ </div>
129
+ <div class="card-body">
130
+ <div id="financial-chart" style="height: 200px;"></div>
131
+ <p id="financial-comment" class="small text-muted mt-2"></p>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <div class="col-md-4">
136
+ <div class="card h-100">
137
+ <div class="card-header py-2">
138
+ <h5 class="mb-0">成长性评分</h5>
139
+ </div>
140
+ <div class="card-body">
141
+ <div id="growth-score-chart" style="height: 200px;"></div>
142
+ <p id="growth-comment" class="small text-muted mt-2"></p>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ {% endblock %}
150
+
151
+ {% block scripts %}
152
+ <script>
153
+ $(document).ready(function() {
154
+ $('#fundamental-form').submit(function(e) {
155
+ e.preventDefault();
156
+ const stockCode = $('#stock-code').val().trim();
157
+ const marketType = $('#market-type').val();
158
+
159
+ if (!stockCode) {
160
+ showError('请输入股票代码!');
161
+ return;
162
+ }
163
+
164
+ fetchFundamentalAnalysis(stockCode);
165
+ });
166
+ });
167
+
168
+ function fetchFundamentalAnalysis(stockCode) {
169
+ showLoading();
170
+
171
+ $.ajax({
172
+ url: '/api/fundamental_analysis',
173
+ type: 'POST',
174
+ contentType: 'application/json',
175
+ data: JSON.stringify({
176
+ stock_code: stockCode
177
+ }),
178
+ success: function(response) {
179
+ hideLoading();
180
+ renderFundamentalAnalysis(response, stockCode);
181
+ $('#fundamental-result').show();
182
+ },
183
+ error: function(xhr, status, error) {
184
+ hideLoading();
185
+ let errorMsg = '获取基本面分析失败';
186
+ if (xhr.responseJSON && xhr.responseJSON.error) {
187
+ errorMsg += ': ' + xhr.responseJSON.error;
188
+ } else if (error) {
189
+ errorMsg += ': ' + error;
190
+ }
191
+ showError(errorMsg);
192
+ }
193
+ });
194
+ }
195
+
196
+ function renderFundamentalAnalysis(data, stockCode) {
197
+ // 设置基本信息
198
+ $('#stock-name').text(data.details.indicators.stock_name || stockCode);
199
+ $('#stock-info').text(data.details.indicators.industry || '未知行业');
200
+
201
+ // 设置评分
202
+ const scoreClass = getScoreColorClass(data.total);
203
+ $('#fundamental-score').text(data.total).addClass(scoreClass);
204
+
205
+ // 设置估值指标
206
+ $('#pe-ttm').text(formatNumber(data.details.indicators.pe_ttm, 2));
207
+ $('#pb').text(formatNumber(data.details.indicators.pb, 2));
208
+ $('#ps-ttm').text(formatNumber(data.details.indicators.ps_ttm, 2));
209
+
210
+ // 设置盈利能力
211
+ $('#roe').text(formatPercent(data.details.indicators.roe, 2));
212
+ $('#gross-margin').text(formatPercent(data.details.indicators.gross_margin, 2));
213
+ $('#net-profit-margin').text(formatPercent(data.details.indicators.net_profit_margin, 2));
214
+
215
+ // 设置成长率
216
+ $('#revenue-growth-3y').text(formatPercent(data.details.growth.revenue_growth_3y, 2));
217
+ $('#revenue-growth-5y').text(formatPercent(data.details.growth.revenue_growth_5y, 2));
218
+ $('#profit-growth-3y').text(formatPercent(data.details.growth.profit_growth_3y, 2));
219
+ $('#profit-growth-5y').text(formatPercent(data.details.growth.profit_growth_5y, 2));
220
+
221
+ // 评论
222
+ $('#valuation-comment').text("估值处于行业" + (data.valuation > 20 ? "合理水平" : "偏高水平"));
223
+ $('#financial-comment').text("财务状况" + (data.financial_health > 30 ? "良好" : "一般"));
224
+ $('#growth-comment').text("成长性" + (data.growth > 20 ? "较好" : "一般"));
225
+
226
+ // 渲染图表
227
+ renderValuationChart(data.valuation);
228
+ renderFinancialChart(data.financial_health);
229
+ renderGrowthScoreChart(data.growth);
230
+ renderGrowthChart(data.details.growth);
231
+ }
232
+
233
+ function renderValuationChart(score) {
234
+ const options = {
235
+ series: [score],
236
+ chart: {
237
+ height: 200,
238
+ type: 'radialBar',
239
+ },
240
+ plotOptions: {
241
+ radialBar: {
242
+ hollow: {
243
+ size: '70%',
244
+ },
245
+ dataLabels: {
246
+ name: {
247
+ fontSize: '22px',
248
+ },
249
+ value: {
250
+ fontSize: '16px',
251
+ },
252
+ total: {
253
+ show: true,
254
+ label: '估值',
255
+ formatter: function() {
256
+ return score;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ },
262
+ colors: ['#1ab7ea'],
263
+ labels: ['估值'],
264
+ };
265
+
266
+ const chart = new ApexCharts(document.querySelector("#valuation-chart"), options);
267
+ chart.render();
268
+ }
269
+
270
+ function renderFinancialChart(score) {
271
+ const options = {
272
+ series: [score],
273
+ chart: {
274
+ height: 200,
275
+ type: 'radialBar',
276
+ },
277
+ plotOptions: {
278
+ radialBar: {
279
+ hollow: {
280
+ size: '70%',
281
+ },
282
+ dataLabels: {
283
+ name: {
284
+ fontSize: '22px',
285
+ },
286
+ value: {
287
+ fontSize: '16px',
288
+ },
289
+ total: {
290
+ show: true,
291
+ label: '财务',
292
+ formatter: function() {
293
+ return score;
294
+ }
295
+ }
296
+ }
297
+ }
298
+ },
299
+ colors: ['#20E647'],
300
+ labels: ['财务'],
301
+ };
302
+
303
+ const chart = new ApexCharts(document.querySelector("#financial-chart"), options);
304
+ chart.render();
305
+ }
306
+
307
+ function renderGrowthScoreChart(score) {
308
+ const options = {
309
+ series: [score],
310
+ chart: {
311
+ height: 200,
312
+ type: 'radialBar',
313
+ },
314
+ plotOptions: {
315
+ radialBar: {
316
+ hollow: {
317
+ size: '70%',
318
+ },
319
+ dataLabels: {
320
+ name: {
321
+ fontSize: '22px',
322
+ },
323
+ value: {
324
+ fontSize: '16px',
325
+ },
326
+ total: {
327
+ show: true,
328
+ label: '成长',
329
+ formatter: function() {
330
+ return score;
331
+ }
332
+ }
333
+ }
334
+ }
335
+ },
336
+ colors: ['#F9CE1D'],
337
+ labels: ['成长'],
338
+ };
339
+
340
+ const chart = new ApexCharts(document.querySelector("#growth-score-chart"), options);
341
+ chart.render();
342
+ }
343
+
344
+ function renderGrowthChart(growthData) {
345
+ const options = {
346
+ series: [{
347
+ name: '营收增长率',
348
+ data: [
349
+ growthData.revenue_growth_3y || 0,
350
+ growthData.revenue_growth_5y || 0
351
+ ]
352
+ }, {
353
+ name: '利润增长率',
354
+ data: [
355
+ growthData.profit_growth_3y || 0,
356
+ growthData.profit_growth_5y || 0
357
+ ]
358
+ }],
359
+ chart: {
360
+ type: 'bar',
361
+ height: 150,
362
+ toolbar: {
363
+ show: false
364
+ }
365
+ },
366
+ plotOptions: {
367
+ bar: {
368
+ horizontal: false,
369
+ columnWidth: '55%',
370
+ endingShape: 'rounded'
371
+ },
372
+ },
373
+ dataLabels: {
374
+ enabled: false
375
+ },
376
+ stroke: {
377
+ show: true,
378
+ width: 2,
379
+ colors: ['transparent']
380
+ },
381
+ xaxis: {
382
+ categories: ['3年CAGR', '5年CAGR'],
383
+ },
384
+ yaxis: {
385
+ title: {
386
+ text: '百分比 (%)'
387
+ }
388
+ },
389
+ fill: {
390
+ opacity: 1
391
+ },
392
+ tooltip: {
393
+ y: {
394
+ formatter: function(val) {
395
+ return val + "%"
396
+ }
397
+ }
398
+ }
399
+ };
400
+
401
+ const chart = new ApexCharts(document.querySelector("#growth-chart"), options);
402
+ chart.render();
403
+ }
404
+ </script>
405
+ {% endblock %}
templates/index.html ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}首页 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container py-5">
7
+ <div class="row mb-5">
8
+ <div class="col-md-8 mx-auto text-center">
9
+ <h1 class="display-4 mb-4">智能分析系统</h1>
10
+ <p class="lead mb-4">基于人工智能的多维度股票分析平台,为您提供专业的投资决策支持</p>
11
+ <div class="d-grid gap-2 d-md-flex justify-content-md-center">
12
+ <a href="/dashboard" class="btn btn-primary btn-lg px-4 me-md-2">
13
+ <i class="fas fa-chart-line"></i> 开始分析
14
+ </a>
15
+ <button type="button" class="btn btn-outline-secondary btn-lg px-4" data-bs-toggle="modal" data-bs-target="#quickAnalysisModal">
16
+ <i class="fas fa-search"></i> 快速分析
17
+ </button>
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="row mb-5">
23
+ <div class="col-md-4 mb-4">
24
+ <div class="card h-100">
25
+ <div class="card-body text-center py-5">
26
+ <i class="fas fa-chart-pie fa-3x text-primary mb-3"></i>
27
+ <h4>多维度分析</h4>
28
+ <p class="text-muted">技术面、基本面、资金面多角度评估,全方位解读股票价值</p>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div class="col-md-4 mb-4">
33
+ <div class="card h-100">
34
+ <div class="card-body text-center py-5">
35
+ <i class="fas fa-robot fa-3x text-primary mb-3"></i>
36
+ <h4>AI智能辅助</h4>
37
+ <p class="text-muted">基于先进算法的智能分析,提供专业级投资建议和趋势预测</p>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <div class="col-md-4 mb-4">
42
+ <div class="card h-100">
43
+ <div class="card-body text-center py-5">
44
+ <i class="fas fa-briefcase fa-3x text-primary mb-3"></i>
45
+ <h4>投资组合管理</h4>
46
+ <p class="text-muted">智能构建和评估投资组合,优化资产配置,控制风险</p>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- 替换index.html中的功能列表部分 -->
53
+ <div class="row mb-5">
54
+ <div class="col-md-10 mx-auto">
55
+ <div class="card">
56
+ <div class="card-header">
57
+ <h5 class="mb-0">主要功能</h5>
58
+ </div>
59
+ <div class="card-body">
60
+ <div class="row">
61
+ <div class="col-md-6">
62
+ <ul class="list-group list-group-flush">
63
+ <li class="list-group-item d-flex align-items-center">
64
+ <i class="fas fa-chart-line text-primary me-3"></i>
65
+ <div>
66
+ <strong>单股智能分析</strong>
67
+ <p class="text-muted mb-0">多维度评估股票表现,提供精准分析</p>
68
+ </div>
69
+ </li>
70
+ <li class="list-group-item d-flex align-items-center">
71
+ <i class="fas fa-search text-primary me-3"></i>
72
+ <div>
73
+ <strong>市场扫描</strong>
74
+ <p class="text-muted mb-0">筛选高评分股票,发现潜在投资机会</p>
75
+ </div>
76
+ </li>
77
+ <li class="list-group-item d-flex align-items-center">
78
+ <i class="fas fa-robot text-primary me-3"></i>
79
+ <div>
80
+ <strong>AI智能问答</strong>
81
+ <p class="text-muted mb-0">提供智能分析解读,解答投资疑问</p>
82
+ </div>
83
+ </li>
84
+ <li class="list-group-item d-flex align-items-center">
85
+ <i class="fas fa-file-invoice-dollar text-success me-3"></i>
86
+ <div>
87
+ <strong>基本面分析</strong>
88
+ <p class="text-muted mb-0">透视公司财务健康和增长潜力</p>
89
+ </div>
90
+ </li>
91
+ <li class="list-group-item d-flex align-items-center">
92
+ <i class="fas fa-money-bill-wave text-info me-3"></i>
93
+ <div>
94
+ <strong>资金流向分析</strong>
95
+ <p class="text-muted mb-0">跟踪主力资金、北向资金和机构持仓</p>
96
+ </div>
97
+ </li>
98
+ </ul>
99
+ </div>
100
+ <div class="col-md-6">
101
+ <ul class="list-group list-group-flush">
102
+ <li class="list-group-item d-flex align-items-center">
103
+ <i class="fas fa-briefcase text-primary me-3"></i>
104
+ <div>
105
+ <strong>投资组合分析</strong>
106
+ <p class="text-muted mb-0">评估组合表现,提供优化建议</p>
107
+ </div>
108
+ </li>
109
+ <li class="list-group-item d-flex align-items-center">
110
+ <i class="fas fa-exclamation-triangle text-danger me-3"></i>
111
+ <div>
112
+ <strong>风险预警</strong>
113
+ <p class="text-muted mb-0">监控技术指标,及时预警潜在风险</p>
114
+ </div>
115
+ </li>
116
+ <li class="list-group-item d-flex align-items-center">
117
+ <i class="fas fa-lightbulb text-warning me-3"></i>
118
+ <div>
119
+ <strong>情景预测</strong>
120
+ <p class="text-muted mb-0">分析多种市场情景,提前布局应对</p>
121
+ </div>
122
+ </li>
123
+ <li class="list-group-item d-flex align-items-center">
124
+ <i class="fas fa-industry text-secondary me-3"></i>
125
+ <div>
126
+ <strong>行业指数分析</strong>
127
+ <p class="text-muted mb-0">洞察行业格局和板块轮动机会</p>
128
+ </div>
129
+ </li>
130
+ <li class="list-group-item d-flex align-items-center">
131
+ <i class="fas fa-chart-area text-primary me-3"></i>
132
+ <div>
133
+ <strong>可视化图表</strong>
134
+ <p class="text-muted mb-0">直观展示分析结果,辅助决策</p>
135
+ </div>
136
+ </li>
137
+ </ul>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- 在index.html的功能卡片部分(现有的卡片之后)添加新卡片 -->
146
+ <div class="row mb-5">
147
+ <!-- 保留原有的三个卡片 -->
148
+ <div class="col-md-4 mb-4">
149
+ <div class="card h-100">
150
+ <div class="card-body text-center py-5">
151
+ <i class="fas fa-chart-pie fa-3x text-primary mb-3"></i>
152
+ <h4>多维度分析</h4>
153
+ <p class="text-muted">技术面、基本面、资金面多角度评估,全方位解读股票价值</p>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <div class="col-md-4 mb-4">
158
+ <div class="card h-100">
159
+ <div class="card-body text-center py-5">
160
+ <i class="fas fa-robot fa-3x text-primary mb-3"></i>
161
+ <h4>AI智能辅助</h4>
162
+ <p class="text-muted">基于先进算法的智能分析,提供专业级投资建议和趋势预测</p>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ <div class="col-md-4 mb-4">
167
+ <div class="card h-100">
168
+ <div class="card-body text-center py-5">
169
+ <i class="fas fa-briefcase fa-3x text-primary mb-3"></i>
170
+ <h4>投资组合管理</h4>
171
+ <p class="text-muted">智能构建和评估投资组合,优化资产配置,控制风险</p>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- 添加新的功能卡片行 -->
178
+ <div class="row mb-5">
179
+ <div class="col-md-4 mb-4">
180
+ <div class="card h-100">
181
+ <div class="card-body text-center py-5">
182
+ <i class="fas fa-file-invoice-dollar fa-3x text-success mb-3"></i>
183
+ <h4>基本面透视</h4>
184
+ <p class="text-muted">财务指标分析、估值评估、成长性预测,揭示公司内在价值</p>
185
+ <a href="/fundamental" class="btn btn-outline-success mt-2">开始分析</a>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ <div class="col-md-4 mb-4">
190
+ <div class="card h-100">
191
+ <div class="card-body text-center py-5">
192
+ <i class="fas fa-money-bill-wave fa-3x text-info mb-3"></i>
193
+ <h4>资金流向分析</h4>
194
+ <p class="text-muted">主力资金、北向资金、机构持仓变化,把握资金动向</p>
195
+ <a href="/capital_flow" class="btn btn-outline-info mt-2">查看资金</a>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ <div class="col-md-4 mb-4">
200
+ <div class="card h-100">
201
+ <div class="card-body text-center py-5">
202
+ <i class="fas fa-crystal-ball fa-3x text-warning mb-3"></i>
203
+ <h4>多情景预测</h4>
204
+ <p class="text-muted">乐观、中性、悲观三种市场情景,助您提前布局应对不同市场环境</p>
205
+ <a href="/scenario_predict" class="btn btn-outline-warning mt-2">查看预测</a>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <div class="row mb-5">
212
+ <div class="col-md-4 mb-4">
213
+ <div class="card h-100">
214
+ <div class="card-body text-center py-5">
215
+ <i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
216
+ <h4>风险监控</h4>
217
+ <p class="text-muted">多维度风险预警,帮助您及时发现潜在风险,保护投资安全</p>
218
+ <a href="/risk_monitor" class="btn btn-outline-danger mt-2">查看风险</a>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ <div class="col-md-4 mb-4">
223
+ <div class="card h-100">
224
+ <div class="card-body text-center py-5">
225
+ <i class="fas fa-question-circle fa-3x text-purple mb-3"></i>
226
+ <h4>智能问答</h4>
227
+ <p class="text-muted">AI助手随时回答您关于股票的各种问题,专业知识触手可及</p>
228
+ <a href="/qa" class="btn btn-outline-primary mt-2">开始提问</a>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ <div class="col-md-4 mb-4">
233
+ <div class="card h-100">
234
+ <div class="card-body text-center py-5">
235
+ <i class="fas fa-industry fa-3x text-secondary mb-3"></i>
236
+ <h4>行业指数分析</h4>
237
+ <p class="text-muted">行业整体分析与板块轮动把握,发现行业机会,洞察市场格局</p>
238
+ <a href="/industry_analysis" class="btn btn-outline-secondary mt-2">查看行业</a>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <!-- 替换index.html底部的导航按钮区域 -->
245
+ <div class="row">
246
+ <div class="col-md-10 mx-auto text-center">
247
+ <h4 class="mb-4">开始使用智能分析系统</h4>
248
+ <div class="d-grid gap-2 d-md-flex justify-content-md-center flex-wrap">
249
+ <a href="/dashboard" class="btn btn-primary btn-lg px-4 me-md-2 mb-2">
250
+ <i class="fas fa-chart-line"></i> 智能仪表盘
251
+ </a>
252
+ <a href="/fundamental" class="btn btn-success btn-lg px-4 me-md-2 mb-2">
253
+ <i class="fas fa-file-invoice-dollar"></i> 基本面分析
254
+ </a>
255
+ <a href="/capital_flow" class="btn btn-info btn-lg px-4 me-md-2 mb-2">
256
+ <i class="fas fa-money-bill-wave"></i> 资金流向
257
+ </a>
258
+ <a href="/market_scan" class="btn btn-secondary btn-lg px-4 me-md-2 mb-2">
259
+ <i class="fas fa-search"></i> 市场扫描
260
+ </a>
261
+ <a href="/scenario_predict" class="btn btn-warning btn-lg px-4 me-md-2 mb-2">
262
+ <i class="fas fa-crystal-ball"></i> 情景预测
263
+ </a>
264
+ <a href="/portfolio" class="btn btn-dark btn-lg px-4 me-md-2 mb-2">
265
+ <i class="fas fa-briefcase"></i> 投资组合
266
+ </a>
267
+ <a href="/qa" class="btn btn-primary btn-lg px-4 me-md-2 mb-2">
268
+ <i class="fas fa-question-circle"></i> 智能问答
269
+ </a>
270
+ <a href="/risk_monitor" class="btn btn-danger btn-lg px-4 me-md-2 mb-2">
271
+ <i class="fas fa-exclamation-triangle"></i> 风险监控
272
+ </a>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ <!-- 快速分析模态框 -->
279
+ <div class="modal fade" id="quickAnalysisModal" tabindex="-1" aria-labelledby="quickAnalysisModalLabel" aria-hidden="true">
280
+ <div class="modal-dialog">
281
+ <div class="modal-content">
282
+ <div class="modal-header">
283
+ <h5 class="modal-title" id="quickAnalysisModalLabel">快速股票分析</h5>
284
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
285
+ </div>
286
+ <div class="modal-body">
287
+ <form id="quick-analysis-form">
288
+ <div class="mb-3">
289
+ <label for="quick-stock-code" class="form-label">股票代码</label>
290
+ <input type="text" class="form-control" id="quick-stock-code" placeholder="例如: 600519" required>
291
+ </div>
292
+ <div class="mb-3">
293
+ <label for="quick-market-type" class="form-label">市场类型</label>
294
+ <select class="form-select" id="quick-market-type">
295
+ <option value="A" selected>A股</option>
296
+ <option value="HK">港股</option>
297
+ <option value="US">美股</option>
298
+ </select>
299
+ </div>
300
+ </form>
301
+ </div>
302
+ <div class="modal-footer">
303
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
304
+ <button type="button" class="btn btn-primary" id="quick-analysis-btn">开始分析</button>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ {% endblock %}
310
+
311
+ {% block scripts %}
312
+ <script>
313
+ $(document).ready(function() {
314
+ // 快速分析按钮点击事件
315
+ $('#quick-analysis-btn').click(function() {
316
+ const stockCode = $('#quick-stock-code').val().trim();
317
+ const marketType = $('#quick-market-type').val();
318
+
319
+ if (!stockCode) {
320
+ alert('请输入股票代码');
321
+ return;
322
+ }
323
+
324
+ // 跳转到股票详情页
325
+ window.location.href = `/stock_detail/${stockCode}?market_type=${marketType}`;
326
+ });
327
+
328
+ // 回车键提交表单
329
+ $('#quick-stock-code').keypress(function(e) {
330
+ if (e.which === 13) {
331
+ $('#quick-analysis-btn').click();
332
+ return false;
333
+ }
334
+ });
335
+ });
336
+ </script>
337
+ {% endblock %}
templates/industry_analysis.html ADDED
@@ -0,0 +1,1135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}行业分析 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-3">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header py-2">
13
+ <h5 class="mb-0">行业资金流向分析</h5>
14
+ </div>
15
+ <div class="card-body py-2">
16
+ <form id="industry-form" class="row g-2">
17
+ <div class="col-md-3">
18
+ <div class="input-group input-group-sm">
19
+ <span class="input-group-text">周期</span>
20
+ <select class="form-select" id="fund-flow-period">
21
+ <option value="即时" selected>即时</option>
22
+ <option value="3日排行">3日排行</option>
23
+ <option value="5日排行">5日排行</option>
24
+ <option value="10日排行">10日排行</option>
25
+ <option value="20日排行">20日排行</option>
26
+ </select>
27
+ </div>
28
+ </div>
29
+ <div class="col-md-3">
30
+ <div class="input-group input-group-sm">
31
+ <span class="input-group-text">行业</span>
32
+ <select class="form-select" id="industry-selector">
33
+ <option value="">-- 选择行业 --</option>
34
+ <!-- 行业选项将通过JS动态填充 -->
35
+ </select>
36
+ </div>
37
+ </div>
38
+ <div class="col-md-2">
39
+ <button type="submit" class="btn btn-primary btn-sm w-100">
40
+ <i class="fas fa-search"></i> 分析
41
+ </button>
42
+ </div>
43
+ </form>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <div id="loading-panel" class="text-center py-5" style="display: none;">
50
+ <div class="spinner-border text-primary" role="status">
51
+ <span class="visually-hidden">Loading...</span>
52
+ </div>
53
+ <p class="mt-3 mb-0">正在获取行业数据...</p>
54
+ </div>
55
+
56
+ <!-- 行业资金流向概览 -->
57
+ <div id="industry-overview" class="row g-3 mb-3" style="display: none;">
58
+ <div class="col-12">
59
+ <div class="card">
60
+ <div class="card-header py-2 d-flex justify-content-between align-items-center">
61
+ <h5 class="mb-0">行业资金流向概览</h5>
62
+ <div class="d-flex">
63
+ <span id="period-badge" class="badge bg-primary ms-2">即时</span>
64
+ <button class="btn btn-sm btn-outline-primary ms-2" id="export-btn">
65
+ <i class="fas fa-download"></i> 导出数据
66
+ </button>
67
+ </div>
68
+ </div>
69
+ <div class="card-body p-0">
70
+ <div class="table-responsive">
71
+ <table class="table table-sm table-striped table-hover mb-0">
72
+ <thead>
73
+ <tr>
74
+ <th>序号</th>
75
+ <th>行业</th>
76
+ <th>行业指数</th>
77
+ <th>涨跌幅</th>
78
+ <th>流入资金(亿)</th>
79
+ <th>流出资金(亿)</th>
80
+ <th>净额(亿)</th>
81
+ <th>公司家数</th>
82
+ <th>领涨股</th>
83
+ <th>操作</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody id="industry-table">
87
+ <!-- 行业资金流向数据将在JS中动态填充 -->
88
+ </tbody>
89
+ </table>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- 行业详细分析 -->
97
+ <div id="industry-detail" class="row g-3 mb-3" style="display: none;">
98
+ <div class="col-md-6">
99
+ <div class="card h-100">
100
+ <div class="card-header py-2">
101
+ <h5 id="industry-name" class="mb-0">行业详情</h5>
102
+ </div>
103
+ <div class="card-body">
104
+ <div class="row">
105
+ <div class="col-md-6">
106
+ <h6>行业概况</h6>
107
+ <p><span class="text-muted">行业指数:</span> <span id="industry-index" class="fw-bold"></span></p>
108
+ <p><span class="text-muted">涨跌幅:</span> <span id="industry-change" class="fw-bold"></span></p>
109
+ <p><span class="text-muted">公司家数:</span> <span id="industry-company-count" class="fw-bold"></span></p>
110
+ </div>
111
+ <div class="col-md-6">
112
+ <h6>资金流向</h6>
113
+ <p><span class="text-muted">流入资金:</span> <span id="industry-inflow" class="fw-bold"></span></p>
114
+ <p><span class="text-muted">流出资金:</span> <span id="industry-outflow" class="fw-bold"></span></p>
115
+ <p><span class="text-muted">净额:</span> <span id="industry-net-flow" class="fw-bold"></span></p>
116
+ </div>
117
+ </div>
118
+ <div class="mt-3">
119
+ <div id="industry-flow-chart" style="height: 200px;"></div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="col-md-6">
126
+ <div class="card h-100">
127
+ <div class="card-header py-2">
128
+ <h5 class="mb-0">行业评分</h5>
129
+ </div>
130
+ <div class="card-body">
131
+ <div class="row">
132
+ <div class="col-md-5">
133
+ <div id="industry-score-chart" style="height: 150px;"></div>
134
+ <h4 id="industry-score" class="text-center mt-2">--</h4>
135
+ <p class="text-muted text-center">综合评分</p>
136
+ </div>
137
+ <div class="col-md-7">
138
+ <div class="mb-3">
139
+ <div class="d-flex justify-content-between mb-1">
140
+ <span>技术面</span>
141
+ <span id="technical-score">--/40</span>
142
+ </div>
143
+ <div class="progress">
144
+ <div id="technical-progress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
145
+ </div>
146
+ </div>
147
+ <div class="mb-3">
148
+ <div class="d-flex justify-content-between mb-1">
149
+ <span>基本面</span>
150
+ <span id="fundamental-score">--/40</span>
151
+ </div>
152
+ <div class="progress">
153
+ <div id="fundamental-progress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
154
+ </div>
155
+ </div>
156
+ <div class="mb-3">
157
+ <div class="d-flex justify-content-between mb-1">
158
+ <span>资金面</span>
159
+ <span id="capital-flow-score">--/20</span>
160
+ </div>
161
+ <div class="progress">
162
+ <div id="capital-flow-progress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ <div class="mt-3">
168
+ <h6>投资建议</h6>
169
+ <p id="industry-recommendation" class="mb-0"></p>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <!-- 行业成分股表现 -->
177
+ <div id="industry-stocks" class="row g-3 mb-3" style="display: none;">
178
+ <div class="col-12">
179
+ <div class="card">
180
+ <div class="card-header py-2">
181
+ <h5 class="mb-0">行业成分股表现</h5>
182
+ </div>
183
+ <div class="card-body p-0">
184
+ <div class="table-responsive">
185
+ <table class="table table-sm table-striped table-hover mb-0">
186
+ <thead>
187
+ <tr>
188
+ <th>代码</th>
189
+ <th>名称</th>
190
+ <th>最新价</th>
191
+ <th>涨跌幅</th>
192
+ <th>成交量</th>
193
+ <th>成交额(万)</th>
194
+ <th>换手率</th>
195
+ <th>评分</th>
196
+ <th>操作</th>
197
+ </tr>
198
+ </thead>
199
+ <tbody id="industry-stocks-table">
200
+ <!-- 行业成分股数据将在JS中动态填充 -->
201
+ </tbody>
202
+ </table>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- 行业对比分析 -->
210
+ <div class="row g-3 mb-3">
211
+ <div class="col-12">
212
+ <div class="card">
213
+ <div class="card-header py-2">
214
+ <h5 class="mb-0">行业对比分析</h5>
215
+ </div>
216
+ <div class="card-body">
217
+ <ul class="nav nav-tabs" id="industry-compare-tabs" role="tablist">
218
+ <li class="nav-item" role="presentation">
219
+ <button class="nav-link active" id="fund-flow-tab" data-bs-toggle="tab" data-bs-target="#fund-flow" type="button" role="tab" aria-controls="fund-flow" aria-selected="true">资金流向</button>
220
+ </li>
221
+ <li class="nav-item" role="presentation">
222
+ <button class="nav-link" id="performance-tab" data-bs-toggle="tab" data-bs-target="#performance" type="button" role="tab" aria-controls="performance" aria-selected="false">行业涨跌幅</button>
223
+ </li>
224
+ </ul>
225
+ <div class="tab-content mt-3" id="industry-compare-tabs-content">
226
+ <div class="tab-pane fade show active" id="fund-flow" role="tabpanel" aria-labelledby="fund-flow-tab">
227
+ <div class="row">
228
+ <div class="col-md-6">
229
+ <h6>资金净流入前10行业</h6>
230
+ <div id="top-inflow-chart" style="height: 300px;"></div>
231
+ </div>
232
+ <div class="col-md-6">
233
+ <h6>资金净流出前10行业</h6>
234
+ <div id="top-outflow-chart" style="height: 300px;"></div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ <div class="tab-pane fade" id="performance" role="tabpanel" aria-labelledby="performance-tab">
239
+ <div class="row">
240
+ <div class="col-md-6">
241
+ <h6>涨幅前10行业</h6>
242
+ <div id="top-gainers-chart" style="height: 300px;"></div>
243
+ </div>
244
+ <div class="col-md-6">
245
+ <h6>跌幅前10行业</h6>
246
+ <div id="top-losers-chart" style="height: 300px;"></div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ {% endblock %}
257
+
258
+ {% block scripts %}
259
+ <script>
260
+
261
+ $(document).ready(function() {
262
+ // 加载行业资金流向数据
263
+ loadIndustryFundFlow();
264
+
265
+ $('#industry-form').submit(function(e) {
266
+ e.preventDefault();
267
+
268
+ // 获取选择的行业
269
+ const industry = $('#industry-selector').val();
270
+
271
+ if (!industry) {
272
+ showError('请选择行业名称');
273
+ return;
274
+ }
275
+
276
+ // 分析行业
277
+ loadIndustryDetail(industry);
278
+ });
279
+
280
+ // 资金流向周期切换
281
+ $('#fund-flow-period').change(function() {
282
+ const period = $(this).val();
283
+ loadIndustryFundFlow(period);
284
+ });
285
+
286
+ // 导出按钮点击事件
287
+ $('#export-btn').click(function() {
288
+ exportToCSV();
289
+ });
290
+ });
291
+
292
+ function analyzeIndustry(industry) {
293
+ $('#loading-panel').show();
294
+ $('#industry-result').hide();
295
+
296
+ // 1. 获取行业详情
297
+ $.ajax({
298
+ url: `/api/industry_detail?industry=${encodeURIComponent(industry)}`,
299
+ type: 'GET',
300
+ success: function(industryDetail) {
301
+ console.log("Industry detail loaded successfully:", industryDetail);
302
+
303
+ // 2. 获取行业成分股
304
+ $.ajax({
305
+ url: `/api/industry_stocks?industry=${encodeURIComponent(industry)}`,
306
+ type: 'GET',
307
+ success: function(stocksResponse) {
308
+ console.log("Industry stocks loaded successfully:", stocksResponse);
309
+
310
+ $('#loading-panel').hide();
311
+
312
+ // 渲染行业详情和成分股
313
+ renderIndustryDetail(industryDetail);
314
+ renderIndustryStocks(stocksResponse);
315
+
316
+ $('#industry-detail').show();
317
+ $('#industry-stocks').show();
318
+ },
319
+ error: function(xhr, status, error) {
320
+ $('#loading-panel').hide();
321
+ console.error("Error loading industry stocks:", error);
322
+ showError('获取行业成分股失败: ' + (xhr.responseJSON?.error || error));
323
+ }
324
+ });
325
+ },
326
+ error: function(xhr, status, error) {
327
+ $('#loading-panel').hide();
328
+ console.error("Error loading industry detail:", error);
329
+ showError('获取行业详情失败: ' + (xhr.responseJSON?.error || error));
330
+ }
331
+ });
332
+ }
333
+
334
+ // 加载行业资金流向数据
335
+ function loadIndustryFundFlow(period = '即时') {
336
+ $('#loading-panel').show();
337
+ $('#industry-overview').hide();
338
+ $('#industry-detail').hide();
339
+ $('#industry-stocks').hide();
340
+
341
+ $.ajax({
342
+ url: `/api/industry_fund_flow?symbol=${encodeURIComponent(period)}`,
343
+ type: 'GET',
344
+ dataType: 'json',
345
+ success: function(response) {
346
+ if (Array.isArray(response) && response.length > 0) {
347
+ renderIndustryFundFlow(response, period);
348
+ populateIndustrySelector(response);
349
+
350
+ // 加载行业对比数据
351
+ loadIndustryCompare();
352
+
353
+ $('#loading-panel').hide();
354
+ $('#industry-overview').show();
355
+ } else {
356
+ showError('获取行业资金流向数据失败:返回数据为空');
357
+ $('#loading-panel').hide();
358
+ }
359
+ },
360
+ error: function(xhr, status, error) {
361
+ $('#loading-panel').hide();
362
+ let errorMsg = '获取行业资金流向数据失败';
363
+ if (xhr.responseJSON && xhr.responseJSON.error) {
364
+ errorMsg += ': ' + xhr.responseJSON.error;
365
+ } else if (error) {
366
+ errorMsg += ': ' + error;
367
+ }
368
+ showError(errorMsg);
369
+ }
370
+ });
371
+ }
372
+
373
+
374
+ // 统一的行业详情加载函数
375
+ function loadIndustryDetail(industry) {
376
+ console.log(`Loading industry detail for: ${industry}`);
377
+ $('#loading-panel').show();
378
+ $('#industry-overview').hide();
379
+ $('#industry-detail').hide();
380
+ $('#industry-stocks').hide();
381
+
382
+ // 并行加载行业详情和行业成分股
383
+ $.when(
384
+ // 获取行业详情
385
+ $.ajax({
386
+ url: `/api/industry_detail?industry=${encodeURIComponent(industry)}`,
387
+ type: 'GET',
388
+ dataType: 'json'
389
+ }),
390
+ // 获取行业成分股
391
+ $.ajax({
392
+ url: `/api/industry_stocks?industry=${encodeURIComponent(industry)}`,
393
+ type: 'GET',
394
+ dataType: 'json'
395
+ })
396
+ ).done(function(detailResponse, stocksResponse) {
397
+ // 处理行业详情数据
398
+ const industryData = detailResponse[0];
399
+
400
+ // 处理行业成分股数据
401
+ const stocksData = stocksResponse[0];
402
+
403
+ console.log("Industry detail loaded:", industryData);
404
+ console.log("Industry stocks loaded:", stocksData);
405
+
406
+ renderIndustryDetail(industryData);
407
+ renderIndustryStocks(stocksData);
408
+
409
+ $('#loading-panel').hide();
410
+ $('#industry-detail').show();
411
+ $('#industry-stocks').show();
412
+ }).fail(function(jqXHR, textStatus, errorThrown) {
413
+ $('#loading-panel').hide();
414
+ console.error("Error loading industry data:", textStatus, errorThrown);
415
+ let errorMsg = '获取行业数据失败';
416
+ try {
417
+ if (jqXHR.responseJSON && jqXHR.responseJSON.error) {
418
+ errorMsg += ': ' + jqXHR.responseJSON.error;
419
+ } else if (errorThrown) {
420
+ errorMsg += ': ' + errorThrown;
421
+ }
422
+ } catch (e) {
423
+ console.error("Error parsing error response:", e);
424
+ }
425
+ showError(errorMsg);
426
+ });
427
+ }
428
+
429
+ // 加载行业对比数据
430
+ function loadIndustryCompare() {
431
+ $.ajax({
432
+ url: '/api/industry_compare',
433
+ type: 'GET',
434
+ dataType: 'json',
435
+ success: function(response) {
436
+ try {
437
+ if (response && response.results) {
438
+ // 按资金净流入排序
439
+ const sortedByNetFlow = [...response.results]
440
+ .filter(item => item.netFlow !== undefined)
441
+ .sort((a, b) => parseFloat(b.netFlow || 0) - parseFloat(a.netFlow || 0));
442
+
443
+ // 按涨跌幅排序
444
+ const sortedByChange = [...response.results]
445
+ .filter(item => item.change !== undefined)
446
+ .sort((a, b) => parseFloat(b.change || 0) - parseFloat(a.change || 0));
447
+
448
+ // 资金净流入前10行业
449
+ const topInflow = sortedByNetFlow.slice(0, 10);
450
+ renderBarChart('top-inflow-chart',
451
+ topInflow.map(item => item.industry),
452
+ topInflow.map(item => parseFloat(item.netFlow || 0)),
453
+ '资金净流入(亿)',
454
+ '#00E396');
455
+
456
+ // 资金净流出前10行业
457
+ const bottomInflow = [...sortedByNetFlow].reverse().slice(0, 10);
458
+ renderBarChart('top-outflow-chart',
459
+ bottomInflow.map(item => item.industry),
460
+ bottomInflow.map(item => Math.abs(parseFloat(item.netFlow || 0))),
461
+ '资金净流出(亿)',
462
+ '#FF4560');
463
+
464
+ // 涨幅前10行业
465
+ const topGainers = sortedByChange.slice(0, 10);
466
+ renderBarChart('top-gainers-chart',
467
+ topGainers.map(item => item.industry),
468
+ topGainers.map(item => parseFloat(item.change || 0)),
469
+ '涨幅(%)',
470
+ '#00E396');
471
+
472
+ // 跌幅前10行业
473
+ const topLosers = [...sortedByChange].reverse().slice(0, 10);
474
+ renderBarChart('top-losers-chart',
475
+ topLosers.map(item => item.industry),
476
+ topLosers.map(item => Math.abs(parseFloat(item.change || 0))),
477
+ '跌幅(%)',
478
+ '#FF4560');
479
+ }
480
+ } catch (e) {
481
+ console.error("Error processing industry comparison data:", e);
482
+ }
483
+ },
484
+ error: function(xhr, status, error) {
485
+ console.error('获取行业对比数据失败:', error);
486
+ }
487
+ });
488
+ }
489
+
490
+ // 渲染行业资金流向表格
491
+ function renderIndustryFundFlow(data, period) {
492
+ $('#period-badge').text(period);
493
+
494
+ let html = '';
495
+ if (data.length === 0) {
496
+ html = '<tr><td colspan="10" class="text-center">暂无数据</td></tr>';
497
+ } else {
498
+ data.forEach((item, index) => {
499
+ const changeClass = parseFloat(item.change) >= 0 ? 'trend-up' : 'trend-down';
500
+ const changeIcon = parseFloat(item.change) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
501
+
502
+ const netFlowClass = parseFloat(item.netFlow) >= 0 ? 'trend-up' : 'trend-down';
503
+ const netFlowIcon = parseFloat(item.netFlow) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
504
+
505
+ html += `
506
+ <tr>
507
+ <td>${item.rank}</td>
508
+ <td>
509
+ <a href="javascript:void(0)" onclick="loadIndustryDetail('${item.industry}')" class="industry-link">
510
+ ${item.industry}
511
+ </a>
512
+ </td>
513
+ <td>${formatNumber(item.index, 2)}</td>
514
+ <td class="${changeClass}">${changeIcon} ${item.change}%</td>
515
+ <td>${formatNumber(item.inflow, 2)}</td>
516
+ <td>${formatNumber(item.outflow, 2)}</td>
517
+ <td class="${netFlowClass}">${netFlowIcon} ${formatNumber(item.netFlow, 2)}</td>
518
+ <td>${item.companyCount}</td>
519
+ <td>${item.leadingStock || '-'}</td>
520
+ <td>
521
+ <button class="btn btn-sm btn-outline-primary" onclick="loadIndustryDetail('${item.industry}')">
522
+ <i class="fas fa-search"></i>
523
+ </button>
524
+ </td>
525
+ </tr>
526
+ `;
527
+ });
528
+ }
529
+
530
+ $('#industry-table').html(html);
531
+ }
532
+
533
+ // 渲染行业详情
534
+ function renderIndustryDetail(data) {
535
+ if (!data) {
536
+ console.error("renderIndustryDetail: No data provided");
537
+ return;
538
+ }
539
+
540
+ console.log("Rendering industry detail:", data);
541
+
542
+ // 设置基本信息
543
+ $('#industry-name').text(data.industry);
544
+
545
+ // 设置行业评分
546
+ const scoreClass = getScoreColorClass(data.score);
547
+ $('#industry-score').text(data.score).removeClass().addClass(scoreClass);
548
+
549
+ // 设置技术面、基本面、资金面分数 (模拟分数)
550
+ const technicalScore = Math.round(data.score * 0.4);
551
+ const fundamentalScore = Math.round(data.score * 0.4);
552
+ const capitalFlowScore = Math.round(data.score * 0.2);
553
+
554
+ $('#technical-score').text(`${technicalScore}/40`);
555
+ $('#fundamental-score').text(`${fundamentalScore}/40`);
556
+ $('#capital-flow-score').text(`${capitalFlowScore}/20`);
557
+
558
+ $('#technical-progress').css('width', `${technicalScore / 40 * 100}%`);
559
+ $('#fundamental-progress').css('width', `${fundamentalScore / 40 * 100}%`);
560
+ $('#capital-flow-progress').css('width', `${capitalFlowScore / 20 * 100}%`);
561
+
562
+ // 设置行业基本信息
563
+ $('#industry-index').text(formatNumber(data.index, 2));
564
+ $('#industry-company-count').text(data.companyCount);
565
+
566
+ // 设置涨跌幅
567
+ const changeClass = parseFloat(data.change) >= 0 ? 'trend-up' : 'trend-down';
568
+ const changeIcon = parseFloat(data.change) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
569
+ $('#industry-change').html(`<span class="${changeClass}">${changeIcon} ${data.change}%</span>`);
570
+
571
+ // 设置资金流向
572
+ $('#industry-inflow').text(formatNumber(data.inflow, 2) + ' 亿');
573
+ $('#industry-outflow').text(formatNumber(data.outflow, 2) + ' 亿');
574
+
575
+ const netFlowClass = parseFloat(data.netFlow) >= 0 ? 'trend-up' : 'trend-down';
576
+ const netFlowIcon = parseFloat(data.netFlow) >= 0 ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
577
+ $('#industry-net-flow').html(`<span class="${netFlowClass}">${netFlowIcon} ${formatNumber(data.netFlow, 2)} 亿</span>`);
578
+
579
+ // 设置投资建议
580
+ $('#industry-recommendation').text(data.recommendation);
581
+
582
+ // 绘制行业评分图表
583
+ renderIndustryScoreChart(data.score);
584
+
585
+ // 绘制资金流向图表
586
+ renderIndustryFlowChart(data.flowHistory);
587
+ }
588
+
589
+
590
+ // 渲染行业成分股表格
591
+ function renderIndustryStocks(data) {
592
+ if (!data) {
593
+ console.error("renderIndustryStocks: No data provided");
594
+ return;
595
+ }
596
+
597
+ console.log("Rendering industry stocks:", data);
598
+
599
+ let html = '';
600
+
601
+ if (!Array.isArray(data) || data.length === 0) {
602
+ html = '<tr><td colspan="9" class="text-center">暂无成分股数据</td></tr>';
603
+ } else {
604
+ data.forEach(stock => {
605
+ const changeClass = parseFloat(stock.change) >= 0 ? 'trend-up' : 'trend-down';
606
+ const changeIcon = parseFloat(stock.change) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
607
+
608
+ html += `
609
+ <tr>
610
+ <td>${stock.code}</td>
611
+ <td>${stock.name}</td>
612
+ <td>${formatNumber(stock.price, 2)}</td>
613
+ <td class="${changeClass}">${changeIcon} ${formatNumber(stock.change, 2)}%</td>
614
+ <td>${formatNumber(stock.volume, 0)}</td>
615
+ <td>${formatMoney(stock.turnover)}</td>
616
+ <td>${formatNumber(stock.turnover_rate || stock.turnoverRate, 2)}%</td>
617
+ <td>${stock.score ? formatNumber(stock.score, 0) : '-'}</td>
618
+ <td>
619
+ <a href="/stock_detail/${stock.code}" class="btn btn-sm btn-outline-primary">
620
+ <i class="fas fa-chart-line"></i>
621
+ </a>
622
+ </td>
623
+ </tr>
624
+ `;
625
+ });
626
+ }
627
+
628
+ $('#industry-stocks-table').html(html);
629
+ }
630
+
631
+ function renderCapitalFlowChart(flowHistory) {
632
+
633
+ // 添加数据检查
634
+ if (!flowHistory || !Array.isArray(flowHistory) || flowHistory.length === 0) {
635
+ // 如果没有历史数据,显示提示信息
636
+ document.querySelector("#industry-flow-chart").innerHTML =
637
+ '<div class="text-center text-muted py-5">暂无资金流向历史数据</div>';
638
+ return;
639
+ }
640
+
641
+ const dates = flowHistory.map(item => item.date);
642
+ const netFlows = flowHistory.map(item => parseFloat(item.netFlow));
643
+ const changes = flowHistory.map(item => parseFloat(item.change));
644
+
645
+ // 确保所有数组都有值
646
+ if (dates.length === 0 || netFlows.length === 0 || changes.length === 0) {
647
+ document.querySelector("#industry-flow-chart").innerHTML =
648
+ '<div class="text-center text-muted py-5">资金流向数据格式不正确</div>';
649
+ return;
650
+ }
651
+
652
+ const options = {
653
+ series: [
654
+ {
655
+ name: '净流入(亿)',
656
+ type: 'column',
657
+ data: netFlows
658
+ },
659
+ {
660
+ name: '涨跌幅(%)',
661
+ type: 'line',
662
+ data: changes
663
+ }
664
+ ],
665
+ chart: {
666
+ height: 265,
667
+ type: 'line',
668
+ toolbar: {
669
+ show: false
670
+ }
671
+ },
672
+ plotOptions: {
673
+ bar: {
674
+ borderRadius: 2,
675
+ dataLabels: {
676
+ position: 'top'
677
+ }
678
+ }
679
+ },
680
+ dataLabels: {
681
+ enabled: false
682
+ },
683
+ stroke: {
684
+ width: [0, 3]
685
+ },
686
+ colors: ['#0d6efd', '#dc3545'],
687
+ xaxis: {
688
+ categories: dates,
689
+ labels: {
690
+ formatter: function(value) {
691
+ return value.slice(5); // 只显示月-日
692
+ }
693
+ }
694
+ },
695
+ yaxis: [
696
+ {
697
+ title: {
698
+ text: '净流入(亿)',
699
+ style: {
700
+ fontSize: '12px'
701
+ }
702
+ },
703
+ labels: {
704
+ formatter: function(val) {
705
+ return val.toFixed(2);
706
+ }
707
+ }
708
+ },
709
+ {
710
+ opposite: true,
711
+ title: {
712
+ text: '涨跌幅(%)',
713
+ style: {
714
+ fontSize: '12px'
715
+ }
716
+ },
717
+ labels: {
718
+ formatter: function(val) {
719
+ return val.toFixed(2);
720
+ }
721
+ }
722
+ }
723
+ ],
724
+ tooltip: {
725
+ shared: true,
726
+ intersect: false,
727
+ y: {
728
+ formatter: function(value, { seriesIndex }) {
729
+ if (seriesIndex === 0) {
730
+ return value.toFixed(2) + ' 亿';
731
+ }
732
+ return value.toFixed(2) + '%';
733
+ }
734
+ }
735
+ },
736
+ legend: {
737
+ position: 'top'
738
+ }
739
+ };
740
+
741
+ // 清除任何现有图表
742
+ document.querySelector("#industry-flow-chart").innerHTML = '';
743
+ const chart = new ApexCharts(document.querySelector("#industry-flow-chart"), options);
744
+ chart.render();
745
+ }
746
+
747
+ // 填充行业选择器
748
+ function populateIndustrySelector(data) {
749
+ let options = '<option value="">-- 选择行业 --</option>';
750
+ const industries = data.map(item => item.industry);
751
+
752
+ industries.forEach(industry => {
753
+ options += `<option value="${industry}">${industry}</option>`;
754
+ });
755
+
756
+ $('#industry-selector').html(options);
757
+ }
758
+
759
+ // 评分颜色类
760
+ function getScoreColorClass(score) {
761
+ if (score >= 80) return 'badge rounded-pill bg-success';
762
+ if (score >= 60) return 'badge rounded-pill bg-primary';
763
+ if (score >= 40) return 'badge rounded-pill bg-warning text-dark';
764
+ return 'badge rounded-pill bg-danger';
765
+ }
766
+
767
+ // 获取评分颜色
768
+ function getScoreColor(score) {
769
+ if (score >= 80) return '#28a745'; // 绿色
770
+ if (score >= 60) return '#007bff'; // 蓝色
771
+ if (score >= 40) return '#ffc107'; // 黄色
772
+ return '#dc3545'; // 红色
773
+ }
774
+
775
+ // 格式化金额(单位:万元)
776
+ function formatMoney(value) {
777
+ if (value === undefined || value === null) {
778
+ return '--';
779
+ }
780
+
781
+ value = parseFloat(value);
782
+ if (isNaN(value)) {
783
+ return '--';
784
+ }
785
+
786
+ if (value >= 100000000) {
787
+ return (value / 100000000).toFixed(2) + ' 亿';
788
+ } else if (value >= 10000) {
789
+ return (value / 10000).toFixed(2) + ' 万';
790
+ } else {
791
+ return value.toFixed(2);
792
+ }
793
+ }
794
+
795
+ // 渲染行业资金流向图表
796
+ function renderIndustryFlowChart(data) {
797
+ const options = {
798
+ series: [
799
+ {
800
+ name: '流入资金',
801
+ data: data.flowHistory.map(item => item.inflow)
802
+ },
803
+ {
804
+ name: '流出资金',
805
+ data: data.flowHistory.map(item => item.outflow)
806
+ },
807
+ {
808
+ name: '净流入',
809
+ data: data.flowHistory.map(item => item.netFlow)
810
+ }
811
+ ],
812
+ chart: {
813
+ type: 'bar',
814
+ height: 200,
815
+ toolbar: {
816
+ show: false
817
+ }
818
+ },
819
+ plotOptions: {
820
+ bar: {
821
+ horizontal: false,
822
+ columnWidth: '55%',
823
+ endingShape: 'rounded'
824
+ },
825
+ },
826
+ dataLabels: {
827
+ enabled: false
828
+ },
829
+ stroke: {
830
+ show: true,
831
+ width: 2,
832
+ colors: ['transparent']
833
+ },
834
+ xaxis: {
835
+ categories: data.flowHistory.map(item => item.date)
836
+ },
837
+ yaxis: {
838
+ title: {
839
+ text: '亿元'
840
+ }
841
+ },
842
+ fill: {
843
+ opacity: 1
844
+ },
845
+ tooltip: {
846
+ y: {
847
+ formatter: function(val) {
848
+ return val + " 亿元";
849
+ }
850
+ }
851
+ },
852
+ colors: ['#00E396', '#FF4560', '#008FFB']
853
+ };
854
+
855
+ const chart = new ApexCharts(document.querySelector("#industry-flow-chart"), options);
856
+ chart.render();
857
+ }
858
+
859
+ // 绘制行业评分图表
860
+ function renderIndustryScoreChart(score) {
861
+ const options = {
862
+ series: [score],
863
+ chart: {
864
+ height: 150,
865
+ type: 'radialBar',
866
+ },
867
+ plotOptions: {
868
+ radialBar: {
869
+ hollow: {
870
+ size: '70%',
871
+ },
872
+ dataLabels: {
873
+ show: false
874
+ }
875
+ }
876
+ },
877
+ colors: [getScoreColor(score)],
878
+ stroke: {
879
+ lineCap: 'round'
880
+ }
881
+ };
882
+
883
+ // 清除旧图表并创建新图表
884
+ $('#industry-score-chart').empty();
885
+ const chart = new ApexCharts(document.querySelector("#industry-score-chart"), options);
886
+ chart.render();
887
+ }
888
+
889
+ // 绘制行业资金流向图表
890
+ function renderIndustryFlowChart(flowHistory) {
891
+ if (!flowHistory || !Array.isArray(flowHistory) || flowHistory.length === 0) {
892
+ console.error("renderIndustryFlowChart: Invalid flow history data");
893
+ return;
894
+ }
895
+
896
+ console.log("Rendering flow chart with data:", flowHistory);
897
+
898
+ const dates = flowHistory.map(item => item.date);
899
+ const netFlows = flowHistory.map(item => parseFloat(item.netFlow));
900
+ const changes = flowHistory.map(item => parseFloat(item.change));
901
+
902
+ const options = {
903
+ series: [
904
+ {
905
+ name: '净流入(亿)',
906
+ type: 'column',
907
+ data: netFlows
908
+ },
909
+ {
910
+ name: '涨跌幅(%)',
911
+ type: 'line',
912
+ data: changes
913
+ }
914
+ ],
915
+ chart: {
916
+ height: 200,
917
+ type: 'line',
918
+ toolbar: {
919
+ show: false
920
+ }
921
+ },
922
+ plotOptions: {
923
+ bar: {
924
+ borderRadius: 2,
925
+ dataLabels: {
926
+ position: 'top'
927
+ }
928
+ }
929
+ },
930
+ dataLabels: {
931
+ enabled: false
932
+ },
933
+ stroke: {
934
+ width: [0, 3]
935
+ },
936
+ colors: ['#0d6efd', '#dc3545'],
937
+ xaxis: {
938
+ categories: dates,
939
+ labels: {
940
+ formatter: function(value) {
941
+ // Only show month-day if it's a date string
942
+ if (typeof value === 'string' && value.includes('-')) {
943
+ return value.slice(5); // 只显示月-日
944
+ }
945
+ return value;
946
+ }
947
+ }
948
+ },
949
+ yaxis: [
950
+ {
951
+ title: {
952
+ text: '净流入(亿)',
953
+ style: {
954
+ fontSize: '12px'
955
+ }
956
+ },
957
+ labels: {
958
+ formatter: function(val) {
959
+ return val.toFixed(2);
960
+ }
961
+ }
962
+ },
963
+ {
964
+ opposite: true,
965
+ title: {
966
+ text: '涨跌幅(%)',
967
+ style: {
968
+ fontSize: '12px'
969
+ }
970
+ },
971
+ labels: {
972
+ formatter: function(val) {
973
+ return val.toFixed(2);
974
+ }
975
+ }
976
+ }
977
+ ],
978
+ tooltip: {
979
+ shared: true,
980
+ intersect: false,
981
+ y: {
982
+ formatter: function(value, { seriesIndex }) {
983
+ if (seriesIndex === 0) {
984
+ return value.toFixed(2) + ' 亿';
985
+ }
986
+ return value.toFixed(2) + '%';
987
+ }
988
+ }
989
+ },
990
+ legend: {
991
+ position: 'top'
992
+ }
993
+ };
994
+
995
+ // 清除旧图表并创建新图表
996
+ $('#industry-flow-chart').empty();
997
+ try {
998
+ const chart = new ApexCharts(document.querySelector("#industry-flow-chart"), options);
999
+ chart.render();
1000
+ } catch (e) {
1001
+ console.error("Error rendering flow chart:", e);
1002
+ }
1003
+ }
1004
+
1005
+ // 渲染行业对比图表
1006
+ function renderIndustryCompareCharts(data) {
1007
+ // 按资金净流入排序
1008
+ const sortedByNetFlow = [...data].sort((a, b) => b.netFlow - a.netFlow);
1009
+
1010
+ // 资金净流入前10
1011
+ const topInflow = sortedByNetFlow.slice(0, 10);
1012
+ renderBarChart('top-inflow-chart', topInflow.map(item => item.industry), topInflow.map(item => item.netFlow), '资金净流入(亿元)', '#00E396');
1013
+
1014
+ // 资金净流出前10
1015
+ const bottomInflow = [...sortedByNetFlow].reverse().slice(0, 10);
1016
+ renderBarChart('top-outflow-chart', bottomInflow.map(item => item.industry), bottomInflow.map(item => Math.abs(item.netFlow)), '资金净流出(亿元)', '#FF4560');
1017
+
1018
+ // 按涨跌幅排序
1019
+ const sortedByChange = [...data].sort((a, b) => parseFloat(b.change) - parseFloat(a.change));
1020
+
1021
+ // 涨幅前10
1022
+ const topGainers = sortedByChange.slice(0, 10);
1023
+ renderBarChart('top-gainers-chart', topGainers.map(item => item.industry), topGainers.map(item => parseFloat(item.change)), '涨幅(%)', '#00E396');
1024
+
1025
+ // 跌幅前10
1026
+ const topLosers = [...sortedByChange].reverse().slice(0, 10);
1027
+ renderBarChart('top-losers-chart', topLosers.map(item => item.industry), topLosers.map(item => Math.abs(parseFloat(item.change))), '跌幅(%)', '#FF4560');
1028
+ }
1029
+
1030
+ // 通用水平条形图
1031
+ function renderBarChart(elementId, categories, data, title, color) {
1032
+ if (!categories || !data || categories.length === 0 || data.length === 0) {
1033
+ console.error(`renderBarChart: Invalid data for ${elementId}`);
1034
+ return;
1035
+ }
1036
+
1037
+ const options = {
1038
+ series: [{
1039
+ name: title,
1040
+ data: data
1041
+ }],
1042
+ chart: {
1043
+ type: 'bar',
1044
+ height: 300,
1045
+ toolbar: {
1046
+ show: false
1047
+ }
1048
+ },
1049
+ plotOptions: {
1050
+ bar: {
1051
+ horizontal: true,
1052
+ dataLabels: {
1053
+ position: 'top',
1054
+ },
1055
+ }
1056
+ },
1057
+ dataLabels: {
1058
+ enabled: true,
1059
+ offsetX: -6,
1060
+ style: {
1061
+ fontSize: '12px',
1062
+ colors: ['#fff']
1063
+ },
1064
+ formatter: function(val) {
1065
+ return val.toFixed(2);
1066
+ }
1067
+ },
1068
+ stroke: {
1069
+ show: true,
1070
+ width: 1,
1071
+ colors: ['#fff']
1072
+ },
1073
+ xaxis: {
1074
+ categories: categories
1075
+ },
1076
+ yaxis: {
1077
+ title: {
1078
+ text: title
1079
+ }
1080
+ },
1081
+ fill: {
1082
+ opacity: 1
1083
+ },
1084
+ colors: [color]
1085
+ };
1086
+
1087
+ // 清除旧图表并创建新图表
1088
+ $(`#${elementId}`).empty();
1089
+ try {
1090
+ const chart = new ApexCharts(document.querySelector(`#${elementId}`), options);
1091
+ chart.render();
1092
+ } catch (e) {
1093
+ console.error(`Error rendering chart ${elementId}:`, e);
1094
+ }
1095
+ }
1096
+
1097
+ // 导出CSV
1098
+ function exportToCSV() {
1099
+ // 获取表格数据
1100
+ const table = document.querySelector('#industry-overview table');
1101
+ let csv = [];
1102
+ let rows = table.querySelectorAll('tr');
1103
+
1104
+ for (let i = 0; i < rows.length; i++) {
1105
+ let row = [], cols = rows[i].querySelectorAll('td, th');
1106
+
1107
+ for (let j = 0; j < cols.length - 1; j++) { // 跳过最后一列(操作列)
1108
+ // 获取单元格的文本内容,去除HTML标签
1109
+ let text = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/,/g, ',');
1110
+ row.push(text);
1111
+ }
1112
+
1113
+ csv.push(row.join(','));
1114
+ }
1115
+
1116
+ // 下载CSV文件
1117
+ const period = $('#period-badge').text();
1118
+ const csvString = csv.join('\n');
1119
+ const filename = `行业资金流向_${period}_${new Date().toISOString().slice(0, 10)}.csv`;
1120
+
1121
+ const blob = new Blob(['\uFEFF' + csvString], { type: 'text/csv;charset=utf-8;' });
1122
+ const link = document.createElement('a');
1123
+ link.href = URL.createObjectURL(blob);
1124
+ link.download = filename;
1125
+
1126
+ link.style.display = 'none';
1127
+ document.body.appendChild(link);
1128
+
1129
+ link.click();
1130
+
1131
+ document.body.removeChild(link);
1132
+ }
1133
+
1134
+ </script>
1135
+ {% endblock %}
templates/layout.html ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
6
+ <link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>{% block title %}智能分析系统{% endblock %}</title>
9
+ <!-- Bootstrap CSS -->
10
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
11
+ <!-- Font Awesome -->
12
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
13
+ <!-- ApexCharts -->
14
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/apexcharts.min.css" rel="stylesheet">
15
+ <!-- Custom CSS -->
16
+ <style>
17
+ body {
18
+ font-family: 'Helvetica Neue', Arial, sans-serif;
19
+ background-color: #f8f9fa;
20
+ }
21
+ .navbar-brand {
22
+ font-weight: bold;
23
+ }
24
+ .nav-item {
25
+ margin-left: 10px;
26
+ }
27
+ .sidebar {
28
+ background-color: #343a40;
29
+ color: white;
30
+ min-height: calc(100vh - 56px);
31
+ }
32
+ .sidebar .nav-link {
33
+ color: #ced4da;
34
+ padding: 0.75rem 1rem;
35
+ }
36
+ .sidebar .nav-link:hover {
37
+ color: #fff;
38
+ background-color: rgba(255, 255, 255, 0.1);
39
+ }
40
+ .sidebar .nav-link.active {
41
+ color: #fff;
42
+ background-color: rgba(255, 255, 255, 0.2);
43
+ }
44
+ .sidebar .nav-link i {
45
+ margin-right: 10px;
46
+ width: 20px;
47
+ text-align: center;
48
+ }
49
+ .main-content {
50
+ padding: 20px;
51
+ }
52
+ .card {
53
+ margin-bottom: 20px;
54
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
55
+ overflow: hidden; /* Prevent content from stretching container */
56
+ }
57
+
58
+ .card-header {
59
+ padding: 0.5rem 1rem;
60
+ height: auto !important;
61
+ max-height: 50px;
62
+ }
63
+
64
+ .form-control, .form-select, .input-group-text {
65
+ font-size: 0.875rem;
66
+ }
67
+
68
+ .input-group-sm .input-group-text {
69
+ padding: 0.25rem 0.5rem;
70
+ }
71
+
72
+ .card-body.py-2 {
73
+ padding-top: 0.5rem;
74
+ padding-bottom: 0.5rem;
75
+ }
76
+
77
+
78
+ .card-body {
79
+ padding: 1.25rem;
80
+ overflow: hidden; /* Prevent content from stretching container */
81
+ }
82
+ .loading {
83
+ display: flex;
84
+ justify-content: center;
85
+ align-items: center;
86
+ height: 200px;
87
+ }
88
+ .spinner-border {
89
+ width: 3rem;
90
+ height: 3rem;
91
+ }
92
+ .badge-success {
93
+ background-color: #28a745;
94
+ }
95
+ .badge-danger {
96
+ background-color: #dc3545;
97
+ }
98
+ .badge-warning {
99
+ background-color: #ffc107;
100
+ color: #212529;
101
+ }
102
+ .score-pill {
103
+ font-size: 1.2rem;
104
+ padding: 0.5rem 1rem;
105
+ }
106
+ #loading-overlay {
107
+ position: fixed;
108
+ top: 0;
109
+ left: 0;
110
+ width: 100%;
111
+ height: 100%;
112
+ background-color: rgba(255, 255, 255, 0.8);
113
+ display: none;
114
+ justify-content: center;
115
+ align-items: center;
116
+ z-index: 9999;
117
+ }
118
+ .text-strong {
119
+ font-weight: bold;
120
+ }
121
+ .text-larger {
122
+ font-size: 1.1em;
123
+ }
124
+ .trend-up {
125
+ color: #28a745;
126
+ }
127
+ .trend-down {
128
+ color: #dc3545;
129
+ }
130
+ .analysis-section {
131
+ margin-bottom: 1.5rem;
132
+ }
133
+
134
+ /* Fix for chart container heights */
135
+ #price-chart {
136
+ height: 400px !important;
137
+ max-height: 400px;
138
+ }
139
+
140
+ /* Fix for indicators chart container */
141
+ #indicators-chart {
142
+ height: 350px !important;
143
+ max-height: 350px;
144
+ }
145
+
146
+ /* Fix chart containers */
147
+ .apexcharts-canvas {
148
+ overflow: visible !important;
149
+ }
150
+
151
+ /* Fix for radar chart */
152
+ #radar-chart {
153
+ height: 200px !important;
154
+ max-height: 200px;
155
+ }
156
+
157
+ /* Fix for score chart */
158
+ #score-chart {
159
+ height: 200px !important;
160
+ max-height: 200px;
161
+ }
162
+
163
+ /* Fix header alignment */
164
+ .card-header h5 {
165
+ margin-bottom: 0;
166
+ display: flex;
167
+ align-items: center;
168
+ }
169
+
170
+ .apexcharts-tooltip {
171
+ background: #fff !important;
172
+ border: 1px solid #e3e3e3 !important;
173
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
174
+ border-radius: 4px !important;
175
+ padding: 8px !important;
176
+ font-size: 13px !important;
177
+ }
178
+
179
+ .apexcharts-tooltip-title {
180
+ background: #f8f9fa !important;
181
+ border-bottom: 1px solid #e3e3e3 !important;
182
+ padding: 6px 8px !important;
183
+ margin-bottom: 4px !important;
184
+ font-weight: 600 !important;
185
+ }
186
+
187
+ .apexcharts-tooltip-y-group {
188
+ padding: 3px 0 !important;
189
+ }
190
+
191
+ .apexcharts-tooltip-candlestick {
192
+ padding: 5px 8px !important;
193
+ }
194
+
195
+ .apexcharts-tooltip-candlestick div {
196
+ margin: 3px 0 !important;
197
+ }
198
+
199
+ .apexcharts-tooltip-candlestick span {
200
+ font-weight: 600 !important;
201
+ }
202
+
203
+ .apexcharts-crosshairs {
204
+ stroke-width: 1px !important;
205
+ stroke: #90A4AE !important;
206
+ stroke-dasharray: 0 !important;
207
+ opacity: 0.8 !important;
208
+ }
209
+
210
+ .apexcharts-tooltip-marker {
211
+ width: 10px !important;
212
+ height: 10px !important;
213
+ display: inline-block !important;
214
+ margin-right: 5px !important;
215
+ border-radius: 50% !important;
216
+ }
217
+
218
+ .apexcharts-tooltip-series-group {
219
+ padding: 4px 8px !important;
220
+ border-bottom: 1px solid #eee !important;
221
+ }
222
+
223
+ .apexcharts-tooltip-series-group:last-child {
224
+ border-bottom: none !important;
225
+ }
226
+
227
+ .apexcharts-tooltip-text-y-value {
228
+ font-weight: 600 !important;
229
+ }
230
+
231
+ .apexcharts-xaxistooltip {
232
+ background: #fff !important;
233
+ border: 1px solid #e3e3e3 !important;
234
+ border-radius: 2px !important;
235
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
236
+ padding: 4px 8px !important;
237
+ font-size: 12px !important;
238
+ color: #333 !important;
239
+ }
240
+
241
+ .apexcharts-yaxistooltip {
242
+ background: #fff !important;
243
+ border: 1px solid #e3e3e3 !important;
244
+ border-radius: 2px !important;
245
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
246
+ padding: 4px 8px !important;
247
+ font-size: 12px !important;
248
+ color: #333 !important;
249
+ }
250
+
251
+ /* AI分析样式 */
252
+ .analysis-para {
253
+ line-height: 1.8;
254
+ margin-bottom: 1.2rem;
255
+ color: #333;
256
+ }
257
+
258
+ .keyword {
259
+ color: #2c7be5;
260
+ font-weight: 600;
261
+ }
262
+
263
+ .term {
264
+ color: #d6336c;
265
+ font-weight: 500;
266
+ padding: 0 2px;
267
+ }
268
+
269
+ .price {
270
+ color: #00a47c;
271
+ font-family: 'Roboto Mono', monospace;
272
+ background: #f3faf8;
273
+ padding: 2px 4px;
274
+ border-radius: 3px;
275
+ }
276
+
277
+ .date {
278
+ color: #6c757d;
279
+ font-family: 'Roboto Mono', monospace;
280
+ }
281
+
282
+ strong.keyword {
283
+ border-bottom: 2px solid #2c7be5;
284
+ }
285
+
286
+ .table-info {
287
+ position: relative;
288
+ }
289
+
290
+ .table-info:after {
291
+ content: '';
292
+ position: absolute;
293
+ top: 0;
294
+ left: 0;
295
+ right: 0;
296
+ bottom: 0;
297
+ background: rgba(0, 123, 255, 0.1);
298
+ animation: pulse 1.5s infinite;
299
+ }
300
+
301
+ @keyframes pulse {
302
+ 0% { opacity: 0.5; }
303
+ 50% { opacity: 0.3; }
304
+ 100% { opacity: 0.5; }
305
+ }
306
+
307
+ </style>
308
+ {% block head %}{% endblock %}
309
+ </head>
310
+ <body>
311
+ <!-- Loading Overlay -->
312
+ <div id="loading-overlay">
313
+ <div class="spinner-border text-primary" role="status">
314
+ <span class="visually-hidden">Loading...</span>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- Top Navigation -->
319
+ <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
320
+ <div class="container-fluid">
321
+ <a class="navbar-brand" href="/">智能分析系统</a>
322
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
323
+ <span class="navbar-toggler-icon"></span>
324
+ </button>
325
+ <!-- 在layout.html的导航栏部分修改 -->
326
+ <div class="collapse navbar-collapse" id="navbarNav">
327
+ <ul class="navbar-nav me-auto">
328
+ <li class="nav-item">
329
+ <a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/"><i class="fas fa-home"></i> 主页</a>
330
+ </li>
331
+ <li class="nav-item">
332
+ <a class="nav-link {% if request.path == '/dashboard' %}active{% endif %}" href="/dashboard"><i class="fas fa-chart-line"></i> 智能仪表盘</a>
333
+ </li>
334
+ <!-- 新增菜单项 - 基本面分析 -->
335
+ <li class="nav-item">
336
+ <a class="nav-link {% if request.path.startswith('/fundamental') %}active{% endif %}" href="/fundamental"><i class="fas fa-file-invoice-dollar"></i> 基本面分析</a>
337
+ </li>
338
+ <!-- 新增菜单项 - 资金流向 -->
339
+ <li class="nav-item">
340
+ <a class="nav-link {% if request.path.startswith('/capital_flow') %}active{% endif %}" href="/capital_flow"><i class="fas fa-money-bill-wave"></i> 资金流向</a>
341
+ </li>
342
+ <!-- 新增菜单项 - 情景预测 -->
343
+ <li class="nav-item">
344
+ <a class="nav-link {% if request.path.startswith('/scenario') %}active{% endif %}" href="/scenario_predict"><i class="fas fa-lightbulb"></i> 情景预测</a>
345
+ </li>
346
+ <li class="nav-item">
347
+ <a class="nav-link {% if request.path == '/market_scan' %}active{% endif %}" href="/market_scan"><i class="fas fa-search"></i> 市场扫描</a>
348
+ </li>
349
+ <li class="nav-item">
350
+ <a class="nav-link {% if request.path == '/portfolio' %}active{% endif %}" href="/portfolio"><i class="fas fa-briefcase"></i> 投资组合</a>
351
+ </li>
352
+ <!-- 新增菜单项 - 风险监控 -->
353
+ <li class="nav-item">
354
+ <a class="nav-link {% if request.path.startswith('/risk') %}active{% endif %}" href="/risk_monitor"><i class="fas fa-exclamation-triangle"></i> 风险监控</a>
355
+ </li>
356
+ <!-- 新增菜单项 - 智能问答 -->
357
+ <li class="nav-item">
358
+ <a class="nav-link {% if request.path == '/qa' %}active{% endif %}" href="/qa"><i class="fas fa-question-circle"></i> 智能问答</a>
359
+ </li>
360
+ </ul>
361
+ <div class="d-flex">
362
+ <div class="input-group">
363
+ <input type="text" id="search-stock" class="form-control" placeholder="搜索股票代码/名称" aria-label="搜索股票">
364
+ <button class="btn btn-light" type="button" id="search-button">
365
+ <i class="fas fa-search"></i>
366
+ </button>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </nav>
372
+
373
+ <div class="container-fluid">
374
+ <div class="row">
375
+ {% block sidebar %}{% endblock %}
376
+
377
+ <main class="{% if self.sidebar()|trim %}col-md-9 ms-sm-auto col-lg-10 px-md-4{% else %}col-12{% endif %} main-content">
378
+ {% block content %}{% endblock %}
379
+ </main>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- Bootstrap JS with Popper -->
384
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
385
+ <!-- jQuery -->
386
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
387
+ <!-- ApexCharts -->
388
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/apexcharts.min.js"></script>
389
+ <!-- Common JS -->
390
+ <script>
391
+ // 显示加载中覆盖层
392
+ function showLoading() {
393
+ $('#loading-overlay').css('display', 'flex');
394
+ }
395
+
396
+ // 隐藏加载中覆盖层
397
+ function hideLoading() {
398
+ $('#loading-overlay').css('display', 'none');
399
+ }
400
+
401
+ // 显示错误提示
402
+ function showError(message) {
403
+ const alertHtml = `
404
+ <div class="alert alert-danger alert-dismissible fade show" role="alert">
405
+ ${message}
406
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
407
+ </div>
408
+ `;
409
+ $('#alerts-container').html(alertHtml);
410
+ }
411
+
412
+ // 显示信息提示
413
+ function showInfo(message) {
414
+ const alertHtml = `
415
+ <div class="alert alert-info alert-dismissible fade show" role="alert">
416
+ <i class="fas fa-info-circle me-2"></i>${message}
417
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
418
+ </div>
419
+ `;
420
+ $('#alerts-container').html(alertHtml);
421
+ }
422
+
423
+ // 显示成功提示
424
+ function showSuccess(message) {
425
+ const alertHtml = `
426
+ <div class="alert alert-success alert-dismissible fade show" role="alert">
427
+ ${message}
428
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
429
+ </div>
430
+ `;
431
+ $('#alerts-container').html(alertHtml);
432
+ }
433
+
434
+ // 搜索股票并跳转到详情页
435
+ $('#search-button').click(function() {
436
+ const stockCode = $('#search-stock').val().trim();
437
+ if (stockCode) {
438
+ window.location.href = `/stock_detail/${stockCode}`;
439
+ }
440
+ });
441
+
442
+ // 回车键搜索
443
+ $('#search-stock').keypress(function(e) {
444
+ if (e.which === 13) {
445
+ $('#search-button').click();
446
+ }
447
+ });
448
+
449
+ // 格式化数字 - 增强版
450
+ function formatNumber(num, digits = 2) {
451
+ if (num === null || num === undefined) return '-';
452
+ return parseFloat(num).toFixed(digits);
453
+ }
454
+
455
+ // 格式化技术指标 - 新增函数
456
+ function formatIndicator(value, indicatorType) {
457
+ if (value === null || value === undefined) return '-';
458
+
459
+ // 根据指标类型使用不同的小数位数
460
+ if (indicatorType === 'MACD' || indicatorType === 'Signal' || indicatorType === 'Histogram') {
461
+ return parseFloat(value).toFixed(3); // MACD相关指标使用3位小数
462
+ } else if (indicatorType === 'RSI') {
463
+ return parseFloat(value).toFixed(2); // RSI使用2位小数
464
+ } else {
465
+ return parseFloat(value).toFixed(2); // 默认使用2位小数
466
+ }
467
+ }
468
+
469
+ // 格式化百分比
470
+ function formatPercent(num, digits = 2) {
471
+ if (num === null || num === undefined) return '-';
472
+ return parseFloat(num).toFixed(digits) + '%';
473
+ }
474
+
475
+ // 根据评分获取颜色类
476
+ function getScoreColorClass(score) {
477
+ if (score >= 80) return 'bg-success';
478
+ if (score >= 60) return 'bg-primary';
479
+ if (score >= 40) return 'bg-warning';
480
+ return 'bg-danger';
481
+ }
482
+
483
+ // 根据趋势获取颜色类
484
+ function getTrendColorClass(trend) {
485
+ return trend === 'UP' ? 'trend-up' : 'trend-down';
486
+ }
487
+
488
+ // 根据趋势获取图标
489
+ function getTrendIcon(trend) {
490
+ return trend === 'UP' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
491
+ }
492
+ </script>
493
+ {% block scripts %}{% endblock %}
494
+ </body>
495
+ </html>
templates/market_scan.html ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}市场扫描 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-4">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-4">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header d-flex justify-content-between">
13
+ <h5 class="mb-0">市场扫描</h5>
14
+ </div>
15
+ <div class="card-body">
16
+ <form id="scan-form" class="row g-3">
17
+ <div class="col-md-3">
18
+ <div class="input-group">
19
+ <span class="input-group-text">选择指数</span>
20
+ <select class="form-select" id="index-selector">
21
+ <option value="">-- 选择指数 --</option>
22
+ <option value="000300">沪深300</option>
23
+ <option value="000905">中证500</option>
24
+ <option value="000852">中证1000</option>
25
+ <option value="000001">上证指数</option>
26
+ </select>
27
+ </div>
28
+ </div>
29
+ <div class="col-md-3">
30
+ <div class="input-group">
31
+ <span class="input-group-text">选择行业</span>
32
+ <select class="form-select" id="industry-selector">
33
+ <option value="">-- 选择行业 --</option>
34
+ <option value="保险">保险</option>
35
+ <option value="食品饮料">食品饮料</option>
36
+ <option value="多元金融">多元金融</option>
37
+ <option value="游戏">游戏</option>
38
+ <option value="酿酒行业">酿酒行业</option>
39
+ <option value="商业百货">商业百货</option>
40
+ <option value="证券">证券</option>
41
+ <option value="船舶制造">船舶制造</option>
42
+ <option value="家用轻工">家用轻工</option>
43
+ <option value="旅游酒店">旅游酒店</option>
44
+ <option value="美容护理">美容护理</option>
45
+ <option value="医疗服务">医疗服务</option>
46
+ <option value="软件开发">软件开发</option>
47
+ <option value="化学制药">化学制药</option>
48
+ <option value="医疗器械">医疗器械</option>
49
+ <option value="家电行业">家电行业</option>
50
+ <option value="汽车服务">汽车服务</option>
51
+ <option value="造纸印刷">造纸印刷</option>
52
+ <option value="纺织服装">纺织服装</option>
53
+ <option value="光伏设备">光伏设备</option>
54
+ <option value="房地产服务">房地产服务</option>
55
+ <option value="文化传媒">文化传媒</option>
56
+ <option value="医药商业">医药商业</option>
57
+ <option value="中药">中药</option>
58
+ <option value="专业服务">专业服务</option>
59
+ <option value="生物制品">生物制品</option>
60
+ <option value="仪器仪表">仪器仪表</option>
61
+ <option value="房地产开发">房地产开发</option>
62
+ <option value="教育">教育</option>
63
+ <option value="半导体">半导体</option>
64
+ <option value="玻璃玻纤">玻璃玻纤</option>
65
+ <option value="汽车整车">汽车整车</option>
66
+ <option value="消费电子">消费电子</option>
67
+ <option value="贸易行业">贸易行业</option>
68
+ <option value="包装材料">包装材料</option>
69
+ <option value="汽车零部件">汽车零部件</option>
70
+ <option value="电子化学品">电子化学品</option>
71
+ <option value="电子元件">电子元件</option>
72
+ <option value="装修建材">装修建材</option>
73
+ <option value="交运设备">交运设备</option>
74
+ <option value="农牧饲渔">农牧饲渔</option>
75
+ <option value="塑料制品">塑料制品</option>
76
+ <option value="珠宝首饰">珠宝首饰</option>
77
+ <option value="贵金属">贵金属</option>
78
+ <option value="非金属材料">非金属材料</option>
79
+ <option value="装修装饰">装修装饰</option>
80
+ <option value="风电设备">风电设备</option>
81
+ <option value="工程咨询服务">工程咨询服务</option>
82
+ <option value="专用设备">专用设备</option>
83
+ <option value="光学光电子">光学光电子</option>
84
+ <option value="航空机场">航空机场</option>
85
+ <option value="小金属">小金属</option>
86
+ <option value="物流行业">物流行业</option>
87
+ <option value="通用设备">通用设备</option>
88
+ <option value="计算机设备">计算机设备</option>
89
+ <option value="环保行业">环保行业</option>
90
+ <option value="航运港口">航运港口</option>
91
+ <option value="通信设备">通信设备</option>
92
+ <option value="水泥建材">水泥建材</option>
93
+ <option value="电池">电池</option>
94
+ <option value="化肥行业">化肥行业</option>
95
+ <option value="互联网服务">互联网服务</option>
96
+ <option value="工程建设">工程建设</option>
97
+ <option value="橡胶制品">橡胶制品</option>
98
+ <option value="化学原料">化学原料</option>
99
+ <option value="化纤行业">化纤行业</option>
100
+ <option value="农药兽药">农药兽药</option>
101
+ <option value="化学制品">化学制品</option>
102
+ <option value="能源金属">能源金属</option>
103
+ <option value="有色金属">有色金属</option>
104
+ <option value="采掘行业">采掘行业</option>
105
+ <option value="燃气">燃气</option>
106
+ <option value="综合行业">综合行业</option>
107
+ <option value="工程机械">工程机械</option>
108
+ <option value="银行">银行</option>
109
+ <option value="铁路公路">铁路公路</option>
110
+ <option value="石油行业">石油行业</option>
111
+ <option value="公用事业">公用事业</option>
112
+ <option value="电机">电机</option>
113
+ <option value="通信服务">通信服务</option>
114
+ <option value="钢铁行业">钢铁行业</option>
115
+ <option value="电力行业">电力行业</option>
116
+ <option value="电网设备">电网设备</option>
117
+ <option value="煤炭行业">煤炭行业</option>
118
+ <option value="电源设备">电源设备</option>
119
+ <option value="航天航空">航天航空</option>
120
+ </select>
121
+ </div>
122
+ </div>
123
+ <div class="col-md-3">
124
+ <div class="input-group">
125
+ <span class="input-group-text">自定义股票</span>
126
+ <input type="text" class="form-control" id="custom-stocks" placeholder="多个股票代码用逗号分隔">
127
+ </div>
128
+ </div>
129
+ <div class="col-md-3">
130
+ <div class="input-group">
131
+ <span class="input-group-text">最低分数</span>
132
+ <input type="number" class="form-control" id="min-score" value="60" min="0" max="100">
133
+ <button type="submit" class="btn btn-primary">
134
+ <i class="fas fa-search"></i> 扫描
135
+ </button>
136
+ </div>
137
+ </div>
138
+ </form>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <div class="row mb-4">
145
+ <div class="col-12">
146
+ <div class="card">
147
+ <div class="card-header d-flex justify-content-between">
148
+ <h5 class="mb-0">扫描结果</h5>
149
+ <div>
150
+ <span class="badge bg-primary ms-2" id="result-count">0</span>
151
+ <button class="btn btn-sm btn-outline-primary ms-2" id="export-btn" style="display: none;">
152
+ <i class="fas fa-download"></i> 导出结果
153
+ </button>
154
+ </div>
155
+ </div>
156
+ <div class="card-body">
157
+ <div id="scan-loading" class="text-center py-5" style="display: none;">
158
+ <div class="spinner-border text-primary" role="status">
159
+ <span class="visually-hidden">Loading...</span>
160
+ </div>
161
+ <p class="mt-2" id="scan-message">正在扫描市场,请稍候...</p>
162
+ <div class="progress mt-3" style="height: 5px;">
163
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
164
+ </div>
165
+ <button id="cancel-scan-btn" class="btn btn-outline-secondary mt-3">
166
+ <i class="fas fa-times"></i> 取消扫描
167
+ </button>
168
+ </div>
169
+
170
+ <!-- 添加错误重试区域 -->
171
+ <div id="scan-error-retry" class="text-center mt-3" style="display: none;">
172
+ <button id="scan-retry-button" class="btn btn-primary mt-2">
173
+ <i class="fas fa-sync-alt"></i> 重试扫描
174
+ </button>
175
+ <p class="text-muted small mt-2">
176
+ 已超负载
177
+ </p>
178
+ </div>
179
+
180
+ <div id="scan-results">
181
+ <table class="table table-hover">
182
+ <thead>
183
+ <tr>
184
+ <th>代码</th>
185
+ <th>名称</th>
186
+ <th>行业</th>
187
+ <th>得分</th>
188
+ <th>价格</th>
189
+ <th>涨跌幅</th>
190
+ <th>RSI</th>
191
+ <th>MA趋势</th>
192
+ <th>成交量</th>
193
+ <th>建议</th>
194
+ <th>操作</th>
195
+ </tr>
196
+ </thead>
197
+ <tbody id="results-table">
198
+ <tr>
199
+ <td colspan="11" class="text-center">暂无数据,请开始扫描</td>
200
+ </tr>
201
+ </tbody>
202
+ </table>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ {% endblock %}
210
+
211
+ {% block scripts %}
212
+ <script>
213
+ $(document).ready(function() {
214
+ // 表单提交
215
+ $('#scan-form').submit(function(e) {
216
+ e.preventDefault();
217
+
218
+ // 获取股票列表
219
+ let stockList = [];
220
+
221
+ // 获取指数股票
222
+ const indexCode = $('#index-selector').val();
223
+ if (indexCode) {
224
+ fetchIndexStocks(indexCode);
225
+ return;
226
+ }
227
+
228
+ // 获取行业股票
229
+ const industry = $('#industry-selector').val();
230
+ if (industry) {
231
+ fetchIndustryStocks(industry);
232
+ return;
233
+ }
234
+
235
+ // 获取自定义股票
236
+ const customStocks = $('#custom-stocks').val().trim();
237
+ if (customStocks) {
238
+ stockList = customStocks.split(',').map(s => s.trim());
239
+ scanMarket(stockList);
240
+ } else {
241
+ showError('请至少选择一种方式获取股票列表');
242
+ }
243
+ });
244
+
245
+ // 指数选择变化
246
+ $('#index-selector').change(function() {
247
+ if ($(this).val()) {
248
+ $('#industry-selector').val('');
249
+ }
250
+ });
251
+
252
+ // 行业选择变化
253
+ $('#industry-selector').change(function() {
254
+ if ($(this).val()) {
255
+ $('#index-selector').val('');
256
+ }
257
+ });
258
+
259
+ // 导出结果
260
+ $('#export-btn').click(function() {
261
+ exportToCSV();
262
+ });
263
+
264
+ // 获取指数成分股
265
+ function fetchIndexStocks(indexCode) {
266
+ $('#scan-loading').show();
267
+ $('#scan-results').hide();
268
+
269
+ $.ajax({
270
+ url: `/api/index_stocks?index_code=${indexCode}`,
271
+ type: 'GET',
272
+ dataType: 'json',
273
+ success: function(response) {
274
+ const stockList = response.stock_list;
275
+ if (stockList && stockList.length > 0) {
276
+ // 保存最近的扫描列表用于重试
277
+ window.lastScanList = stockList;
278
+
279
+ scanMarket(stockList);
280
+ } else {
281
+ $('#scan-loading').hide();
282
+ $('#scan-results').show();
283
+ showError('获取指数成分股失败,或成分股列表为空');
284
+ }
285
+ },
286
+ error: function(error) {
287
+ $('#scan-loading').hide();
288
+ $('#scan-results').show();
289
+ showError('获取指数成分股失败: ' + (error.responseJSON ? error.responseJSON.error : error.statusText));
290
+ }
291
+ });
292
+ }
293
+
294
+ // 获取行业成分股
295
+ function fetchIndustryStocks(industry) {
296
+ $('#scan-loading').show();
297
+ $('#scan-results').hide();
298
+
299
+ $.ajax({
300
+ url: `/api/industry_stocks?industry=${encodeURIComponent(industry)}`,
301
+ type: 'GET',
302
+ dataType: 'json',
303
+ success: function(response) {
304
+ const stockList = response.stock_list;
305
+ if (stockList && stockList.length > 0) {
306
+ // 保存最近的扫描列表用于重试
307
+ window.lastScanList = stockList;
308
+
309
+ scanMarket(stockList);
310
+ } else {
311
+ $('#scan-loading').hide();
312
+ $('#scan-results').show();
313
+ showError('获取行业成分股失败,或成分股列表为空');
314
+ }
315
+ },
316
+ error: function(error) {
317
+ $('#scan-loading').hide();
318
+ $('#scan-results').show();
319
+ showError('获取行业成分股失败: ' + (error.responseJSON ? error.responseJSON.error : error.statusText));
320
+ }
321
+ });
322
+ }
323
+
324
+ // 扫描市场
325
+ function scanMarket(stockList) {
326
+ $('#scan-loading').show();
327
+ $('#scan-results').hide();
328
+ $('#scan-error-retry').hide();
329
+
330
+ // 添加处理时间计数器
331
+ let processingTime = 0;
332
+ let stockCount = stockList.length;
333
+
334
+ // 保存上次扫描列表
335
+ window.lastScanList = stockList;
336
+
337
+ // 更新扫描提示消息
338
+ $('#scan-message').html(`正在准备扫描${stockCount}只股票,请稍候...`);
339
+
340
+ const minScore = parseInt($('#min-score').val() || 60);
341
+
342
+ // 第一步:启动扫描任务
343
+ $.ajax({
344
+ url: '/api/start_market_scan',
345
+ type: 'POST',
346
+ contentType: 'application/json',
347
+ data: JSON.stringify({
348
+ stock_list: stockList,
349
+ min_score: minScore,
350
+ market_type: 'A'
351
+ }),
352
+ success: function(response) {
353
+ const taskId = response.task_id;
354
+
355
+ if (!taskId) {
356
+ showError('启动扫描任务失败:未获取到任务ID');
357
+ $('#scan-loading').hide();
358
+ $('#scan-results').show();
359
+ $('#scan-error-retry').show();
360
+ return;
361
+ }
362
+
363
+ // 启动轮询任务状态
364
+ pollScanStatus(taskId, processingTime);
365
+ },
366
+ error: function(xhr, status, error) {
367
+ $('#scan-loading').hide();
368
+ $('#scan-results').show();
369
+
370
+ let errorMsg = '启动扫描任务失败';
371
+ if (xhr.responseJSON && xhr.responseJSON.error) {
372
+ errorMsg += ': ' + xhr.responseJSON.error;
373
+ } else if (error) {
374
+ errorMsg += ': ' + error;
375
+ }
376
+
377
+ showError(errorMsg);
378
+ $('#scan-error-retry').show();
379
+ }
380
+ });
381
+ }
382
+
383
+ // 轮询扫描任务状态
384
+ function pollScanStatus(taskId, startTime) {
385
+ let elapsedTime = startTime || 0;
386
+ let pollInterval;
387
+
388
+ // 立即执行一次,然后设置定时器
389
+ checkStatus();
390
+
391
+ function checkStatus() {
392
+ $.ajax({
393
+ url: `/api/scan_status/${taskId}`,
394
+ type: 'GET',
395
+ success: function(response) {
396
+ // 更新计时和进度
397
+ elapsedTime++;
398
+ const progress = response.progress || 0;
399
+
400
+ // 更新进度消息
401
+ $('#scan-message').html(`正在扫描市场...<br>
402
+ 进度: ${progress}% 完成<br>
403
+ 已处理 ${Math.round(response.total * progress / 100)} / ${response.total} 只股票<br>
404
+ 耗时: ${elapsedTime}秒`);
405
+
406
+ // 检查任务状态
407
+ if (response.status === 'completed') {
408
+ // 扫描完成,停止轮询
409
+ clearInterval(pollInterval);
410
+
411
+ // 显示结果
412
+ renderResults(response.result || []);
413
+ $('#scan-loading').hide();
414
+ $('#scan-results').show();
415
+
416
+ // 如果结果为空,显示提示
417
+ if (!response.result || response.result.length === 0) {
418
+ $('#results-table').html('<tr><td colspan="11" class="text-center">未找到符合条件的股票</td></tr>');
419
+ $('#result-count').text('0');
420
+ $('#export-btn').hide();
421
+ }
422
+
423
+ } else if (response.status === 'failed') {
424
+ // 扫描失败,停止轮询
425
+ clearInterval(pollInterval);
426
+
427
+ $('#scan-loading').hide();
428
+ $('#scan-results').show();
429
+
430
+ showError('扫描任务失败: ' + (response.error || '未知错误'));
431
+ $('#scan-error-retry').show();
432
+
433
+ } else {
434
+ // 任务仍在进行中,继续轮询
435
+ // 轮询间隔根据进度动态调整
436
+ if (!pollInterval) {
437
+ pollInterval = setInterval(checkStatus, 2000);
438
+ }
439
+ }
440
+ },
441
+ error: function(xhr, status, error) {
442
+
443
+ // 尝试继续轮询
444
+ if (!pollInterval) {
445
+ pollInterval = setInterval(checkStatus, 3000);
446
+ }
447
+
448
+ // 更新进度消息
449
+ $('#scan-message').html(`正在扫描市场...<br>
450
+ 无法获取最新进度<br>
451
+ 耗时: ${elapsedTime}秒`);
452
+ }
453
+ });
454
+ }
455
+ }
456
+
457
+ // 取消扫描任务
458
+ function cancelScan(taskId) {
459
+ $.ajax({
460
+ url: `/api/cancel_scan/${taskId}`,
461
+ type: 'POST',
462
+ success: function(response) {
463
+ $('#scan-loading').hide();
464
+ $('#scan-results').show();
465
+ showError('扫描任务已取消');
466
+ $('#scan-error-retry').show();
467
+ },
468
+ error: function(xhr, status, error) {
469
+ console.error('取消扫描任务失败:', error);
470
+ }
471
+ });
472
+ }
473
+
474
+ // 渲染扫描结果
475
+ function renderResults(results) {
476
+ if (!results || results.length === 0) {
477
+ $('#results-table').html('<tr><td colspan="11" class="text-center">未找到符合条件的股票</td></tr>');
478
+ $('#result-count').text('0');
479
+ $('#export-btn').hide();
480
+ return;
481
+ }
482
+
483
+ let html = '';
484
+ results.forEach(result => {
485
+ // 获取股票评分的颜色类
486
+ const scoreClass = getScoreColorClass(result.score);
487
+
488
+ // 获取MA趋势的类和图标
489
+ const maTrendClass = getTrendColorClass(result.ma_trend);
490
+ const maTrendIcon = getTrendIcon(result.ma_trend);
491
+
492
+ // 获取价格变动的类和图标
493
+ const priceChangeClass = result.price_change >= 0 ? 'trend-up' : 'trend-down';
494
+ const priceChangeIcon = result.price_change >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
495
+
496
+ html += `
497
+ <tr>
498
+ <td>${result.stock_code}</td>
499
+ <td>${result.stock_name || '未知'}</td>
500
+ <td>${result.industry || '-'}</td>
501
+ <td><span class="badge ${scoreClass}">${result.score}</span></td>
502
+ <td>${formatNumber(result.price)}</td>
503
+ <td class="${priceChangeClass}">${priceChangeIcon} ${formatPercent(result.price_change)}</td>
504
+ <td>${formatNumber(result.rsi)}</td>
505
+ <td class="${maTrendClass}">${maTrendIcon} ${result.ma_trend}</td>
506
+ <td>${result.volume_status}</td>
507
+ <td>${result.recommendation}</td>
508
+ <td>
509
+ <a href="/stock_detail/${result.stock_code}" class="btn btn-sm btn-primary">
510
+ <i class="fas fa-chart-line"></i> 详情
511
+ </a>
512
+ </td>
513
+ </tr>
514
+ `;
515
+ });
516
+
517
+ $('#results-table').html(html);
518
+ $('#result-count').text(results.length);
519
+ $('#export-btn').show();
520
+ }
521
+
522
+ // 导出到CSV
523
+ function exportToCSV() {
524
+ // 获取表格数据
525
+ const table = document.querySelector('#scan-results table');
526
+ let csv = [];
527
+ let rows = table.querySelectorAll('tr');
528
+
529
+ for (let i = 0; i < rows.length; i++) {
530
+ let row = [], cols = rows[i].querySelectorAll('td, th');
531
+
532
+ for (let j = 0; j < cols.length - 1; j++) { // 跳过最后一列(操作列)
533
+ // 获取单元格的文本内容,去除HTML标签
534
+ let text = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/,/g, ',');
535
+ row.push(text);
536
+ }
537
+
538
+ csv.push(row.join(','));
539
+ }
540
+
541
+ // 下载CSV文件
542
+ const csvString = csv.join('\n');
543
+ const filename = '市场扫描结果_' + new Date().toISOString().slice(0, 10) + '.csv';
544
+
545
+ const blob = new Blob(['\uFEFF' + csvString], { type: 'text/csv;charset=utf-8;' });
546
+ const link = document.createElement('a');
547
+ link.href = URL.createObjectURL(blob);
548
+ link.download = filename;
549
+
550
+ link.style.display = 'none';
551
+ document.body.appendChild(link);
552
+
553
+ link.click();
554
+
555
+ document.body.removeChild(link);
556
+ }
557
+ });
558
+
559
+
560
+ // 添加到script部分
561
+ let currentTaskId = null; // 存储当前任务ID
562
+
563
+ // 取消按钮点击事件
564
+ $('#cancel-scan-btn').click(function() {
565
+ if (currentTaskId) {
566
+ cancelScan(currentTaskId);
567
+ } else {
568
+ $('#scan-loading').hide();
569
+ $('#scan-results').show();
570
+ }
571
+ });
572
+
573
+ // 修改启动成功处理
574
+ function handleStartSuccess(response) {
575
+ const taskId = response.task_id;
576
+ currentTaskId = taskId; // 保存当前任务ID
577
+
578
+ if (!taskId) {
579
+ showError('启动扫描任务失败:未获取到任务ID');
580
+ $('#scan-loading').hide();
581
+ $('#scan-results').show();
582
+ $('#scan-error-retry').show();
583
+ return;
584
+ }
585
+
586
+ // 启动轮询任务状态
587
+ pollScanStatus(taskId, 0);
588
+ }
589
+
590
+ </script>
591
+ {% endblock %}
templates/portfolio.html ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}投资组合 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-4">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-4">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header d-flex justify-content-between">
13
+ <h5 class="mb-0">我的投资组合</h5>
14
+ <button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addStockModal">
15
+ <i class="fas fa-plus"></i> 添加股票
16
+ </button>
17
+ </div>
18
+ <div class="card-body">
19
+ <div id="portfolio-empty" class="text-center py-4">
20
+ <i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
21
+ <p>您的投资组合还是空的,请添加股票</p>
22
+ <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addStockModal">
23
+ <i class="fas fa-plus"></i> 添加股票
24
+ </button>
25
+ </div>
26
+
27
+ <div id="portfolio-content" style="display: none;">
28
+ <div class="table-responsive">
29
+ <table class="table table-hover">
30
+ <thead>
31
+ <tr>
32
+ <th>代码</th>
33
+ <th>名称</th>
34
+ <th>行业</th>
35
+ <th>持仓比例</th>
36
+ <th>当前价格</th>
37
+ <th>今日涨跌</th>
38
+ <th>综合评分</th>
39
+ <th>建议</th>
40
+ <th>操作</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody id="portfolio-table">
44
+ <!-- 投资组合数据将在JS中动态填充 -->
45
+ </tbody>
46
+ </table>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div id="portfolio-analysis" class="row mb-4" style="display: none;">
55
+ <div class="col-md-6">
56
+ <div class="card h-100">
57
+ <div class="card-header">
58
+ <h5 class="mb-0">投资组合评分</h5>
59
+ </div>
60
+ <div class="card-body">
61
+ <div class="row">
62
+ <div class="col-md-4 text-center">
63
+ <div id="portfolio-score-chart"></div>
64
+ <h4 id="portfolio-score" class="mt-2">--</h4>
65
+ <p class="text-muted">综合评分</p>
66
+ </div>
67
+ <div class="col-md-8">
68
+ <h5 class="mb-3">维度评分</h5>
69
+ <div class="mb-3">
70
+ <div class="d-flex justify-content-between mb-1">
71
+ <span>技术面</span>
72
+ <span id="technical-score">--/40</span>
73
+ </div>
74
+ <div class="progress">
75
+ <div id="technical-progress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
76
+ </div>
77
+ </div>
78
+ <div class="mb-3">
79
+ <div class="d-flex justify-content-between mb-1">
80
+ <span>基本面</span>
81
+ <span id="fundamental-score">--/40</span>
82
+ </div>
83
+ <div class="progress">
84
+ <div id="fundamental-progress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
85
+ </div>
86
+ </div>
87
+ <div class="mb-3">
88
+ <div class="d-flex justify-content-between mb-1">
89
+ <span>资金面</span>
90
+ <span id="capital-flow-score">--/20</span>
91
+ </div>
92
+ <div class="progress">
93
+ <div id="capital-flow-progress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div class="col-md-6">
102
+ <div class="card h-100">
103
+ <div class="card-header">
104
+ <h5 class="mb-0">行业分布</h5>
105
+ </div>
106
+ <div class="card-body">
107
+ <div id="industry-chart"></div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <div id="portfolio-recommendations" class="row mb-4" style="display: none;">
114
+ <div class="col-12">
115
+ <div class="card">
116
+ <div class="card-header">
117
+ <h5 class="mb-0">投资建议</h5>
118
+ </div>
119
+ <div class="card-body">
120
+ <ul class="list-group" id="recommendations-list">
121
+ <!-- 投资建议将在JS中动态填充 -->
122
+ </ul>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- 添加股票模态框 -->
130
+ <div class="modal fade" id="addStockModal" tabindex="-1" aria-labelledby="addStockModalLabel" aria-hidden="true">
131
+ <div class="modal-dialog">
132
+ <div class="modal-content">
133
+ <div class="modal-header">
134
+ <h5 class="modal-title" id="addStockModalLabel">添加股票到投资组合</h5>
135
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
136
+ </div>
137
+ <div class="modal-body">
138
+ <form id="add-stock-form">
139
+ <div class="mb-3">
140
+ <label for="add-stock-code" class="form-label">股票代码</label>
141
+ <input type="text" class="form-control" id="add-stock-code" required>
142
+ </div>
143
+ <div class="mb-3">
144
+ <label for="add-stock-weight" class="form-label">持仓比例 (%)</label>
145
+ <input type="number" class="form-control" id="add-stock-weight" min="1" max="100" value="10" required>
146
+ </div>
147
+ </form>
148
+ </div>
149
+ <div class="modal-footer">
150
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
151
+ <button type="button" class="btn btn-primary" id="add-stock-btn">添加</button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ {% endblock %}
157
+
158
+ {% block scripts %}
159
+ <script>
160
+ // 投资组合数据
161
+ let portfolio = [];
162
+ let portfolioAnalysis = null;
163
+
164
+ $(document).ready(function() {
165
+ // 从本地存储加载投资组合
166
+ loadPortfolio();
167
+
168
+ // 添加股票按钮点击事件
169
+ $('#add-stock-btn').click(function() {
170
+ addStockToPortfolio();
171
+ });
172
+ });
173
+
174
+ // 从本地存储加载投资组合
175
+ function loadPortfolio() {
176
+ const savedPortfolio = localStorage.getItem('portfolio');
177
+ if (savedPortfolio) {
178
+ portfolio = JSON.parse(savedPortfolio);
179
+ renderPortfolio();
180
+ analyzePortfolio();
181
+ }
182
+ }
183
+
184
+ // 渲染投资组合
185
+ function renderPortfolio() {
186
+ if (portfolio.length === 0) {
187
+ $('#portfolio-empty').show();
188
+ $('#portfolio-content').hide();
189
+ $('#portfolio-analysis').hide();
190
+ $('#portfolio-recommendations').hide();
191
+ return;
192
+ }
193
+
194
+ $('#portfolio-empty').hide();
195
+ $('#portfolio-content').show();
196
+ $('#portfolio-analysis').show();
197
+ $('#portfolio-recommendations').show();
198
+
199
+ let html = '';
200
+ portfolio.forEach((stock, index) => {
201
+ const scoreClass = getScoreColorClass(stock.score || 0);
202
+ const priceChangeClass = (stock.price_change || 0) >= 0 ? 'trend-up' : 'trend-down';
203
+ const priceChangeIcon = (stock.price_change || 0) >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
204
+
205
+ // 显示加载状态或实际数据
206
+ const stockName = stock.loading ?
207
+ '<span class="text-muted"><i class="fas fa-spinner fa-pulse"></i> 加载中...</span>' :
208
+ (stock.stock_name || '未知');
209
+
210
+ const industryDisplay = stock.industry || '-';
211
+
212
+ html += `
213
+ <tr>
214
+ <td>${stock.stock_code}</td>
215
+ <td>${stockName}</td>
216
+ <td>${industryDisplay}</td>
217
+ <td>${stock.weight}%</td>
218
+ <td>${stock.price ? formatNumber(stock.price, 2) : '-'}</td>
219
+ <td class="${priceChangeClass}">${stock.price_change ? (priceChangeIcon + ' ' + formatPercent(stock.price_change, 2)) : '-'}</td>
220
+ <td><span class="badge ${scoreClass}">${stock.score || '-'}</span></td>
221
+ <td>${stock.recommendation || '-'}</td>
222
+ <td>
223
+ <div class="btn-group btn-group-sm" role="group">
224
+ <a href="/stock_detail/${stock.stock_code}" class="btn btn-outline-primary">
225
+ <i class="fas fa-chart-line"></i>
226
+ </a>
227
+ <button type="button" class="btn btn-outline-danger" onclick="removeStock(${index})">
228
+ <i class="fas fa-trash"></i>
229
+ </button>
230
+ </div>
231
+ </td>
232
+ </tr>
233
+ `;
234
+ });
235
+
236
+ $('#portfolio-table').html(html);
237
+ }
238
+
239
+ // 添加股票到投资组合
240
+ function addStockToPortfolio() {
241
+ const stockCode = $('#add-stock-code').val().trim();
242
+ const weight = parseInt($('#add-stock-weight').val() || 10);
243
+
244
+ if (!stockCode) {
245
+ showError('请输入股票代码');
246
+ return;
247
+ }
248
+
249
+ // 检查是否已存在
250
+ const existingIndex = portfolio.findIndex(s => s.stock_code === stockCode);
251
+ if (existingIndex >= 0) {
252
+ showError('此股票已在投资组合中');
253
+ return;
254
+ }
255
+
256
+ // 添加到投资组合
257
+ portfolio.push({
258
+ stock_code: stockCode,
259
+ weight: weight,
260
+ stock_name: '加载中...',
261
+ industry: '-',
262
+ price: null,
263
+ price_change: null,
264
+ score: null,
265
+ recommendation: null,
266
+ loading: true // 添加加载状态标志
267
+ });
268
+
269
+ // 保存到本地存储
270
+ savePortfolio();
271
+
272
+ // 关闭模态框
273
+ $('#addStockModal').modal('hide');
274
+
275
+ // 重置表单
276
+ $('#add-stock-form')[0].reset();
277
+
278
+ // 获取股票数据
279
+ fetchStockData(stockCode);
280
+ }
281
+
282
+ // 添加重试加载功能
283
+ function retryFetchStockData(stockCode) {
284
+ showInfo(`正在重新获取 ${stockCode} 的数据...`);
285
+ fetchStockData(stockCode);
286
+ }
287
+
288
+ // 在渲染函数中添加重试按钮
289
+ html += `
290
+ <tr>
291
+ <td>${stock.stock_code}</td>
292
+ <td>${stockName} ${stock.stock_name === '获取失败' ?
293
+ `<button class="btn btn-sm btn-link p-0 ml-2" onclick="retryFetchStockData('${stock.stock_code}')">
294
+ <i class="fas fa-sync-alt"></i> 重试
295
+ </button>` : ''}
296
+ </td>
297
+ ...
298
+ `;
299
+
300
+ // 获取股票数据
301
+ function fetchStockData(stockCode) {
302
+ const index = portfolio.findIndex(s => s.stock_code === stockCode);
303
+ if (index < 0) return;
304
+
305
+ // 显示加载状态
306
+ portfolio[index].loading = true;
307
+ savePortfolio();
308
+ renderPortfolio();
309
+
310
+ $.ajax({
311
+ url: '/analyze',
312
+ type: 'POST',
313
+ contentType: 'application/json',
314
+ data: JSON.stringify({
315
+ stock_codes: [stockCode],
316
+ market_type: 'A'
317
+ }),
318
+ success: function(response) {
319
+
320
+ if (response.results && response.results.length > 0) {
321
+ const result = response.results[0];
322
+
323
+
324
+ // 确保使用null检查来处理缺失值
325
+ portfolio[index].stock_name = result.stock_name || '未知';
326
+ portfolio[index].industry = result.industry || '未知';
327
+ portfolio[index].price = result.price || 0;
328
+ portfolio[index].price_change = result.price_change || 0;
329
+ portfolio[index].score = result.score || 0;
330
+ portfolio[index].recommendation = result.recommendation || '-';
331
+ portfolio[index].loading = false; // 清除加载状态
332
+
333
+ // 保存更新后的投资组合
334
+ savePortfolio();
335
+
336
+ // 分析投资组合
337
+ analyzePortfolio();
338
+
339
+ showSuccess(`已添加 ${result.stock_name || stockCode} 到投资组合`);
340
+ } else {
341
+ portfolio[index].stock_name = '数据获取失败';
342
+ portfolio[index].loading = false;
343
+ savePortfolio();
344
+ renderPortfolio();
345
+ showError(`获取股票 ${stockCode} 数据失败`);
346
+ }
347
+ },
348
+ error: function(error) {
349
+ portfolio[index].stock_name = '获取失败';
350
+ portfolio[index].loading = false;
351
+ savePortfolio();
352
+ renderPortfolio();
353
+ showError(`获取股票 ${stockCode} 数据失败`);
354
+ }
355
+ });
356
+ }
357
+
358
+ // 从投资组合中移除股票
359
+ function removeStock(index) {
360
+ if (confirm('确定要从投资组合中移除此股票吗?')) {
361
+ portfolio.splice(index, 1);
362
+ savePortfolio();
363
+ renderPortfolio();
364
+ analyzePortfolio();
365
+ }
366
+ }
367
+
368
+ // 保存投资组合到本地存储
369
+ function savePortfolio() {
370
+ localStorage.setItem('portfolio', JSON.stringify(portfolio));
371
+ renderPortfolio();
372
+ }
373
+
374
+
375
+ // 分析投资组合
376
+ function analyzePortfolio() {
377
+ if (portfolio.length === 0) return;
378
+
379
+ // 计算投资组合评分
380
+ let totalScore = 0;
381
+ let totalWeight = 0;
382
+ let industriesMap = {};
383
+
384
+ portfolio.forEach(stock => {
385
+ if (stock.score) {
386
+ totalScore += stock.score * stock.weight;
387
+ totalWeight += stock.weight;
388
+
389
+ // 统计行业分布
390
+ const industry = stock.industry || '其他';
391
+ if (industriesMap[industry]) {
392
+ industriesMap[industry] += stock.weight;
393
+ } else {
394
+ industriesMap[industry] = stock.weight;
395
+ }
396
+ }
397
+ });
398
+
399
+ // 确保总权重不为零
400
+ if (totalWeight > 0) {
401
+ const portfolioScore = Math.round(totalScore / totalWeight);
402
+
403
+ // 更新评分显示
404
+ $('#portfolio-score').text(portfolioScore);
405
+
406
+ // 简化的维度评分计算
407
+ const technicalScore = Math.round(portfolioScore * 0.4);
408
+ const fundamentalScore = Math.round(portfolioScore * 0.4);
409
+ const capitalFlowScore = Math.round(portfolioScore * 0.2);
410
+
411
+ $('#technical-score').text(technicalScore + '/40');
412
+ $('#fundamental-score').text(fundamentalScore + '/40');
413
+ $('#capital-flow-score').text(capitalFlowScore + '/20');
414
+
415
+ $('#technical-progress').css('width', (technicalScore / 40 * 100) + '%');
416
+ $('#fundamental-progress').css('width', (fundamentalScore / 40 * 100) + '%');
417
+ $('#capital-flow-progress').css('width', (capitalFlowScore / 20 * 100) + '%');
418
+
419
+ // 更新投资组合评分图表
420
+ renderPortfolioScoreChart(portfolioScore);
421
+
422
+ // 更新行业分布图表
423
+ renderIndustryChart(industriesMap);
424
+
425
+ // 生成投资建议
426
+ generateRecommendations(portfolioScore);
427
+ }
428
+ }
429
+
430
+ // 渲染投资组合评分图表
431
+ function renderPortfolioScoreChart(score) {
432
+ const options = {
433
+ series: [score],
434
+ chart: {
435
+ height: 150,
436
+ type: 'radialBar',
437
+ },
438
+ plotOptions: {
439
+ radialBar: {
440
+ hollow: {
441
+ size: '70%',
442
+ },
443
+ dataLabels: {
444
+ show: false
445
+ }
446
+ }
447
+ },
448
+ colors: [getScoreColor(score)],
449
+ stroke: {
450
+ lineCap: 'round'
451
+ }
452
+ };
453
+
454
+ // 清除旧图表
455
+ $('#portfolio-score-chart').empty();
456
+
457
+ const chart = new ApexCharts(document.querySelector("#portfolio-score-chart"), options);
458
+ chart.render();
459
+ }
460
+
461
+ // 渲染行业分布图表
462
+ function renderIndustryChart(industriesMap) {
463
+ // 转换数据格式为图表所需
464
+ const seriesData = [];
465
+ const labels = [];
466
+
467
+ for (const industry in industriesMap) {
468
+ if (industriesMap.hasOwnProperty(industry)) {
469
+ seriesData.push(industriesMap[industry]);
470
+ labels.push(industry);
471
+ }
472
+ }
473
+
474
+ const options = {
475
+ series: seriesData,
476
+ chart: {
477
+ type: 'pie',
478
+ height: 300
479
+ },
480
+ labels: labels,
481
+ responsive: [{
482
+ breakpoint: 480,
483
+ options: {
484
+ chart: {
485
+ height: 200
486
+ },
487
+ legend: {
488
+ position: 'bottom'
489
+ }
490
+ }
491
+ }],
492
+ tooltip: {
493
+ y: {
494
+ formatter: function(value) {
495
+ return value + '%';
496
+ }
497
+ }
498
+ }
499
+ };
500
+
501
+ // 清除旧图表
502
+ $('#industry-chart').empty();
503
+
504
+ const chart = new ApexCharts(document.querySelector("#industry-chart"), options);
505
+ chart.render();
506
+ }
507
+
508
+ // 生成投资建议
509
+ function generateRecommendations(portfolioScore) {
510
+ let recommendations = [];
511
+
512
+ // 根据总分生成基本建议
513
+ if (portfolioScore >= 80) {
514
+ recommendations.push({
515
+ text: '您的投资组合整体评级优秀,当前市场环境下建议保持较高仓位',
516
+ type: 'success'
517
+ });
518
+ } else if (portfolioScore >= 60) {
519
+ recommendations.push({
520
+ text: '您的投资组合整体评级良好,可以考虑适度增加仓位',
521
+ type: 'primary'
522
+ });
523
+ } else if (portfolioScore >= 40) {
524
+ recommendations.push({
525
+ text: '您的投资组合整体评级一般,建议持币观望,等待更好的入场时机',
526
+ type: 'warning'
527
+ });
528
+ } else {
529
+ recommendations.push({
530
+ text: '您的投资组合整体评级较弱,建议减仓规避风险',
531
+ type: 'danger'
532
+ });
533
+ }
534
+
535
+ // 检查行业集中度
536
+ const industries = {};
537
+ let totalWeight = 0;
538
+
539
+ portfolio.forEach(stock => {
540
+ const industry = stock.industry || '其他';
541
+ if (industries[industry]) {
542
+ industries[industry] += stock.weight;
543
+ } else {
544
+ industries[industry] = stock.weight;
545
+ }
546
+ totalWeight += stock.weight;
547
+ });
548
+
549
+ // 计算行业集中度
550
+ let maxIndustryWeight = 0;
551
+ let maxIndustry = '';
552
+
553
+ for (const industry in industries) {
554
+ if (industries[industry] > maxIndustryWeight) {
555
+ maxIndustryWeight = industries[industry];
556
+ maxIndustry = industry;
557
+ }
558
+ }
559
+
560
+ const industryConcentration = maxIndustryWeight / totalWeight;
561
+
562
+ if (industryConcentration > 0.5) {
563
+ recommendations.push({
564
+ text: `行业集中度较高,${maxIndustry}行业占比${Math.round(industryConcentration * 100)}%,建议适当分散投资降低非系统性风险`,
565
+ type: 'warning'
566
+ });
567
+ }
568
+
569
+ // 检查需要调整的个股
570
+ const weakStocks = portfolio.filter(stock => stock.score && stock.score < 40);
571
+ if (weakStocks.length > 0) {
572
+ const stockNames = weakStocks.map(s => `${s.stock_name}(${s.stock_code})`).join('、');
573
+ recommendations.push({
574
+ text: `以下个股评分较低,建议考虑调整:${stockNames}`,
575
+ type: 'danger'
576
+ });
577
+ }
578
+
579
+ const strongStocks = portfolio.filter(stock => stock.score && stock.score > 70);
580
+ if (strongStocks.length > 0 && portfolioScore < 60) {
581
+ const stockNames = strongStocks.map(s => `${s.stock_name}(${s.stock_code})`).join('、');
582
+ recommendations.push({
583
+ text: `以下个股表现强势,可考虑增加配置比例:${stockNames}`,
584
+ type: 'success'
585
+ });
586
+ }
587
+
588
+ // 渲染建议
589
+ let html = '';
590
+ recommendations.forEach(rec => {
591
+ html += `<li class="list-group-item list-group-item-${rec.type}">${rec.text}</li>`;
592
+ });
593
+
594
+ $('#recommendations-list').html(html);
595
+ }
596
+
597
+ // 获取评分颜色
598
+ function getScoreColor(score) {
599
+ if (score >= 80) return '#28a745'; // 绿色
600
+ if (score >= 60) return '#007bff'; // 蓝色
601
+ if (score >= 40) return '#ffc107'; // 黄色
602
+ return '#dc3545'; // 红色
603
+ }
604
+ </script>
605
+ {% endblock %}
templates/qa.html ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}智能问答 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-3">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header py-2">
13
+ <h5 class="mb-0">智能问答</h5>
14
+ </div>
15
+ <div class="card-body py-2">
16
+ <form id="qa-form" class="row g-2">
17
+ <div class="col-md-4">
18
+ <div class="input-group input-group-sm">
19
+ <span class="input-group-text">股票代码</span>
20
+ <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519" required>
21
+ </div>
22
+ </div>
23
+ <div class="col-md-3">
24
+ <div class="input-group input-group-sm">
25
+ <span class="input-group-text">市场</span>
26
+ <select class="form-select" id="market-type">
27
+ <option value="A" selected>A股</option>
28
+ <option value="HK">港股</option>
29
+ <option value="US">美股</option>
30
+ </select>
31
+ </div>
32
+ </div>
33
+ <div class="col-md-3">
34
+ <button type="submit" class="btn btn-primary btn-sm w-100">
35
+ <i class="fas fa-info-circle"></i> 选择股票
36
+ </button>
37
+ </div>
38
+ </form>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="row" id="chat-container" style="display: none;">
45
+ <div class="col-md-3">
46
+ <div class="card mb-3">
47
+ <div class="card-header py-2">
48
+ <h5 class="mb-0" id="stock-info-header">股票信息</h5>
49
+ </div>
50
+ <div class="card-body">
51
+ <h4 id="selected-stock-name" class="mb-1">--</h4>
52
+ <p id="selected-stock-code" class="text-muted mb-3">--</p>
53
+ <p class="mb-1"><span class="text-muted">行业:</span> <span id="selected-stock-industry">--</span></p>
54
+ <p class="mb-1"><span class="text-muted">现价:</span> <span id="selected-stock-price">--</span></p>
55
+ <p class="mb-1"><span class="text-muted">涨跌幅:</span> <span id="selected-stock-change">--</span></p>
56
+ <hr class="my-3">
57
+ <h6>常见问题</h6>
58
+ <div class="list-group list-group-flush">
59
+ <button class="list-group-item list-group-item-action common-question" data-question="这只股票的主要支撑位是多少?">主要支撑位分析</button>
60
+ <button class="list-group-item list-group-item-action common-question" data-question="该股票近期的技术面走势如何?">技术面走势分析</button>
61
+ <button class="list-group-item list-group-item-action common-question" data-question="这只股票的基本面情况如何?">基本面情况分析</button>
62
+ <button class="list-group-item list-group-item-action common-question" data-question="该股票主力资金最近的流入情况?">主力资金流向</button>
63
+ <button class="list-group-item list-group-item-action common-question" data-question="这只股票近期有哪些重要事件?">近期重要事件</button>
64
+ <button class="list-group-item list-group-item-action common-question" data-question="您对这只股票有什么投资建议?">综合投资建议</button>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <div class="col-md-9">
70
+ <div class="card mb-3">
71
+ <div class="card-header py-2">
72
+ <h5 class="mb-0">与AI助手对话</h5>
73
+ </div>
74
+ <div class="card-body p-0">
75
+ <div id="chat-messages" class="p-3" style="height: 400px; overflow-y: auto;">
76
+ <div class="chat-message system-message">
77
+ <div class="message-content">
78
+ <p>您好!我是股票分析AI助手,请输入您想了解的关于当前股票的问题。</p>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ <div class="p-3 border-top">
83
+ <form id="question-form" class="d-flex">
84
+ <input type="text" id="question-input" class="form-control me-2" placeholder="输入您的问题..." required>
85
+ <button type="submit" class="btn btn-primary">
86
+ <i class="fas fa-paper-plane"></i>
87
+ </button>
88
+ </form>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <div id="loading-panel" class="text-center py-5" style="display: none;">
96
+ <div class="spinner-border text-primary" role="status">
97
+ <span class="visually-hidden">Loading...</span>
98
+ </div>
99
+ <p class="mt-3 mb-0">正在获取股票数据...</p>
100
+ </div>
101
+ </div>
102
+ {% endblock %}
103
+
104
+ {% block head %}
105
+ <style>
106
+ .chat-message {
107
+ margin-bottom: 15px;
108
+ display: flex;
109
+ flex-direction: column;
110
+ }
111
+
112
+ .user-message {
113
+ align-items: flex-end;
114
+ }
115
+
116
+ .system-message {
117
+ align-items: flex-start;
118
+ }
119
+
120
+ .message-content {
121
+ max-width: 80%;
122
+ padding: 10px 15px;
123
+ border-radius: 15px;
124
+ position: relative;
125
+ }
126
+
127
+ .user-message .message-content {
128
+ background-color: #007bff;
129
+ color: white;
130
+ border-bottom-right-radius: 0;
131
+ }
132
+
133
+ .system-message .message-content {
134
+ background-color: #f1f1f1;
135
+ color: #333;
136
+ border-bottom-left-radius: 0;
137
+ }
138
+
139
+ .message-content p {
140
+ margin-bottom: 0.5rem;
141
+ }
142
+
143
+ .message-content p:last-child {
144
+ margin-bottom: 0;
145
+ }
146
+
147
+ .message-time {
148
+ font-size: 0.75rem;
149
+ color: #aaa;
150
+ margin-top: 4px;
151
+ }
152
+
153
+ .common-question {
154
+ padding: 0.5rem 0.75rem;
155
+ font-size: 0.875rem;
156
+ }
157
+
158
+ .keyword {
159
+ color: #2c7be5;
160
+ font-weight: 600;
161
+ }
162
+
163
+ .term {
164
+ color: #d6336c;
165
+ font-weight: 500;
166
+ padding: 0 2px;
167
+ }
168
+
169
+ .price {
170
+ color: #00a47c;
171
+ font-family: 'Roboto Mono', monospace;
172
+ background: #f3faf8;
173
+ padding: 2px 4px;
174
+ border-radius: 3px;
175
+ }
176
+
177
+ .trend-up {
178
+ color: #28a745;
179
+ }
180
+
181
+ .trend-down {
182
+ color: #dc3545;
183
+ }
184
+ </style>
185
+ {% endblock %}
186
+
187
+ {% block scripts %}
188
+ <script>
189
+ let selectedStock = {
190
+ code: '',
191
+ name: '',
192
+ market_type: 'A'
193
+ };
194
+
195
+ $(document).ready(function() {
196
+ // 选择股票表单提交
197
+ $('#qa-form').submit(function(e) {
198
+ e.preventDefault();
199
+ const stockCode = $('#stock-code').val().trim();
200
+ const marketType = $('#market-type').val();
201
+
202
+ if (!stockCode) {
203
+ showError('请输入股票代码!');
204
+ return;
205
+ }
206
+
207
+ selectStock(stockCode, marketType);
208
+ });
209
+
210
+ // 问题表单提交
211
+ $('#question-form').submit(function(e) {
212
+ e.preventDefault();
213
+ const question = $('#question-input').val().trim();
214
+
215
+ if (!question) {
216
+ return;
217
+ }
218
+
219
+ if (!selectedStock.code) {
220
+ showError('请先选择一只股票');
221
+ return;
222
+ }
223
+
224
+ addUserMessage(question);
225
+ $('#question-input').val('');
226
+ askQuestion(question);
227
+ });
228
+
229
+ // 常见问题点击
230
+ $('.common-question').click(function() {
231
+ const question = $(this).data('question');
232
+
233
+ if (!selectedStock.code) {
234
+ showError('请先选择一只股票');
235
+ return;
236
+ }
237
+
238
+ $('#question-input').val(question);
239
+ $('#question-form').submit();
240
+ });
241
+ });
242
+
243
+ function selectStock(stockCode, marketType) {
244
+ $('#loading-panel').show();
245
+ $('#chat-container').hide();
246
+
247
+ // 重置对话区域
248
+ $('#chat-messages').html(`
249
+ <div class="chat-message system-message">
250
+ <div class="message-content">
251
+ <p>您好!我是股票分析AI助手,请输入您想了解的关于当前股票的问题。</p>
252
+ </div>
253
+ </div>
254
+ `);
255
+
256
+ // 获取股票基本信息
257
+ $.ajax({
258
+ url: '/analyze',
259
+ type: 'POST',
260
+ contentType: 'application/json',
261
+ data: JSON.stringify({
262
+ stock_codes: [stockCode],
263
+ market_type: marketType
264
+ }),
265
+ success: function(response) {
266
+ $('#loading-panel').hide();
267
+
268
+ if (response.results && response.results.length > 0) {
269
+ const stockInfo = response.results[0];
270
+
271
+ // 保存选中的股票信息
272
+ selectedStock = {
273
+ code: stockCode,
274
+ name: stockInfo.stock_name || '未���',
275
+ market_type: marketType,
276
+ industry: stockInfo.industry || '未知',
277
+ price: stockInfo.price || 0,
278
+ price_change: stockInfo.price_change || 0
279
+ };
280
+
281
+ // 更新股票信息区域
282
+ updateStockInfo();
283
+
284
+ // 显示聊天界面
285
+ $('#chat-container').show();
286
+
287
+ // 欢迎消息
288
+ addSystemMessage(`我已加载 ${selectedStock.name}(${selectedStock.code}) 的数据,您可以问我关于这只股票的问题。`);
289
+ } else {
290
+ showError('未找到股票信息,请检查股票代码是否正确');
291
+ }
292
+ },
293
+ error: function(xhr, status, error) {
294
+ $('#loading-panel').hide();
295
+ let errorMsg = '获取股票信息失败';
296
+ if (xhr.responseJSON && xhr.responseJSON.error) {
297
+ errorMsg += ': ' + xhr.responseJSON.error;
298
+ } else if (error) {
299
+ errorMsg += ': ' + error;
300
+ }
301
+ showError(errorMsg);
302
+ }
303
+ });
304
+ }
305
+
306
+ function updateStockInfo() {
307
+ // 更新股票信息区域
308
+ $('#stock-info-header').text(selectedStock.name);
309
+ $('#selected-stock-name').text(selectedStock.name);
310
+ $('#selected-stock-code').text(selectedStock.code);
311
+ $('#selected-stock-industry').text(selectedStock.industry);
312
+ $('#selected-stock-price').text('¥' + formatNumber(selectedStock.price, 2));
313
+
314
+ const priceChangeClass = selectedStock.price_change >= 0 ? 'trend-up' : 'trend-down';
315
+ const priceChangeIcon = selectedStock.price_change >= 0 ? '<i class="fas fa-caret-up"></i> ' : '<i class="fas fa-caret-down"></i> ';
316
+ $('#selected-stock-change').html(`<span class="${priceChangeClass}">${priceChangeIcon}${formatPercent(selectedStock.price_change, 2)}</span>`);
317
+ }
318
+
319
+ function askQuestion(question) {
320
+ // 显示思考中消息
321
+ const thinkingMessageId = 'thinking-' + Date.now();
322
+ addSystemMessage('<i class="fas fa-spinner fa-pulse"></i> 正在思考...', thinkingMessageId);
323
+
324
+ // 发送问题到API
325
+ $.ajax({
326
+ url: '/api/qa',
327
+ type: 'POST',
328
+ contentType: 'application/json',
329
+ data: JSON.stringify({
330
+ stock_code: selectedStock.code,
331
+ question: question,
332
+ market_type: selectedStock.market_type
333
+ }),
334
+ success: function(response) {
335
+ // 移除思考中消息
336
+ $(`#${thinkingMessageId}`).remove();
337
+
338
+ // 添加回答
339
+ addSystemMessage(formatAnswer(response.answer));
340
+
341
+ // 滚动到底部
342
+ scrollToBottom();
343
+ },
344
+ error: function(xhr, status, error) {
345
+ // 移除思考中消息
346
+ $(`#${thinkingMessageId}`).remove();
347
+
348
+ // 添加错误消息
349
+ let errorMsg = '无法回答您的问题';
350
+ if (xhr.responseJSON && xhr.responseJSON.error) {
351
+ errorMsg += ': ' + xhr.responseJSON.error;
352
+ } else if (error) {
353
+ errorMsg += ': ' + error;
354
+ }
355
+
356
+ addSystemMessage(`<span class="text-danger">${errorMsg}</span>`);
357
+
358
+ // 滚动到底部
359
+ scrollToBottom();
360
+ }
361
+ });
362
+ }
363
+
364
+ function addUserMessage(message) {
365
+ const time = new Date().toLocaleTimeString();
366
+
367
+ const messageHtml = `
368
+ <div class="chat-message user-message">
369
+ <div class="message-content">
370
+ <p>${message}</p>
371
+ </div>
372
+ <div class="message-time">${time}</div>
373
+ </div>
374
+ `;
375
+
376
+ $('#chat-messages').append(messageHtml);
377
+ scrollToBottom();
378
+ }
379
+
380
+ function addSystemMessage(message, id = null) {
381
+ const time = new Date().toLocaleTimeString();
382
+ const idAttribute = id ? `id="${id}"` : '';
383
+
384
+ const messageHtml = `
385
+ <div class="chat-message system-message" ${idAttribute}>
386
+ <div class="message-content">
387
+ <p>${message}</p>
388
+ </div>
389
+ <div class="message-time">${time}</div>
390
+ </div>
391
+ `;
392
+
393
+ $('#chat-messages').append(messageHtml);
394
+ scrollToBottom();
395
+ }
396
+
397
+ function scrollToBottom() {
398
+ const chatContainer = document.getElementById('chat-messages');
399
+ chatContainer.scrollTop = chatContainer.scrollHeight;
400
+ }
401
+
402
+ function formatAnswer(text) {
403
+ if (!text) return '';
404
+
405
+ // First, make the text safe for HTML
406
+ const safeText = text
407
+ .replace(/&/g, '&amp;')
408
+ .replace(/</g, '&lt;')
409
+ .replace(/>/g, '&gt;');
410
+
411
+ // Replace basic Markdown elements
412
+ let formatted = safeText
413
+ // Bold text with ** or __
414
+ .replace(/\*\*(.*?)\*\*/g, '<strong class="keyword">$1</strong>')
415
+ .replace(/__(.*?)__/g, '<strong>$1</strong>')
416
+
417
+ // Italic text with * or _
418
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
419
+ .replace(/_(.*?)_/g, '<em>$1</em>')
420
+
421
+ // Headers - only h4, h5, h6 for chat
422
+ .replace(/^#### (.*?)$/gm, '<h6>$1</h6>')
423
+ .replace(/^### (.*?)$/gm, '<h6>$1</h6>')
424
+ .replace(/^## (.*?)$/gm, '<h6>$1</h6>')
425
+ .replace(/^# (.*?)$/gm, '<h6>$1</h6>')
426
+
427
+ // Apply special styling to financial terms
428
+ .replace(/支撑位/g, '<span class="keyword">支撑位</span>')
429
+ .replace(/压力位/g, '<span class="keyword">压力位</span>')
430
+ .replace(/趋势/g, '<span class="keyword">趋势</span>')
431
+ .replace(/均线/g, '<span class="keyword">均线</span>')
432
+ .replace(/MACD/g, '<span class="term">MACD</span>')
433
+ .replace(/RSI/g, '<span class="term">RSI</span>')
434
+ .replace(/KDJ/g, '<span class="term">KDJ</span>')
435
+
436
+ // Highlight price patterns and movements
437
+ .replace(/([上涨升])/g, '<span class="trend-up">$1</span>')
438
+ .replace(/([下跌降])/g, '<span class="trend-down">$1</span>')
439
+ .replace(/(买入|做多|多头|突破)/g, '<span class="trend-up">$1</span>')
440
+ .replace(/(卖出|做空|空头|跌破)/g, '<span class="trend-down">$1</span>')
441
+
442
+ // Highlight price values (matches patterns like 31.25, 120.50)
443
+ .replace(/(\d+\.\d{2})/g, '<span class="price">$1</span>')
444
+
445
+ // Convert line breaks to paragraph tags
446
+ .replace(/\n\n+/g, '</p><p class="mb-2">')
447
+ .replace(/\n/g, '<br>');
448
+
449
+ // Wrap in paragraph tags for consistent styling
450
+ return '<p class="mb-2">' + formatted + '</p>';
451
+ }
452
+ </script>
453
+ {% endblock %}
templates/risk_monitor.html ADDED
@@ -0,0 +1,1041 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}风险监控 - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-3">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header py-2">
13
+ <h5 class="mb-0">风险监控</h5>
14
+ </div>
15
+ <div class="card-body py-2">
16
+ <ul class="nav nav-tabs" id="risk-tabs" role="tablist">
17
+ <li class="nav-item" role="presentation">
18
+ <button class="nav-link active" id="stock-risk-tab" data-bs-toggle="tab" data-bs-target="#stock-risk" type="button" role="tab" aria-controls="stock-risk" aria-selected="true">个股风险</button>
19
+ </li>
20
+ <li class="nav-item" role="presentation">
21
+ <button class="nav-link" id="portfolio-risk-tab" data-bs-toggle="tab" data-bs-target="#portfolio-risk" type="button" role="tab" aria-controls="portfolio-risk" aria-selected="false">组合风险</button>
22
+ </li>
23
+ </ul>
24
+ <div class="tab-content mt-3" id="risk-tabs-content">
25
+ <div class="tab-pane fade show active" id="stock-risk" role="tabpanel" aria-labelledby="stock-risk-tab">
26
+ <form id="stock-risk-form" class="row g-2">
27
+ <div class="col-md-4">
28
+ <div class="input-group input-group-sm">
29
+ <span class="input-group-text">股票代码</span>
30
+ <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519" required>
31
+ </div>
32
+ </div>
33
+ <div class="col-md-3">
34
+ <div class="input-group input-group-sm">
35
+ <span class="input-group-text">市场</span>
36
+ <select class="form-select" id="market-type">
37
+ <option value="A" selected>A股</option>
38
+ <option value="HK">港股</option>
39
+ <option value="US">美股</option>
40
+ </select>
41
+ </div>
42
+ </div>
43
+ <div class="col-md-3">
44
+ <button type="submit" class="btn btn-primary btn-sm w-100">
45
+ <i class="fas fa-search"></i> 分析风险
46
+ </button>
47
+ </div>
48
+ </form>
49
+ </div>
50
+ <div class="tab-pane fade" id="portfolio-risk" role="tabpanel" aria-labelledby="portfolio-risk-tab">
51
+ <div class="alert alert-info">
52
+ <i class="fas fa-info-circle"></i> 分析投资组合的整体风险,识别高风险股票和风险集中度。
53
+ </div>
54
+ <button id="analyze-portfolio-btn" class="btn btn-primary btn-sm">
55
+ <i class="fas fa-briefcase"></i> 分析我的投资组合
56
+ </button>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div id="loading-panel" class="text-center py-5" style="display: none;">
65
+ <div class="spinner-border text-primary" role="status">
66
+ <span class="visually-hidden">Loading...</span>
67
+ </div>
68
+ <p class="mt-3 mb-0">正在分析风险,请稍候...</p>
69
+ </div>
70
+
71
+ <!-- 个股风险分析结果 -->
72
+ <div id="stock-risk-result" style="display: none;">
73
+ <div class="row g-3 mb-3">
74
+ <div class="col-md-6">
75
+ <div class="card h-100">
76
+ <div class="card-header py-2 d-flex justify-content-between align-items-center">
77
+ <h5 class="mb-0">风险概览</h5>
78
+ <span id="risk-level-badge" class="badge"></span>
79
+ </div>
80
+ <div class="card-body">
81
+ <div class="row mb-3">
82
+ <div class="col-md-7">
83
+ <h3 id="stock-name" class="mb-0 fs-4"></h3>
84
+ <p id="stock-info" class="text-muted mb-0 small"></p>
85
+ </div>
86
+ <div class="col-md-5 text-end">
87
+ <div id="risk-gauge-chart" style="height: 120px;"></div>
88
+ </div>
89
+ </div>
90
+ <div class="row">
91
+ <div class="col-12">
92
+ <h6>风险预警</h6>
93
+ <div id="risk-alerts" class="mt-2">
94
+ <!-- 风险预警内容将在JS中动态填充 -->
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div class="col-md-6">
102
+ <div class="card h-100">
103
+ <div class="card-header py-2">
104
+ <h5 class="mb-0">风险构成</h5>
105
+ </div>
106
+ <div class="card-body">
107
+ <div id="risk-radar-chart" style="height: 220px;"></div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="row g-3 mb-3">
114
+ <div class="col-md-6">
115
+ <div class="card h-100">
116
+ <div class="card-header py-2">
117
+ <h5 class="mb-0">波动率风险</h5>
118
+ </div>
119
+ <div class="card-body">
120
+ <div class="row">
121
+ <div class="col-md-6">
122
+ <h6>波动率指标</h6>
123
+ <p><span class="text-muted">当前波动率:</span> <span id="current-volatility" class="fw-bold"></span></p>
124
+ <p><span class="text-muted">变化率:</span> <span id="volatility-change" class="fw-bold"></span></p>
125
+ <p><span class="text-muted">风险等级:</span> <span id="volatility-risk-level" class="fw-bold"></span></p>
126
+ <p class="small text-muted" id="volatility-comment"></p>
127
+ </div>
128
+ <div class="col-md-6">
129
+ <div id="volatility-chart" style="height: 150px;"></div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <div class="col-md-6">
136
+ <div class="card h-100">
137
+ <div class="card-header py-2">
138
+ <h5 class="mb-0">趋势风险</h5>
139
+ </div>
140
+ <div class="card-body">
141
+ <div class="row">
142
+ <div class="col-md-6">
143
+ <h6>趋势指标</h6>
144
+ <p><span class="text-muted">当前趋势:</span> <span id="current-trend" class="fw-bold"></span></p>
145
+ <p><span class="text-muted">均线关系:</span> <span id="ma-relationship" class="fw-bold"></span></p>
146
+ <p><span class="text-muted">风险等级:</span> <span id="trend-risk-level" class="fw-bold"></span></p>
147
+ <p class="small text-muted" id="trend-comment"></p>
148
+ </div>
149
+ <div class="col-md-6">
150
+ <div id="trend-chart" style="height: 150px;"></div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="row g-3 mb-3">
159
+ <div class="col-md-6">
160
+ <div class="card h-100">
161
+ <div class="card-header py-2">
162
+ <h5 class="mb-0">反转风险</h5>
163
+ </div>
164
+ <div class="card-body">
165
+ <div class="row">
166
+ <div class="col-md-6">
167
+ <h6>反转信号</h6>
168
+ <p><span class="text-muted">反转信号数:</span> <span id="reversal-signals" class="fw-bold"></span></p>
169
+ <p><span class="text-muted">可能方向:</span> <span id="reversal-direction" class="fw-bold"></span></p>
170
+ <p><span class="text-muted">风险等级:</span> <span id="reversal-risk-level" class="fw-bold"></span></p>
171
+ <p class="small text-muted" id="reversal-comment"></p>
172
+ </div>
173
+ <div class="col-md-6">
174
+ <div id="reversal-chart" style="height: 150px;"></div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ <div class="col-md-6">
181
+ <div class="card h-100">
182
+ <div class="card-header py-2">
183
+ <h5 class="mb-0">成交量风险</h5>
184
+ </div>
185
+ <div class="card-body">
186
+ <div class="row">
187
+ <div class="col-md-6">
188
+ <h6>成交量指标</h6>
189
+ <p><span class="text-muted">成交量比率:</span> <span id="volume-ratio" class="fw-bold"></span></p>
190
+ <p><span class="text-muted">成交量模式:</span> <span id="volume-pattern" class="fw-bold"></span></p>
191
+ <p><span class="text-muted">风险等级:</span> <span id="volume-risk-level" class="fw-bold"></span></p>
192
+ <p class="small text-muted" id="volume-comment"></p>
193
+ </div>
194
+ <div class="col-md-6">
195
+ <div id="volume-chart" style="height: 150px;"></div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- 投资组合风险分析结果 -->
205
+ <div id="portfolio-risk-result" style="display: none;">
206
+ <div class="row g-3 mb-3">
207
+ <div class="col-md-6">
208
+ <div class="card h-100">
209
+ <div class="card-header py-2 d-flex justify-content-between align-items-center">
210
+ <h5 class="mb-0">组合风险概览</h5>
211
+ <span id="portfolio-risk-level-badge" class="badge"></span>
212
+ </div>
213
+ <div class="card-body">
214
+ <div class="row mb-3">
215
+ <div class="col-md-7">
216
+ <h3 class="mb-0 fs-4">我的投资组合</h3>
217
+ <p class="text-muted mb-0 small">包含 <span id="portfolio-stock-count">0</span> 只股票</p>
218
+ </div>
219
+ <div class="col-md-5 text-end">
220
+ <div id="portfolio-risk-gauge-chart" style="height: 120px;"></div>
221
+ </div>
222
+ </div>
223
+ <div class="row">
224
+ <div class="col-12">
225
+ <h6>风险集中度</h6>
226
+ <p><span class="text-muted">行业集中度:</span> <span id="industry-concentration" class="fw-bold"></span></p>
227
+ <p><span class="text-muted">高风险股票占比:</span> <span id="high-risk-concentration" class="fw-bold"></span></p>
228
+ <div id="portfolio-risk-alerts" class="mt-2">
229
+ <!-- 风险预警内容将在JS中动态填充 -->
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ <div class="col-md-6">
237
+ <div class="card h-100">
238
+ <div class="card-header py-2">
239
+ <h5 class="mb-0">行业分布</h5>
240
+ </div>
241
+ <div class="card-body">
242
+ <div id="industry-distribution-chart" style="height: 220px;"></div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+
248
+ <div class="row g-3 mb-3">
249
+ <div class="col-12">
250
+ <div class="card">
251
+ <div class="card-header py-2">
252
+ <h5 class="mb-0">高风险股票</h5>
253
+ </div>
254
+ <div class="card-body p-0">
255
+ <div class="table-responsive">
256
+ <table class="table table-sm table-striped table-hover mb-0">
257
+ <thead>
258
+ <tr>
259
+ <th>代码</th>
260
+ <th>名称</th>
261
+ <th>风险评分</th>
262
+ <th>风险等级</th>
263
+ <th>权重</th>
264
+ <th>主要风险</th>
265
+ <th>操作</th>
266
+ </tr>
267
+ </thead>
268
+ <tbody id="high-risk-stocks-table">
269
+ <!-- 高风险股票列表将在JS中动态填充 -->
270
+ </tbody>
271
+ </table>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ <div class="row g-3 mb-3">
279
+ <div class="col-12">
280
+ <div class="card">
281
+ <div class="card-header py-2">
282
+ <h5 class="mb-0">风险预警列表</h5>
283
+ </div>
284
+ <div class="card-body p-0">
285
+ <div class="table-responsive">
286
+ <table class="table table-sm table-striped table-hover mb-0">
287
+ <thead>
288
+ <tr>
289
+ <th>代码</th>
290
+ <th>名称</th>
291
+ <th>预警类型</th>
292
+ <th>风险等级</th>
293
+ <th>预警信息</th>
294
+ </tr>
295
+ </thead>
296
+ <tbody id="risk-alerts-table">
297
+ <!-- 风险预警列表将在JS中动态填充 -->
298
+ </tbody>
299
+ </table>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ {% endblock %}
308
+
309
+ {% block scripts %}
310
+ <script>
311
+ $(document).ready(function() {
312
+ // 个股风险分析表单提交
313
+ $('#stock-risk-form').submit(function(e) {
314
+ e.preventDefault();
315
+ const stockCode = $('#stock-code').val().trim();
316
+ const marketType = $('#market-type').val();
317
+
318
+ if (!stockCode) {
319
+ showError('请输入股票代码!');
320
+ return;
321
+ }
322
+
323
+ analyzeStockRisk(stockCode, marketType);
324
+ });
325
+
326
+ // 分析投资组合风险按钮点击
327
+ $('#analyze-portfolio-btn').click(function() {
328
+ analyzePortfolioRisk();
329
+ });
330
+ });
331
+
332
+ function analyzeStockRisk(stockCode, marketType) {
333
+ $('#loading-panel').show();
334
+ $('#stock-risk-result').hide();
335
+ $('#portfolio-risk-result').hide();
336
+
337
+ $.ajax({
338
+ url: '/api/risk_analysis',
339
+ type: 'POST',
340
+ contentType: 'application/json',
341
+ data: JSON.stringify({
342
+ stock_code: stockCode,
343
+ market_type: marketType
344
+ }),
345
+ success: function(response) {
346
+ $('#loading-panel').hide();
347
+ renderStockRiskAnalysis(response, stockCode);
348
+ $('#stock-risk-result').show();
349
+ },
350
+ error: function(xhr, status, error) {
351
+ $('#loading-panel').hide();
352
+ let errorMsg = '获取风险分析数据失败';
353
+ if (xhr.responseJSON && xhr.responseJSON.error) {
354
+ errorMsg += ': ' + xhr.responseJSON.error;
355
+ } else if (error) {
356
+ errorMsg += ': ' + error;
357
+ }
358
+ showError(errorMsg);
359
+ }
360
+ });
361
+ }
362
+
363
+ function analyzePortfolioRisk() {
364
+ // 尝试从本地存储获取投资组合数据
365
+ const savedPortfolio = localStorage.getItem('portfolio');
366
+ if (!savedPortfolio) {
367
+ showError('您的投资组合为空,请先添加股票到投资组合');
368
+ return;
369
+ }
370
+
371
+ const portfolio = JSON.parse(savedPortfolio);
372
+ if (portfolio.length === 0) {
373
+ showError('您的投资组合为空,请先添加股票到投资组合');
374
+ return;
375
+ }
376
+
377
+ $('#loading-panel').show();
378
+ $('#stock-risk-result').hide();
379
+ $('#portfolio-risk-result').hide();
380
+
381
+ $.ajax({
382
+ url: '/api/portfolio_risk',
383
+ type: 'POST',
384
+ contentType: 'application/json',
385
+ data: JSON.stringify({
386
+ portfolio: portfolio
387
+ }),
388
+ success: function(response) {
389
+ $('#loading-panel').hide();
390
+ renderPortfolioRiskAnalysis(response, portfolio);
391
+ $('#portfolio-risk-result').show();
392
+ },
393
+ error: function(xhr, status, error) {
394
+ $('#loading-panel').hide();
395
+ let errorMsg = '获取投资组合风险分析数据失败';
396
+ if (xhr.responseJSON && xhr.responseJSON.error) {
397
+ errorMsg += ': ' + xhr.responseJSON.error;
398
+ } else if (error) {
399
+ errorMsg += ': ' + error;
400
+ }
401
+ showError(errorMsg);
402
+ }
403
+ });
404
+ }
405
+
406
+ function renderStockRiskAnalysis(data, stockCode) {
407
+ // 设置基本信息
408
+ $('#stock-name').text(data.stock_name || stockCode);
409
+ $('#stock-info').text(data.industry || '未知行业');
410
+
411
+ // 设置风险等级
412
+ const riskScore = data.total_risk_score || 0;
413
+ const riskLevel = data.risk_level || '未知';
414
+ const riskLevelBadgeClass = getRiskLevelBadgeClass(riskLevel);
415
+ $('#risk-level-badge').text(riskLevel).removeClass().addClass(`badge ${riskLevelBadgeClass}`);
416
+
417
+ // 设置风险预警
418
+ renderRiskAlerts(data.alerts || []);
419
+
420
+ // 渲染风险仪表图
421
+ renderRiskGaugeChart(riskScore);
422
+
423
+ // 渲染风险雷达图
424
+ renderRiskRadarChart(data);
425
+
426
+ // 设置波动率风险
427
+ const volatilityRisk = data.volatility_risk || {};
428
+ $('#current-volatility').text(formatPercent(volatilityRisk.value || 0, 2));
429
+
430
+ const volatilityChange = volatilityRisk.change || 0;
431
+ const volatilityChangeClass = volatilityChange >= 0 ? 'trend-up' : 'trend-down';
432
+ $('#volatility-change').text(formatPercent(volatilityChange, 2)).addClass(volatilityChangeClass);
433
+
434
+ $('#volatility-risk-level').text(volatilityRisk.risk_level || '未知');
435
+ $('#volatility-comment').text('波动率反映价格波动的剧烈程度,高波动率意味着高风险');
436
+
437
+ // 设置趋势风险
438
+ const trendRisk = data.trend_risk || {};
439
+ $('#current-trend').text(trendRisk.trend || '未知');
440
+ $('#ma-relationship').text(trendRisk.ma_relationship || '未知');
441
+ $('#trend-risk-level').text(trendRisk.risk_level || '未知');
442
+ $('#trend-comment').text('下降趋势中的股票有更高的风险,特别是在空头排列时');
443
+
444
+ // 设置反转风险
445
+ const reversalRisk = data.reversal_risk || {};
446
+ $('#reversal-signals').text(reversalRisk.reversal_signals || 0);
447
+ $('#reversal-direction').text(reversalRisk.direction || '未知');
448
+ $('#reversal-risk-level').text(reversalRisk.risk_level || '未知');
449
+ $('#reversal-comment').text('多个技术指标同时出现反转信号,意味着趋势可能即将改变');
450
+
451
+ // 设置成交量风险
452
+ const volumeRisk = data.volume_risk || {};
453
+ $('#volume-ratio').text(formatNumber(volumeRisk.volume_ratio || 0, 2));
454
+ $('#volume-pattern').text(volumeRisk.pattern || '未知');
455
+ $('#volume-risk-level').text(volumeRisk.risk_level || '未知');
456
+ $('#volume-comment').text('成交量异常变化常常预示价格波动,尤其是量价背离时');
457
+
458
+ // 渲染各个风险维度的图表
459
+ renderVolatilityChart([5.2, 3.8, 4.5, 7.2, 6.3]);
460
+ renderTrendChart([110, 108, 106, 102, 98]);
461
+ renderReversalChart([55, 60, 65, 72, 68]);
462
+ renderVolumeChart([1.2, 0.8, 1.5, 2.8, 2.1]);
463
+ }
464
+
465
+ function renderPortfolioRiskAnalysis(data, portfolio) {
466
+ // 设置基本信息
467
+ $('#portfolio-stock-count').text(portfolio.length);
468
+
469
+ // 设置风险等级
470
+ const riskScore = data.portfolio_risk_score || 0;
471
+ const riskLevel = data.risk_level || '未知';
472
+ const riskLevelBadgeClass = getRiskLevelBadgeClass(riskLevel);
473
+ $('#portfolio-risk-level-badge').text(riskLevel).removeClass().addClass(`badge ${riskLevelBadgeClass}`);
474
+
475
+ // 设置风险集中度
476
+ const riskConcentration = data.risk_concentration || {};
477
+ $('#industry-concentration').text(`${riskConcentration.max_industry || '未知'} (${formatPercent(riskConcentration.max_industry_weight || 0, 1)})`);
478
+ $('#high-risk-concentration').text(formatPercent(riskConcentration.high_risk_weight || 0, 1));
479
+
480
+ // 设置风险预警
481
+ renderPortfolioRiskAlerts(data.alerts || []);
482
+
483
+ // 渲染投资组合风险仪表图
484
+ renderPortfolioRiskGaugeChart(riskScore);
485
+
486
+ // 渲染行业分布图
487
+ renderIndustryDistributionChart(portfolio);
488
+
489
+ // 渲染高风险股票列表
490
+ renderHighRiskStocksTable(data.high_risk_stocks || []);
491
+
492
+ // 渲染风险预警列表
493
+ renderRiskAlertsTable(data.alerts || []);
494
+ }
495
+
496
+ function renderRiskAlerts(alerts) {
497
+ let html = '';
498
+
499
+ if (alerts.length === 0) {
500
+ html = '<div class="alert alert-success">未发现显著风险因素</div>';
501
+ } else {
502
+ alerts.forEach(alert => {
503
+ const alertClass = getRiskAlertClass(alert.level);
504
+ html += `
505
+ <div class="alert ${alertClass} mb-2">
506
+ <strong>${alert.type}风险:</strong> ${alert.message}
507
+ </div>
508
+ `;
509
+ });
510
+ }
511
+
512
+ $('#risk-alerts').html(html);
513
+ }
514
+
515
+ function renderPortfolioRiskAlerts(alerts) {
516
+ let html = '';
517
+
518
+ if (alerts.length === 0) {
519
+ html = '<div class="alert alert-success">投资组合风险均衡,未发现显著风险集中</div>';
520
+ } else {
521
+ let alertCount = 0;
522
+ alerts.forEach(alert => {
523
+ if (alertCount < 3) { // 只显示前3条
524
+ const alertClass = getRiskAlertClass(alert.level);
525
+ html += `
526
+ <div class="alert ${alertClass} mb-2">
527
+ <strong>${alert.stock_code}:</strong> ${alert.message}
528
+ </div>
529
+ `;
530
+ alertCount++;
531
+ }
532
+ });
533
+
534
+ if (alerts.length > 3) {
535
+ html += `<p class="text-muted small">还有 ${alerts.length - 3} 条风险预警,请查看下方详情表格</p>`;
536
+ }
537
+ }
538
+
539
+ $('#portfolio-risk-alerts').html(html);
540
+ }
541
+
542
+ function renderRiskGaugeChart(score) {
543
+ const options = {
544
+ series: [score],
545
+ chart: {
546
+ height: 120,
547
+ type: 'radialBar',
548
+ },
549
+ plotOptions: {
550
+ radialBar: {
551
+ hollow: {
552
+ size: '70%',
553
+ },
554
+ dataLabels: {
555
+ show: true,
556
+ name: {
557
+ show: false
558
+ },
559
+ value: {
560
+ fontSize: '16px',
561
+ fontWeight: 'bold',
562
+ offsetY: 5
563
+ }
564
+ },
565
+ track: {
566
+ background: '#f2f2f2'
567
+ }
568
+ }
569
+ },
570
+ fill: {
571
+ colors: [getRiskColor(score)]
572
+ },
573
+ labels: ['风险分数']
574
+ };
575
+
576
+ const chart = new ApexCharts(document.querySelector("#risk-gauge-chart"), options);
577
+ chart.render();
578
+ }
579
+
580
+ function renderPortfolioRiskGaugeChart(score) {
581
+ const options = {
582
+ series: [score],
583
+ chart: {
584
+ height: 120,
585
+ type: 'radialBar',
586
+ },
587
+ plotOptions: {
588
+ radialBar: {
589
+ hollow: {
590
+ size: '70%',
591
+ },
592
+ dataLabels: {
593
+ show: true,
594
+ name: {
595
+ show: false
596
+ },
597
+ value: {
598
+ fontSize: '16px',
599
+ fontWeight: 'bold',
600
+ offsetY: 5
601
+ }
602
+ },
603
+ track: {
604
+ background: '#f2f2f2'
605
+ }
606
+ }
607
+ },
608
+ fill: {
609
+ colors: [getRiskColor(score)]
610
+ },
611
+ labels: ['风险分数']
612
+ };
613
+
614
+ const chart = new ApexCharts(document.querySelector("#portfolio-risk-gauge-chart"), options);
615
+ chart.render();
616
+ }
617
+
618
+ function renderRiskRadarChart(data) {
619
+ const volatilityRisk = data.volatility_risk?.score || 0;
620
+ const trendRisk = data.trend_risk?.score || 0;
621
+ const reversalRisk = data.reversal_risk?.score || 0;
622
+ const volumeRisk = data.volume_risk?.score || 0;
623
+
624
+ const options = {
625
+ series: [{
626
+ name: '风险评分',
627
+ data: [volatilityRisk, trendRisk, reversalRisk, volumeRisk]
628
+ }],
629
+ chart: {
630
+ height: 220,
631
+ type: 'radar',
632
+ toolbar: {
633
+ show: false
634
+ }
635
+ },
636
+ xaxis: {
637
+ categories: ['波动率风险', '趋势风险', '反转风险', '成交量风险']
638
+ },
639
+ fill: {
640
+ opacity: 0.7,
641
+ colors: ['#dc3545']
642
+ },
643
+ markers: {
644
+ size: 4
645
+ },
646
+ title: {
647
+ text: '风险雷达图',
648
+ align: 'center',
649
+ style: {
650
+ fontSize: '14px'
651
+ }
652
+ }
653
+ };
654
+
655
+ const chart = new ApexCharts(document.querySelector("#risk-radar-chart"), options);
656
+ chart.render();
657
+ }
658
+
659
+ function renderIndustryDistributionChart(portfolio) {
660
+ // 根据投资组合计算行业分布
661
+ const industries = {};
662
+ let totalWeight = 0;
663
+
664
+ portfolio.forEach(stock => {
665
+ const industry = stock.industry || '未知';
666
+ const weight = stock.weight || 1;
667
+
668
+ if (industries[industry]) {
669
+ industries[industry] += weight;
670
+ } else {
671
+ industries[industry] = weight;
672
+ }
673
+
674
+ totalWeight += weight;
675
+ });
676
+
677
+ const series = [];
678
+ const labels = [];
679
+
680
+ for (const industry in industries) {
681
+ if (industries.hasOwnProperty(industry)) {
682
+ series.push(industries[industry]);
683
+ labels.push(industry);
684
+ }
685
+ }
686
+
687
+ const options = {
688
+ series: series,
689
+ chart: {
690
+ height: 220,
691
+ type: 'pie',
692
+ },
693
+ labels: labels,
694
+ colors: ['#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b', '#858796', '#5a5c69', '#6f42c1'],
695
+ legend: {
696
+ position: 'bottom'
697
+ },
698
+ tooltip: {
699
+ y: {
700
+ formatter: function(value) {
701
+ return value + ' (' + ((value / totalWeight) * 100).toFixed(1) + '%)';
702
+ }
703
+ }
704
+ }
705
+ };
706
+
707
+ const chart = new ApexCharts(document.querySelector("#industry-distribution-chart"), options);
708
+ chart.render();
709
+ }
710
+
711
+ function renderVolatilityChart(data) {
712
+ const options = {
713
+ series: [{
714
+ name: '波动率(%)',
715
+ data: data
716
+ }],
717
+ chart: {
718
+ height: 150,
719
+ type: 'line',
720
+ toolbar: {
721
+ show: false
722
+ }
723
+ },
724
+ stroke: {
725
+ curve: 'smooth',
726
+ width: 3
727
+ },
728
+ xaxis: {
729
+ labels: {
730
+ show: false
731
+ }
732
+ },
733
+ yaxis: {
734
+ labels: {
735
+ formatter: function(val) {
736
+ return val.toFixed(1) + '%';
737
+ },
738
+ style: {
739
+ fontSize: '10px'
740
+ }
741
+ }
742
+ },
743
+ colors: ['#dc3545'],
744
+ tooltip: {
745
+ y: {
746
+ formatter: function(value) {
747
+ return value.toFixed(2) + '%';
748
+ }
749
+ }
750
+ },
751
+ markers: {
752
+ size: 3
753
+ }
754
+ };
755
+
756
+ const chart = new ApexCharts(document.querySelector("#volatility-chart"), options);
757
+ chart.render();
758
+ }
759
+
760
+ function renderTrendChart(data) {
761
+ const options = {
762
+ series: [{
763
+ name: '价格',
764
+ data: data
765
+ }, {
766
+ name: 'MA20',
767
+ data: [109, 107, 105, 103, 101]
768
+ }],
769
+ chart: {
770
+ height: 150,
771
+ type: 'line',
772
+ toolbar: {
773
+ show: false
774
+ }
775
+ },
776
+ stroke: {
777
+ curve: 'straight',
778
+ width: [3, 2]
779
+ },
780
+ xaxis: {
781
+ labels: {
782
+ show: false
783
+ }
784
+ },
785
+ yaxis: {
786
+ labels: {
787
+ formatter: function(val) {
788
+ return val.toFixed(0);
789
+ },
790
+ style: {
791
+ fontSize: '10px'
792
+ }
793
+ }
794
+ },
795
+ colors: ['#dc3545', '#007bff'],
796
+ tooltip: {
797
+ y: {
798
+ formatter: function(value) {
799
+ return value.toFixed(2);
800
+ }
801
+ }
802
+ },
803
+ markers: {
804
+ size: 3
805
+ },
806
+ legend: {
807
+ show: false
808
+ }
809
+ };
810
+
811
+ const chart = new ApexCharts(document.querySelector("#trend-chart"), options);
812
+ chart.render();
813
+ }
814
+
815
+ function renderReversalChart(data) {
816
+ const options = {
817
+ series: [{
818
+ name: 'RSI',
819
+ data: data
820
+ }],
821
+ chart: {
822
+ height: 150,
823
+ type: 'line',
824
+ toolbar: {
825
+ show: false
826
+ }
827
+ },
828
+ stroke: {
829
+ curve: 'smooth',
830
+ width: 3
831
+ },
832
+ xaxis: {
833
+ labels: {
834
+ show: false
835
+ }
836
+ },
837
+ yaxis: {
838
+ min: 0,
839
+ max: 100,
840
+ labels: {
841
+ formatter: function(val) {
842
+ return val.toFixed(0);
843
+ },
844
+ style: {
845
+ fontSize: '10px'
846
+ }
847
+ }
848
+ },
849
+ colors: ['#ffc107'],
850
+ tooltip: {
851
+ y: {
852
+ formatter: function(value) {
853
+ return value.toFixed(2);
854
+ }
855
+ }
856
+ },
857
+ markers: {
858
+ size: 3
859
+ },
860
+ annotations: {
861
+ yaxis: [{
862
+ y: 70,
863
+ borderColor: '#dc3545',
864
+ label: {
865
+ text: '超买',
866
+ style: {
867
+ color: '#fff',
868
+ background: '#dc3545'
869
+ }
870
+ }
871
+ }, {
872
+ y: 30,
873
+ borderColor: '#28a745',
874
+ label: {
875
+ text: '超卖',
876
+ style: {
877
+ color: '#fff',
878
+ background: '#28a745'
879
+ }
880
+ }
881
+ }]
882
+ }
883
+ };
884
+
885
+ const chart = new ApexCharts(document.querySelector("#reversal-chart"), options);
886
+ chart.render();
887
+ }
888
+
889
+ function renderVolumeChart(data) {
890
+ const options = {
891
+ series: [{
892
+ name: '成交量比率',
893
+ data: data
894
+ }],
895
+ chart: {
896
+ height: 150,
897
+ type: 'bar',
898
+ toolbar: {
899
+ show: false
900
+ }
901
+ },
902
+ xaxis: {
903
+ labels: {
904
+ show: false
905
+ }
906
+ },
907
+ yaxis: {
908
+ labels: {
909
+ formatter: function(val) {
910
+ return val.toFixed(1) + 'x';
911
+ },
912
+ style: {
913
+ fontSize: '10px'
914
+ }
915
+ }
916
+ },
917
+ colors: ['#4e73df'],
918
+ tooltip: {
919
+ y: {
920
+ formatter: function(value) {
921
+ return value.toFixed(2) + 'x';
922
+ }
923
+ }
924
+ },
925
+ plotOptions: {
926
+ bar: {
927
+ columnWidth: '50%'
928
+ }
929
+ },
930
+ dataLabels: {
931
+ enabled: false
932
+ }
933
+ };
934
+
935
+ const chart = new ApexCharts(document.querySelector("#volume-chart"), options);
936
+ chart.render();
937
+ }
938
+
939
+ function renderHighRiskStocksTable(highRiskStocks) {
940
+ let html = '';
941
+
942
+ if (highRiskStocks.length === 0) {
943
+ html = '<tr><td colspan="7" class="text-center">未发现高风险股票</td></tr>';
944
+ } else {
945
+ highRiskStocks.forEach(stock => {
946
+ const riskScoreClass = getRiskScoreClass(stock.risk_score);
947
+ const riskLevelBadgeClass = getRiskLevelBadgeClass(stock.risk_level);
948
+
949
+ html += `
950
+ <tr>
951
+ <td>${stock.stock_code}</td>
952
+ <td>${stock.stock_name || '未知'}</td>
953
+ <td><span class="badge ${riskScoreClass}">${stock.risk_score}</span></td>
954
+ <td><span class="badge ${riskLevelBadgeClass}">${stock.risk_level}</span></td>
955
+ <td>${formatPercent(stock.weight || 0, 1)}</td>
956
+ <td>${stock.main_risk || '未知'}</td>
957
+ <td>
958
+ <a href="/stock_detail/${stock.stock_code}" class="btn btn-sm btn-outline-info me-1">
959
+ <i class="fas fa-chart-line"></i>
960
+ </a>
961
+ <a href="/risk_monitor?stock=${stock.stock_code}" class="btn btn-sm btn-outline-danger">
962
+ <i class="fas fa-exclamation-triangle"></i>
963
+ </a>
964
+ </td>
965
+ </tr>
966
+ `;
967
+ });
968
+ }
969
+
970
+ $('#high-risk-stocks-table').html(html);
971
+ }
972
+
973
+ function renderRiskAlertsTable(alerts) {
974
+ let html = '';
975
+
976
+ if (alerts.length === 0) {
977
+ html = '<tr><td colspan="5" class="text-center">暂无风险预警</td></tr>';
978
+ } else {
979
+ alerts.forEach(alert => {
980
+ const riskLevelBadgeClass = getRiskLevelBadgeClass(alert.level);
981
+
982
+ html += `
983
+ <tr>
984
+ <td>${alert.stock_code}</td>
985
+ <td>${alert.stock_name || '未知'}</td>
986
+ <td>${alert.type}</td>
987
+ <td><span class="badge ${riskLevelBadgeClass}">${alert.level}</span></td>
988
+ <td>${alert.message}</td>
989
+ </tr>
990
+ `;
991
+ });
992
+ }
993
+
994
+ $('#risk-alerts-table').html(html);
995
+ }
996
+
997
+ function getRiskLevelBadgeClass(level) {
998
+ switch (level) {
999
+ case '极高':
1000
+ return 'bg-danger';
1001
+ case '高':
1002
+ return 'bg-warning text-dark';
1003
+ case '中等':
1004
+ return 'bg-info text-dark';
1005
+ case '低':
1006
+ return 'bg-success';
1007
+ case '极低':
1008
+ return 'bg-secondary';
1009
+ default:
1010
+ return 'bg-secondary';
1011
+ }
1012
+ }
1013
+
1014
+ function getRiskAlertClass(level) {
1015
+ switch (level) {
1016
+ case '高':
1017
+ return 'alert-danger';
1018
+ case '中':
1019
+ return 'alert-warning';
1020
+ case '低':
1021
+ return 'alert-info';
1022
+ default:
1023
+ return 'alert-secondary';
1024
+ }
1025
+ }
1026
+
1027
+ function getRiskScoreClass(score) {
1028
+ if (score >= 80) return 'bg-danger';
1029
+ if (score >= 60) return 'bg-warning text-dark';
1030
+ if (score >= 40) return 'bg-info text-dark';
1031
+ return 'bg-success';
1032
+ }
1033
+
1034
+ function getRiskColor(score) {
1035
+ if (score >= 80) return '#dc3545'; // 红色
1036
+ if (score >= 60) return '#ffc107'; // 黄色
1037
+ if (score >= 40) return '#17a2b8'; // 蓝色
1038
+ return '#28a745'; // 绿色
1039
+ }
1040
+ </script>
1041
+ {% endblock %}
templates/scenario_predict.html ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}情景预测 - {{ stock_code }} - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-3">
7
+ <div id="alerts-container"></div>
8
+
9
+ <div class="row mb-3">
10
+ <div class="col-12">
11
+ <div class="card">
12
+ <div class="card-header py-2">
13
+ <h5 class="mb-0">多情景预测</h5>
14
+ </div>
15
+ <div class="card-body py-2">
16
+ <form id="scenario-form" class="row g-2">
17
+ <div class="col-md-3">
18
+ <div class="input-group input-group-sm">
19
+ <span class="input-group-text">股票代码</span>
20
+ <input type="text" class="form-control" id="stock-code" placeholder="例如: 600519" required>
21
+ </div>
22
+ </div>
23
+ <div class="col-md-3">
24
+ <div class="input-group input-group-sm">
25
+ <span class="input-group-text">市场</span>
26
+ <select class="form-select" id="market-type">
27
+ <option value="A" selected>A股</option>
28
+ <option value="HK">港股</option>
29
+ <option value="US">美股</option>
30
+ </select>
31
+ </div>
32
+ </div>
33
+ <div class="col-md-3">
34
+ <div class="input-group input-group-sm">
35
+ <span class="input-group-text">预测天数</span>
36
+ <select class="form-select" id="days">
37
+ <option value="30">30天</option>
38
+ <option value="60" selected>60天</option>
39
+ <option value="90">90天</option>
40
+ <option value="180">180天</option>
41
+ </select>
42
+ </div>
43
+ </div>
44
+ <div class="col-md-3">
45
+ <button type="submit" class="btn btn-primary btn-sm w-100">
46
+ <i class="fas fa-chart-line"></i> 预测
47
+ </button>
48
+ </div>
49
+ </form>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div id="loading-panel" class="text-center py-5" style="display: none;">
56
+ <div class="spinner-border text-primary" role="status">
57
+ <span class="visually-hidden">Loading...</span>
58
+ </div>
59
+ <p class="mt-3 mb-0">正在生成预测结果...</p>
60
+ <p class="text-muted small mt-2">
61
+ <i class="fas fa-info-circle"></i>
62
+ AI分析需要一些时间,请耐心等待
63
+ </p>
64
+ </div>
65
+
66
+ <div id="scenario-result" style="display: none;">
67
+ <div class="row g-3 mb-3">
68
+ <div class="col-12">
69
+ <div class="card">
70
+ <div class="card-header py-2">
71
+ <h5 class="mb-0">价格预测图</h5>
72
+ </div>
73
+ <div class="card-body p-0">
74
+ <div id="price-prediction-chart" style="height: 400px;"></div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="row g-3 mb-3">
81
+ <div class="col-md-4">
82
+ <div class="card h-100">
83
+ <div class="card-header py-2 bg-success text-white">
84
+ <h5 class="mb-0">乐观情景</h5>
85
+ </div>
86
+ <div class="card-body">
87
+ <div class="d-flex justify-content-between mb-3">
88
+ <div>
89
+ <h6>目标价</h6>
90
+ <h3 id="optimistic-price" class="text-success">--</h3>
91
+ </div>
92
+ <div>
93
+ <h6>预期涨幅</h6>
94
+ <h3 id="optimistic-change" class="text-success">--</h3>
95
+ </div>
96
+ </div>
97
+ <p id="optimistic-analysis" class="small"></p>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div class="col-md-4">
102
+ <div class="card h-100">
103
+ <div class="card-header py-2 bg-primary text-white">
104
+ <h5 class="mb-0">中性情景</h5>
105
+ </div>
106
+ <div class="card-body">
107
+ <div class="d-flex justify-content-between mb-3">
108
+ <div>
109
+ <h6>目标价</h6>
110
+ <h3 id="neutral-price" class="text-primary">--</h3>
111
+ </div>
112
+ <div>
113
+ <h6>预期涨幅</h6>
114
+ <h3 id="neutral-change" class="text-primary">--</h3>
115
+ </div>
116
+ </div>
117
+ <p id="neutral-analysis" class="small"></p>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <div class="col-md-4">
122
+ <div class="card h-100">
123
+ <div class="card-header py-2 bg-danger text-white">
124
+ <h5 class="mb-0">悲观情景</h5>
125
+ </div>
126
+ <div class="card-body">
127
+ <div class="d-flex justify-content-between mb-3">
128
+ <div>
129
+ <h6>目标价</h6>
130
+ <h3 id="pessimistic-price" class="text-danger">--</h3>
131
+ </div>
132
+ <div>
133
+ <h6>预期涨幅</h6>
134
+ <h3 id="pessimistic-change" class="text-danger">--</h3>
135
+ </div>
136
+ </div>
137
+ <p id="pessimistic-analysis" class="small"></p>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <div class="row g-3 mb-3">
144
+ <div class="col-12">
145
+ <div class="card">
146
+ <div class="card-header py-2">
147
+ <h5 class="mb-0">风险与机会</h5>
148
+ </div>
149
+ <div class="card-body">
150
+ <div class="row">
151
+ <div class="col-md-6">
152
+ <h6 class="text-danger"><i class="fas fa-exclamation-triangle"></i> 风险因素</h6>
153
+ <ul id="risk-factors" class="small"></ul>
154
+ </div>
155
+ <div class="col-md-6">
156
+ <h6 class="text-success"><i class="fas fa-lightbulb"></i> 有利因素</h6>
157
+ <ul id="opportunity-factors" class="small"></ul>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ {% endblock %}
167
+
168
+ {% block scripts %}
169
+ <script>
170
+ $(document).ready(function() {
171
+ $('#scenario-form').submit(function(e) {
172
+ e.preventDefault();
173
+ const stockCode = $('#stock-code').val().trim();
174
+ const marketType = $('#market-type').val();
175
+ const days = $('#days').val();
176
+
177
+ if (!stockCode) {
178
+ showError('请输入股票代码!');
179
+ return;
180
+ }
181
+
182
+ fetchScenarioPrediction(stockCode, marketType, days);
183
+ });
184
+ });
185
+
186
+ function fetchScenarioPrediction(stockCode, marketType, days) {
187
+ $('#loading-panel').show();
188
+ $('#scenario-result').hide();
189
+
190
+ $.ajax({
191
+ url: '/api/scenario_predict',
192
+ type: 'POST',
193
+ contentType: 'application/json',
194
+ data: JSON.stringify({
195
+ stock_code: stockCode,
196
+ market_type: marketType,
197
+ days: parseInt(days)
198
+ }),
199
+ success: function(response) {
200
+ $('#loading-panel').hide();
201
+ renderScenarioPrediction(response, stockCode);
202
+ $('#scenario-result').show();
203
+ },
204
+ error: function(xhr, status, error) {
205
+ $('#loading-panel').hide();
206
+ let errorMsg = '获取情景预测失败';
207
+ if (xhr.responseJSON && xhr.responseJSON.error) {
208
+ errorMsg += ': ' + xhr.responseJSON.error;
209
+ } else if (error) {
210
+ errorMsg += ': ' + error;
211
+ }
212
+ showError(errorMsg);
213
+ }
214
+ });
215
+ }
216
+
217
+ function renderScenarioPrediction(data, stockCode) {
218
+ // 设置乐观情景数据
219
+ $('#optimistic-price').text('¥' + formatNumber(data.optimistic.target_price, 2));
220
+ $('#optimistic-change').text(formatPercent(data.optimistic.change_percent, 2));
221
+ $('#optimistic-analysis').text(data.optimistic_analysis || '暂无分析');
222
+
223
+ // 设置中性情景数据
224
+ $('#neutral-price').text('¥' + formatNumber(data.neutral.target_price, 2));
225
+ $('#neutral-change').text(formatPercent(data.neutral.change_percent, 2));
226
+ $('#neutral-analysis').text(data.neutral_analysis || '暂无分析');
227
+
228
+ // 设置悲观情景数据
229
+ $('#pessimistic-price').text('¥' + formatNumber(data.pessimistic.target_price, 2));
230
+ $('#pessimistic-change').text(formatPercent(data.pessimistic.change_percent, 2));
231
+ $('#pessimistic-analysis').text(data.pessimistic_analysis || '暂无分析');
232
+
233
+ // 设置风险与机会因素
234
+ setDefaultRiskOpportunityFactors();
235
+
236
+ // 渲染价格预测图表
237
+ renderPricePredictionChart(data);
238
+ }
239
+
240
+ function setDefaultRiskOpportunityFactors() {
241
+ // 示例风险因素
242
+ const riskFactors = [
243
+ '宏观经济下行压力增大',
244
+ '行业政策收紧可能性',
245
+ '原材料价格上涨',
246
+ '市场竞争加剧',
247
+ '技术迭代风险'
248
+ ];
249
+
250
+ // 示例有利因素
251
+ const opportunityFactors = [
252
+ '行业景气度持续向好',
253
+ '公司新产品上市',
254
+ '成本控制措施见效',
255
+ '产能扩张计划',
256
+ '国际市场开拓机会'
257
+ ];
258
+
259
+ // 填充HTML
260
+ $('#risk-factors').html('');
261
+ riskFactors.forEach(factor => {
262
+ $('#risk-factors').append(`<li>${factor}</li>`);
263
+ });
264
+
265
+ $('#opportunity-factors').html('');
266
+ opportunityFactors.forEach(factor => {
267
+ $('#opportunity-factors').append(`<li>${factor}</li>`);
268
+ });
269
+ }
270
+
271
+ function renderPricePredictionChart(data) {
272
+ // 准备数据
273
+ const currentPrice = data.current_price;
274
+
275
+ // 提取日期和价格路径
276
+ const dates = Object.keys(data.optimistic.path);
277
+ const optimisticPrices = Object.values(data.optimistic.path);
278
+ const neutralPrices = Object.values(data.neutral.path);
279
+ const pessimisticPrices = Object.values(data.pessimistic.path);
280
+
281
+ const options = {
282
+ series: [
283
+ {
284
+ name: '乐观情景',
285
+ data: optimisticPrices.map((price, i) => ({
286
+ x: new Date(dates[i]),
287
+ y: price
288
+ }))
289
+ },
290
+ {
291
+ name: '中性情景',
292
+ data: neutralPrices.map((price, i) => ({
293
+ x: new Date(dates[i]),
294
+ y: price
295
+ }))
296
+ },
297
+ {
298
+ name: '悲观情景',
299
+ data: pessimisticPrices.map((price, i) => ({
300
+ x: new Date(dates[i]),
301
+ y: price
302
+ }))
303
+ }
304
+ ],
305
+ chart: {
306
+ height: 400,
307
+ type: 'line',
308
+ zoom: {
309
+ enabled: true
310
+ },
311
+ toolbar: {
312
+ show: true
313
+ }
314
+ },
315
+ colors: ['#20E647', '#2E93fA', '#FF4560'],
316
+ dataLabels: {
317
+ enabled: false
318
+ },
319
+ stroke: {
320
+ curve: 'smooth',
321
+ width: [3, 3, 3]
322
+ },
323
+ title: {
324
+ text: '多情景预测',
325
+ align: 'left'
326
+ },
327
+ grid: {
328
+ borderColor: '#e7e7e7',
329
+ row: {
330
+ colors: ['#f3f3f3', 'transparent'],
331
+ opacity: 0.5
332
+ },
333
+ },
334
+ markers: {
335
+ size: 1
336
+ },
337
+ xaxis: {
338
+ type: 'datetime',
339
+ title: {
340
+ text: '日期'
341
+ }
342
+ },
343
+ yaxis: {
344
+ title: {
345
+ text: '价格 (¥)'
346
+ },
347
+ labels: {
348
+ formatter: function(val) {
349
+ return formatNumber(val, 2);
350
+ }
351
+ }
352
+ },
353
+ legend: {
354
+ position: 'top',
355
+ horizontalAlign: 'right'
356
+ },
357
+ tooltip: {
358
+ shared: true,
359
+ intersect: false,
360
+ y: {
361
+ formatter: function(value) {
362
+ return '¥' + formatNumber(value, 2);
363
+ }
364
+ }
365
+ },
366
+ annotations: {
367
+ yaxis: [
368
+ {
369
+ y: currentPrice,
370
+ borderColor: '#000',
371
+ label: {
372
+ borderColor: '#000',
373
+ style: {
374
+ color: '#fff',
375
+ background: '#000'
376
+ },
377
+ text: '当前价格: ¥' + formatNumber(currentPrice, 2)
378
+ }
379
+ }
380
+ ]
381
+ }
382
+ };
383
+
384
+ const chart = new ApexCharts(document.querySelector("#price-prediction-chart"), options);
385
+ chart.render();
386
+ }
387
+ </script>
388
+ {% endblock %}
templates/stock_detail.html ADDED
@@ -0,0 +1,1200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}股票详情 - {{ stock_code }} - 智能分析系统{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid py-1" style="padding-top: 0.2rem !important; padding-bottom: 0.2rem !important;">
7
+
8
+ <div id="alerts-container"></div>
9
+
10
+ <!-- 调整布局: 减少垂直方向上的间距 -->
11
+ <div class="d-flex justify-content-between align-items-center mb-1" style="margin-top: 0.2rem; margin-bottom: 0.2rem;">
12
+ <h4 id="stock-title" class="mb-0 fw-bold" style="font-size: 1.1rem; line-height: 1.2;">股票详情加载中...</h4>
13
+ <div class="d-flex align-items-center">
14
+ <select class="form-select form-select-sm me-2" id="market-type" style="max-width: 100px; height: 32px; padding-top: 2px; padding-bottom: 2px;">
15
+ <option value="A" {% if market_type == 'A' %}selected{% endif %}>A股</option>
16
+ <option value="HK" {% if market_type == 'HK' %}selected{% endif %}>港股</option>
17
+ <option value="US" {% if market_type == 'US' %}selected{% endif %}>美股</option>
18
+ </select>
19
+ <select class="form-select form-select-sm me-2" id="analysis-period" style="max-width: 100px; height: 32px; padding-top: 2px; padding-bottom: 2px;">
20
+ <option value="1m">1个月</option>
21
+ <option value="3m">3个月</option>
22
+ <option value="6m">6个月</option>
23
+ <option value="1y" selected>1年</option>
24
+ </select>
25
+ <button id="refresh-btn" class="btn btn-primary btn-sm" style="height: 32px; padding: 2px 8px;">
26
+ <i class="fas fa-sync-alt"></i>
27
+ </button>
28
+ </div>
29
+ </div>
30
+
31
+ <div id="loading-panel" class="text-center py-5">
32
+ <div class="spinner-border text-primary" role="status">
33
+ <span class="visually-hidden">Loading...</span>
34
+ </div>
35
+ <p class="mt-3 mb-0">正在加载股票数据和分析结果...</p>
36
+ <p class="text-muted small mt-2">
37
+ <i class="fas fa-info-circle"></i>
38
+ AI分析需要30-300秒,已处理<span id="processing-time">0</span>秒
39
+ </p>
40
+ <div class="progress mt-3" style="height: 5px;">
41
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
42
+ </div>
43
+ <button id="cancel-analysis-btn" class="btn btn-outline-secondary mt-3">
44
+ <i class="fas fa-times"></i> 取消分析
45
+ </button>
46
+ </div>
47
+
48
+ <div id="error-retry" class="text-center mt-3" style="display: none;">
49
+ <button id="retry-button" class="btn btn-primary mt-2">
50
+ <i class="fas fa-sync-alt"></i> 重试分析
51
+ </button>
52
+ <p class="text-muted small mt-2">
53
+ 如果重试失败,请访问<a href="/dashboard">仪表盘</a>尝试其他股票
54
+ </p>
55
+ </div>
56
+
57
+ <div id="analysis-result" style="display: none;">
58
+ <div class="row g-3 mb-3">
59
+ <div class="col-md-6">
60
+ <div class="card h-100">
61
+ <div class="card-header py-2">
62
+ <h5 class="mb-0">股票概要</h5>
63
+ </div>
64
+ <div class="card-body">
65
+ <div class="row mb-3">
66
+ <div class="col-md-7">
67
+ <h3 id="stock-name" class="mb-0 fs-4"></h3>
68
+ <p id="stock-info" class="text-muted mb-0 small"></p>
69
+ </div>
70
+ <div class="col-md-5 text-end">
71
+ <h2 id="stock-price" class="mb-0 fs-4"></h2>
72
+ <p id="price-change" class="mb-0"></p>
73
+ </div>
74
+ </div>
75
+ <div class="row">
76
+ <div class="col-md-6">
77
+ <div class="mb-2">
78
+ <span class="text-muted small">综合评分:</span>
79
+ <div class="mt-1">
80
+ <span id="total-score" class="badge rounded-pill score-pill"></span>
81
+ </div>
82
+ </div>
83
+ <div class="mb-2">
84
+ <span class="text-muted small">投资建议:</span>
85
+ <p id="recommendation" class="mb-0 text-strong"></p>
86
+ </div>
87
+ </div>
88
+ <div class="col-md-6">
89
+ <div class="mb-2">
90
+ <span class="text-muted small">技术面指标:</span>
91
+ <ul class="list-unstyled mt-1 mb-0 small">
92
+ <li><span class="text-muted">RSI:</span> <span id="rsi-value"></span></li>
93
+ <li><span class="text-muted">MA趋势:</span> <span id="ma-trend"></span></li>
94
+ <li><span class="text-muted">MACD信号:</span> <span id="macd-signal"></span></li>
95
+ <li><span class="text-muted">成交量:</span> <span id="volume-status"></span></li>
96
+ </ul>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <div class="col-md-6">
104
+ <div class="card h-100">
105
+ <div class="card-header py-2">
106
+ <h5 class="mb-0">多维度评分</h5>
107
+ </div>
108
+ <div class="card-body">
109
+ <div class="row">
110
+ <div class="col-md-6">
111
+ <div id="score-chart" style="height: 180px;"></div>
112
+ </div>
113
+ <div class="col-md-6">
114
+ <div id="radar-chart" style="height: 180px;"></div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <div class="row g-3 mb-3">
123
+ <div class="col-12">
124
+ <div class="card">
125
+ <div class="card-header py-2">
126
+ <h5 class="mb-0">价格与技术指标</h5>
127
+ </div>
128
+ <div class="card-body p-0">
129
+ <div id="price-chart" style="height: 400px;"></div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <div class="row g-3 mb-3">
136
+ <div class="col-12">
137
+ <div class="card">
138
+ <div class="card-header py-2">
139
+ <h5 class="mb-0">MACD & RSI 指标</h5>
140
+ </div>
141
+ <div class="card-body p-0">
142
+ <div id="indicators-chart" style="height: 350px;"></div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <div class="row g-3 mb-3">
149
+ <div class="col-md-4">
150
+ <div class="card h-100">
151
+ <div class="card-header py-2">
152
+ <h5 class="mb-0">支撑与压力位</h5>
153
+ </div>
154
+ <div class="card-body">
155
+ <table class="table table-sm">
156
+ <thead>
157
+ <tr>
158
+ <th>类型</th>
159
+ <th>价格</th>
160
+ <th>距离</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody id="support-resistance-table">
164
+ <!-- 支撑压力位数据将在JS中动态填充 -->
165
+ </tbody>
166
+ </table>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ <div class="col-md-8">
171
+ <div class="card h-100">
172
+ <div class="card-header py-2">
173
+ <h5 class="mb-0">AI分析建议</h5>
174
+ </div>
175
+ <div class="card-body">
176
+ <div id="ai-analysis" class="analysis-section">
177
+ <!-- AI分析结果将在JS中动态填充 -->
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ {% endblock %}
186
+
187
+ {% block scripts %}
188
+ <script>
189
+ const stockCode = '{{ stock_code }}';
190
+ let marketType = '{{ market_type }}';
191
+ let period = '1y';
192
+ let stockData = [];
193
+ let analysisResult = null;
194
+
195
+ $(document).ready(function() {
196
+ // 初始加载
197
+ loadStockData();
198
+
199
+ // 刷新按钮点击事件
200
+ $('#refresh-btn').click(function() {
201
+ marketType = $('#market-type').val();
202
+ period = $('#analysis-period').val();
203
+ loadStockData();
204
+ });
205
+
206
+ // 市场类型改变事件
207
+ $('#market-type').change(function() {
208
+ marketType = $(this).val();
209
+ });
210
+
211
+ // 分析周期改变事件
212
+ $('#analysis-period').change(function() {
213
+ period = $(this).val();
214
+ });
215
+ });
216
+
217
+ // 加载股票数据
218
+ function loadStockData() {
219
+ $('#loading-panel').show();
220
+ $('#analysis-result').hide();
221
+
222
+ // 获取股票数据
223
+ $.ajax({
224
+ url: `/api/stock_data?stock_code=${stockCode}&market_type=${marketType}&period=${period}`,
225
+ type: 'GET',
226
+ dataType: 'json',
227
+ success: function(response) {
228
+
229
+ // 检查response是否有data属性
230
+ if (!response.data) {
231
+ $('#loading-panel').hide();
232
+ showError('响应格式不正确: 缺少data字段');
233
+ return;
234
+ }
235
+
236
+ if (response.data.length === 0) {
237
+ $('#loading-panel').hide();
238
+ showError('未找到股票数据');
239
+ return;
240
+ }
241
+
242
+ stockData = response.data;
243
+
244
+ // 获取增强分析数据
245
+ loadAnalysisResult();
246
+ },
247
+ error: function(xhr, status, error) {
248
+ $('#loading-panel').hide();
249
+
250
+ let errorMsg = '获取股票数据失败';
251
+ if (xhr.responseJSON && xhr.responseJSON.error) {
252
+ errorMsg += ': ' + xhr.responseJSON.error;
253
+ } else if (error) {
254
+ errorMsg += ': ' + error;
255
+ }
256
+ showError(errorMsg);
257
+ }
258
+ });
259
+ }
260
+
261
+ // 加载分析结果
262
+ function loadAnalysisResult() {
263
+
264
+ // 显示加载状态并启动进度更新
265
+ $('#loading-panel').show();
266
+ $('#analysis-result').hide();
267
+ $('#error-retry').hide();
268
+
269
+ // 添加处理时间计数器
270
+ let processingTime = 0;
271
+ const processingTimer = setInterval(function() {
272
+ processingTime++;
273
+ $('#processing-time').text(processingTime);
274
+ }, 1000);
275
+
276
+ // 使用新的API启动分析任务
277
+ $.ajax({
278
+ url: '/api/start_stock_analysis',
279
+ type: 'POST',
280
+ contentType: 'application/json',
281
+ data: JSON.stringify({
282
+ stock_code: stockCode,
283
+ market_type: marketType
284
+ }),
285
+ success: function(response) {
286
+
287
+ // 检查是否已有结果
288
+ if (response.status === 'completed' && response.result) {
289
+ // 任务已完成,直接处理结果
290
+ handleAnalysisResult(response.result);
291
+ clearInterval(processingTimer);
292
+ return;
293
+ }
294
+
295
+ // 开始轮询任务状态
296
+ pollAnalysisStatus(response.task_id, processingTime, processingTimer);
297
+ },
298
+ error: function(xhr, status, error) {
299
+ clearInterval(processingTimer);
300
+ handleAnalysisError(xhr, status, error);
301
+ }
302
+ });
303
+ }
304
+
305
+ // 轮询分析任务状态
306
+ function pollAnalysisStatus(taskId, startTime, timerInterval) {
307
+ let elapsedTime = startTime || 0;
308
+ let pollInterval;
309
+
310
+ // 保存当前任务ID,用于取消
311
+ window.currentAnalysisTaskId = taskId;
312
+
313
+ // 立即执行一次,然后设置定时器
314
+ checkStatus();
315
+
316
+ function checkStatus() {
317
+ $.ajax({
318
+ url: `/api/analysis_status/${taskId}`,
319
+ type: 'GET',
320
+ success: function(response) {
321
+ // 更新计时和进度
322
+ elapsedTime = startTime + 1;
323
+ const progress = response.progress || 0;
324
+
325
+ // 更新进度显示
326
+ $('#processing-time').text(elapsedTime);
327
+
328
+ // 根据任务状态处理
329
+ if (response.status === 'completed') {
330
+ // 分析完成,停止轮询
331
+ clearInterval(pollInterval);
332
+ clearInterval(timerInterval);
333
+
334
+ // 处理结果
335
+ handleAnalysisResult(response.result);
336
+ } else if (response.status === 'failed') {
337
+ // 分析失败,停止轮询
338
+ clearInterval(pollInterval);
339
+ clearInterval(timerInterval);
340
+
341
+ $('#loading-panel').hide();
342
+
343
+ showError('分析失败: ' + (response.error || '未知错误'));
344
+ $('#error-retry').show();
345
+ } else {
346
+ // 任务仍在进行中,继续轮询
347
+ if (!pollInterval) {
348
+ pollInterval = setInterval(checkStatus, 2000);
349
+ }
350
+ }
351
+ },
352
+ error: function(xhr, status, error) {
353
+ if (!pollInterval) {
354
+ pollInterval = setInterval(checkStatus, 3000);
355
+ }
356
+ }
357
+ });
358
+ }
359
+ }
360
+
361
+ // 处理分析结果
362
+ function handleAnalysisResult(result) {
363
+ try {
364
+ // 设置全局变量
365
+ analysisResult = result;
366
+
367
+ // 渲染分析结果
368
+ renderAnalysisResult();
369
+
370
+ // 更新UI
371
+ $('#loading-panel').hide();
372
+ $('#analysis-result').show();
373
+ } catch (error) {
374
+ $('#loading-panel').hide();
375
+ showError('处理分析结果时出错: ' + error.message);
376
+ }
377
+ }
378
+
379
+ // 处理分析错误
380
+ function handleAnalysisError(xhr, status, error) {
381
+ $('#loading-panel').hide();
382
+
383
+ let errorMsg = '获取分析数据失败';
384
+ if (status === 'timeout') {
385
+ errorMsg = '请求超时,分析可能需要较长时间,请稍后再试';
386
+ } else if (xhr.status === 524 || xhr.status === 504) {
387
+ errorMsg = '请求超时,服务器处理时间过长';
388
+ } else if (xhr.responseJSON && xhr.responseJSON.error) {
389
+ errorMsg += ': ' + xhr.responseJSON.error;
390
+ } else if (error) {
391
+ errorMsg += ': ' + error;
392
+ }
393
+
394
+ showError(errorMsg);
395
+ $('#error-retry').show();
396
+ }
397
+
398
+ // 取消按钮功能
399
+ $('#cancel-analysis-btn').click(function() {
400
+ if (window.currentAnalysisTaskId) {
401
+ $.ajax({
402
+ url: `/api/cancel_analysis/${window.currentAnalysisTaskId}`,
403
+ type: 'POST',
404
+ success: function(response) {
405
+ $('#loading-panel').hide();
406
+ showInfo('分析已取消');
407
+ },
408
+ error: function(error) {
409
+ console.error('取消分析失败:', error);
410
+ }
411
+ });
412
+ } else {
413
+ $('#loading-panel').hide();
414
+ }
415
+ });
416
+
417
+ // 重试按钮功能
418
+ $('#retry-button').click(function() {
419
+ $('#error-retry').hide();
420
+ loadAnalysisResult();
421
+ });
422
+
423
+ // 通用安全格式化函数
424
+ function safeFormat(value, decimals=2) {
425
+ try {
426
+ // 处理numpy对象残留
427
+ if (value && typeof value === 'object') {
428
+ if (value._dtype === 'float64') {
429
+ return parseFloat(value._values[0]).toFixed(decimals);
430
+ }
431
+ if (value._dtype === 'int64') {
432
+ return parseInt(value._values[0]);
433
+ }
434
+ }
435
+ return parseFloat(value).toFixed(decimals);
436
+ } catch (e) {
437
+ console.error('Format error:', e);
438
+ return '--';
439
+ }
440
+ }
441
+
442
+ // 使用示例
443
+ $('#rsi-value').text(safeFormat(analysisResult.technical_analysis.indicators.rsi));
444
+
445
+ // 渲染分析结果
446
+ function renderAnalysisResult() {
447
+ if (!analysisResult) {
448
+ showError("分析结果为空");
449
+ return;
450
+ }
451
+
452
+ try {
453
+
454
+ // 使用全新的安全访问函数
455
+ function safeGet(obj, path, defaultValue) {
456
+ if (!obj) return defaultValue;
457
+
458
+ const props = path.split('.');
459
+ let current = obj;
460
+
461
+ for (let i = 0; i < props.length; i++) {
462
+ if (current === undefined || current === null) {
463
+ console.warn(`属性路径 ${path} 在 ${props.slice(0, i).join('.')} 处中断`);
464
+ return defaultValue;
465
+ }
466
+ current = current[props[i]];
467
+ }
468
+
469
+ return current !== undefined && current !== null ? current : defaultValue;
470
+ }
471
+
472
+ // 安全检查技术分析数据
473
+ if (!analysisResult.technical_analysis) {
474
+ analysisResult.technical_analysis = {
475
+ trend: {ma_trend: 'UNKNOWN', ma_status: '未知', ma_values: {}},
476
+ indicators: {rsi: 50, macd: 0, macd_signal: 0, macd_histogram: 0, volatility: 0},
477
+ volume: {current_volume: 0, volume_ratio: 0, volume_status: 'NORMAL'},
478
+ support_resistance: {support_levels: {}, resistance_levels: {}}
479
+ };
480
+ }
481
+
482
+ // 使用安全函数获取所有数据
483
+ const stockName = safeGet(analysisResult, 'basic_info.stock_name', '未知');
484
+ const stockCode = safeGet(analysisResult, 'basic_info.stock_code', '未知');
485
+ const industry = safeGet(analysisResult, 'basic_info.industry', '未知');
486
+ const analysisDate = safeGet(analysisResult, 'basic_info.analysis_date', '未知日期');
487
+
488
+ // 更新页面标题
489
+ $('#stock-title').text(`${stockName} (${stockCode}) 股票分析`);
490
+
491
+ // 渲染股票基本信息
492
+ $('#stock-name').text(`${stockName} (${stockCode})`);
493
+ $('#stock-info').text(`${industry} | ${analysisDate}`);
494
+
495
+ // 渲染价格信息
496
+ const currentPrice = safeGet(analysisResult, 'price_data.current_price', 0);
497
+ const priceChange = safeGet(analysisResult, 'price_data.price_change', 0);
498
+ const priceChangeValue = safeGet(analysisResult, 'price_data.price_change_value', 0);
499
+
500
+ $('#stock-price').text('¥' + formatNumber(currentPrice, 2));
501
+ const priceChangeClass = priceChange >= 0 ? 'trend-up' : 'trend-down';
502
+ const priceChangeIcon = priceChange >= 0 ? '<i class="fas fa-caret-up"></i>' : '<i class="fas fa-caret-down"></i>';
503
+ $('#price-change').html(`<span class="${priceChangeClass}">${priceChangeIcon} ${formatNumber(priceChangeValue, 2)} (${formatPercent(priceChange, 2)})</span>`);
504
+
505
+ // 渲染评分和建议
506
+ const totalScore = safeGet(analysisResult, 'scores.total', 0);
507
+ const scoreClass = getScoreColorClass(totalScore);
508
+ $('#total-score').text(totalScore).addClass(scoreClass);
509
+ $('#recommendation').text(safeGet(analysisResult, 'recommendation.action', '无建议'));
510
+
511
+ // 渲染技术指标 - 所有属性都使用安全访问
512
+ $('#rsi-value').text(formatNumber(safeGet(analysisResult, 'technical_analysis.indicators.rsi', 0), 2));
513
+
514
+ const maTrend = safeGet(analysisResult, 'technical_analysis.trend.ma_trend', 'UNKNOWN');
515
+ const maStatus = safeGet(analysisResult, 'technical_analysis.trend.ma_status', '未知');
516
+ const maTrendClass = getTrendColorClass(maTrend);
517
+ const maTrendIcon = getTrendIcon(maTrend);
518
+ $('#ma-trend').html(`<span class="${maTrendClass}">${maTrendIcon} ${maStatus}</span>`);
519
+
520
+ // MACD信号
521
+ const macd = safeGet(analysisResult, 'technical_analysis.indicators.macd', 0);
522
+ const macdSignal = safeGet(analysisResult, 'technical_analysis.indicators.macd_signal', 0);
523
+ const macdStatus = macd > macdSignal ? 'BUY' : 'SELL';
524
+ const macdClass = macdStatus === 'BUY' ? 'trend-up' : 'trend-down';
525
+ const macdIcon = macdStatus === 'BUY' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
526
+ $('#macd-signal').html(`<span class="${macdClass}">${macdIcon} ${macdStatus}</span>`);
527
+
528
+ // 成交量状态
529
+ const volumeStatus = safeGet(analysisResult, 'technical_analysis.volume.volume_status', 'NORMAL');
530
+ const volumeClass = volumeStatus === 'HIGH' ? 'trend-up' : 'trend-down';
531
+ const volumeIcon = volumeStatus === 'HIGH' ? '<i class="fas fa-arrow-up"></i>' : '<i class="fas fa-arrow-down"></i>';
532
+ $('#volume-status').html(`<span class="${volumeClass}">${volumeIcon} ${volumeStatus}</span>`);
533
+
534
+ // 支撑压力位 - 完全重写为更安全的版本
535
+ let supportResistanceHtml = '';
536
+
537
+ // 渲染压力位
538
+ const shortTermResistance = safeGet(analysisResult, 'technical_analysis.support_resistance.resistance_levels.short_term', []);
539
+ if (shortTermResistance.length > 0) {
540
+ const resistance = shortTermResistance[0];
541
+ const distance = ((resistance - currentPrice) / currentPrice * 100).toFixed(2);
542
+ supportResistanceHtml += `
543
+ <tr>
544
+ <td><span class="badge bg-danger">短期压力</span></td>
545
+ <td>${formatNumber(resistance, 2)}</td>
546
+ <td>+${distance}%</td>
547
+ </tr>
548
+ `;
549
+ }
550
+
551
+ const mediumTermResistance = safeGet(analysisResult, 'technical_analysis.support_resistance.resistance_levels.medium_term', []);
552
+ if (mediumTermResistance.length > 0) {
553
+ const resistance = mediumTermResistance[0];
554
+ const distance = ((resistance - currentPrice) / currentPrice * 100).toFixed(2);
555
+ supportResistanceHtml += `
556
+ <tr>
557
+ <td><span class="badge bg-warning text-dark">中期压力</span></td>
558
+ <td>${formatNumber(resistance, 2)}</td>
559
+ <td>+${distance}%</td>
560
+ </tr>
561
+ `;
562
+ }
563
+
564
+ // 渲染支撑位
565
+ const shortTermSupport = safeGet(analysisResult, 'technical_analysis.support_resistance.support_levels.short_term', []);
566
+ if (shortTermSupport.length > 0) {
567
+ const support = shortTermSupport[0];
568
+ const distance = ((support - currentPrice) / currentPrice * 100).toFixed(2);
569
+ supportResistanceHtml += `
570
+ <tr>
571
+ <td><span class="badge bg-success">短期支撑</span></td>
572
+ <td>${formatNumber(support, 2)}</td>
573
+ <td>${distance}%</td>
574
+ </tr>
575
+ `;
576
+ }
577
+
578
+ const mediumTermSupport = safeGet(analysisResult, 'technical_analysis.support_resistance.support_levels.medium_term', []);
579
+ if (mediumTermSupport.length > 0) {
580
+ const support = mediumTermSupport[0];
581
+ const distance = ((support - currentPrice) / currentPrice * 100).toFixed(2);
582
+ supportResistanceHtml += `
583
+ <tr>
584
+ <td><span class="badge bg-info">中期支撑</span></td>
585
+ <td>${formatNumber(support, 2)}</td>
586
+ <td>${distance}%</td>
587
+ </tr>
588
+ `;
589
+ }
590
+
591
+ if (supportResistanceHtml === '') {
592
+ supportResistanceHtml = '<tr><td colspan="3" class="text-center">暂无支撑压力位数据</td></tr>';
593
+ }
594
+
595
+ $('#support-resistance-table').html(supportResistanceHtml);
596
+
597
+ // 渲染AI分析
598
+ const aiAnalysis = safeGet(analysisResult, 'ai_analysis', '暂无AI分析');
599
+ $('#ai-analysis').html(formatAIAnalysis(aiAnalysis));
600
+
601
+ // 安全地绘制图表
602
+ try {
603
+ renderScoreChart();
604
+ } catch (e) {
605
+ $('#score-chart').html('<div class="text-center text-muted">评分图表渲染失败</div>');
606
+ }
607
+
608
+ try {
609
+ renderRadarChart();
610
+ } catch (e) {
611
+ $('#radar-chart').html('<div class="text-center text-muted">雷达图表渲染失败</div>');
612
+ }
613
+
614
+ try {
615
+ renderPriceChart();
616
+ } catch (e) {
617
+ $('#price-chart').html('<div class="text-center text-muted">价格图表渲染失败</div>');
618
+ }
619
+
620
+ try {
621
+ renderIndicatorsChart();
622
+ } catch (e) {
623
+ $('#indicators-chart').html('<div class="text-center text-muted">指标图表渲染失败</div>');
624
+ }
625
+
626
+ } catch (error) {
627
+ showError(`渲染分析结果时出错: ${error.message}`);
628
+ }
629
+ }
630
+
631
+ // 绘制评分图表
632
+ function renderScoreChart() {
633
+ if (!analysisResult) return;
634
+
635
+ const totalScore = analysisResult.scores.total || 0;
636
+
637
+ const options = {
638
+ series: [totalScore],
639
+ chart: {
640
+ height: 180,
641
+ type: 'radialBar',
642
+ toolbar: {
643
+ show: false
644
+ }
645
+ },
646
+ plotOptions: {
647
+ radialBar: {
648
+ hollow: {
649
+ size: '70%',
650
+ },
651
+ dataLabels: {
652
+ showOn: 'always',
653
+ name: {
654
+ show: true,
655
+ fontSize: '14px',
656
+ fontWeight: 600,
657
+ offsetY: -10
658
+ },
659
+ value: {
660
+ formatter: function(val) {
661
+ return val;
662
+ },
663
+ fontSize: '22px',
664
+ fontWeight: 700,
665
+ offsetY: 5
666
+ }
667
+ }
668
+ }
669
+ },
670
+ fill: {
671
+ type: 'gradient',
672
+ gradient: {
673
+ shade: 'dark',
674
+ type: 'horizontal',
675
+ gradientToColors: ['#ABE5A1'],
676
+ stops: [0, 100]
677
+ }
678
+ },
679
+ stroke: {
680
+ lineCap: 'round'
681
+ },
682
+ labels: ['总分'],
683
+ colors: ['#20E647']
684
+ };
685
+
686
+ // 清除旧图表
687
+ $('#score-chart').empty();
688
+
689
+ const chart = new ApexCharts(document.querySelector("#score-chart"), options);
690
+ chart.render();
691
+ }
692
+
693
+ // 绘制雷达图
694
+ function renderRadarChart() {
695
+ if (!analysisResult) return;
696
+
697
+ const trendScore = analysisResult.scores.trend || 0;
698
+ const indicatorsScore = analysisResult.scores.indicators || 0;
699
+ const supportResistanceScore = analysisResult.scores.support_resistance || 0;
700
+ const volatilityVolumeScore = analysisResult.scores.volatility_volume || 0;
701
+
702
+ const options = {
703
+ series: [{
704
+ name: '评分',
705
+ data: [
706
+ trendScore,
707
+ indicatorsScore,
708
+ supportResistanceScore,
709
+ volatilityVolumeScore
710
+ ]
711
+ }],
712
+ chart: {
713
+ height: 180,
714
+ type: 'radar',
715
+ toolbar: {
716
+ show: false
717
+ }
718
+ },
719
+ xaxis: {
720
+ categories: ['趋势', '指标', '支压', '波动量']
721
+ },
722
+ yaxis: {
723
+ max: 10,
724
+ min: 0
725
+ },
726
+ fill: {
727
+ opacity: 0.5,
728
+ colors: ['#4e73df']
729
+ },
730
+ markers: {
731
+ size: 4
732
+ }
733
+ };
734
+
735
+ // 清除旧图表
736
+ $('#radar-chart').empty();
737
+
738
+ const chart = new ApexCharts(document.querySelector("#radar-chart"), options);
739
+ chart.render();
740
+ }
741
+
742
+ // 绘制价格图表
743
+ function renderPriceChart() {
744
+
745
+ try {
746
+ // Create a fully separate array for each price series (no OHLC array)
747
+ const closePrices = stockData.map(item => {
748
+ // 处理numpy日期格式
749
+ let dateStr = item.date;
750
+ if (dateStr && typeof dateStr === 'object') {
751
+ dateStr = dateStr.toString().split('T')[0];
752
+ }
753
+
754
+ return {
755
+ x: new Date(dateStr + 'T00:00:00'), // 标准化日期
756
+ y: safeFormat(item.close)
757
+ };
758
+ });
759
+
760
+ const ma5Data = stockData.map(item => ({
761
+ x: new Date(item.date),
762
+ y: parseFloat(item.MA5 || 0)
763
+ }));
764
+
765
+ const ma20Data = stockData.map(item => ({
766
+ x: new Date(item.date),
767
+ y: parseFloat(item.MA20 || 0)
768
+ }));
769
+
770
+ const ma60Data = stockData.map(item => ({
771
+ x: new Date(item.date),
772
+ y: parseFloat(item.MA60 || 0)
773
+ }));
774
+
775
+ // Create chart options using line chart instead of candlestick
776
+ const priceOptions = {
777
+ series: [
778
+ {
779
+ name: '收盘价',
780
+ type: 'line',
781
+ data: closePrices
782
+ },
783
+ {
784
+ name: 'MA5',
785
+ type: 'line',
786
+ data: ma5Data
787
+ },
788
+ {
789
+ name: 'MA20',
790
+ type: 'line',
791
+ data: ma20Data
792
+ },
793
+ {
794
+ name: 'MA60',
795
+ type: 'line',
796
+ data: ma60Data
797
+ }
798
+ ],
799
+ chart: {
800
+ height: 400,
801
+ type: 'line',
802
+ toolbar: {
803
+ show: true
804
+ },
805
+ animations: {
806
+ enabled: false
807
+ }
808
+ },
809
+ stroke: {
810
+ width: [3, 2, 2, 2],
811
+ curve: 'straight'
812
+ },
813
+ title: {
814
+ text: `价格走势图`,
815
+ align: 'left'
816
+ },
817
+ xaxis: {
818
+ type: 'datetime'
819
+ },
820
+ yaxis: {
821
+ labels: {
822
+ formatter: function(value) {
823
+ return formatNumber(value, 2);
824
+ }
825
+ }
826
+ },
827
+ tooltip: {
828
+ enabled: true,
829
+ shared: true,
830
+ intersect: false,
831
+ x: {
832
+ format: 'yyyy-MM-dd'
833
+ },
834
+ y: {
835
+ formatter: function(value) {
836
+ return formatNumber(value, 2);
837
+ }
838
+ }
839
+ },
840
+ legend: {
841
+ show: true,
842
+ position: 'top',
843
+ horizontalAlign: 'left'
844
+ },
845
+ markers: {
846
+ size: 0
847
+ },
848
+ grid: {
849
+ show: true
850
+ }
851
+ };
852
+
853
+
854
+ $('#price-chart').empty();
855
+
856
+
857
+ const chart = new ApexCharts(document.querySelector("#price-chart"), priceOptions);
858
+
859
+ chart.render();
860
+
861
+
862
+ } catch (error) {
863
+ // Show error message
864
+ $('#price-chart').html('<div class="alert alert-danger">图表加载失败: ' + error.message + '</div>');
865
+ }
866
+ }
867
+
868
+ // Format AI analysis text
869
+ function formatAIAnalysis(text) {
870
+ if (!text) return '';
871
+
872
+ // First, make the text safe for HTML
873
+ const safeText = text
874
+ .replace(/&/g, '&amp;')
875
+ .replace(/</g, '&lt;')
876
+ .replace(/>/g, '&gt;');
877
+
878
+ // Replace basic Markdown elements
879
+ let formatted = safeText
880
+ // Bold text with ** or __
881
+ .replace(/\*\*(.*?)\*\*/g, '<strong class="keyword">$1</strong>')
882
+ .replace(/__(.*?)__/g, '<strong>$1</strong>')
883
+
884
+ // Italic text with * or _
885
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
886
+ .replace(/_(.*?)_/g, '<em>$1</em>')
887
+
888
+ // Headers
889
+ .replace(/^# (.*?)$/gm, '<h4 class="mt-3 mb-2">$1</h4>')
890
+ .replace(/^## (.*?)$/gm, '<h5 class="mt-2 mb-2">$1</h5>')
891
+
892
+ // Apply special styling to financial terms
893
+ .replace(/支撑位/g, '<span class="keyword">支撑位</span>')
894
+ .replace(/压力位/g, '<span class="keyword">压力位</span>')
895
+ .replace(/趋势/g, '<span class="keyword">趋势</span>')
896
+ .replace(/均线/g, '<span class="keyword">均线</span>')
897
+ .replace(/MACD/g, '<span class="term">MACD</span>')
898
+ .replace(/RSI/g, '<span class="term">RSI</span>')
899
+ .replace(/KDJ/g, '<span class="term">KDJ</span>')
900
+
901
+ // Highlight price patterns and movements
902
+ .replace(/([上涨升])/g, '<span class="trend-up">$1</span>')
903
+ .replace(/([下跌降])/g, '<span class="trend-down">$1</span>')
904
+ .replace(/(买入|做多|多头|突破)/g, '<span class="trend-up">$1</span>')
905
+ .replace(/(卖出|做空|空头|跌破)/g, '<span class="trend-down">$1</span>')
906
+
907
+ // Highlight price values (matches patterns like 31.25, 120.50)
908
+ .replace(/(\d+\.\d{2})/g, '<span class="price">$1</span>')
909
+
910
+ // Convert line breaks to paragraph tags
911
+ .replace(/\n\n+/g, '</p><p class="analysis-para">')
912
+ .replace(/\n/g, '<br>');
913
+
914
+ // Wrap in paragraph tags for consistent styling
915
+ return '<p class="analysis-para">' + formatted + '</p>';
916
+ }
917
+
918
+ function renderPriceChartWithOHLC() {
919
+
920
+
921
+ try {
922
+ // Create OHLC data table to display below the chart
923
+ let ohlcTableHtml = '<div class="ohlc-data mt-3"><h6>价格数据 (最近5天)</h6><table class="table table-sm table-bordered">';
924
+ ohlcTableHtml += '<thead><tr><th>日期</th><th>开盘</th><th>最高</th><th>最低</th><th>收盘</th></tr></thead><tbody>';
925
+
926
+ // Get last 5 days of data
927
+ const recentData = stockData.slice(-5);
928
+ recentData.forEach(item => {
929
+ ohlcTableHtml += `<tr>
930
+ <td>${item.date}</td>
931
+ <td>${formatNumber(item.open, 2)}</td>
932
+ <td>${formatNumber(item.high, 2)}</td>
933
+ <td>${formatNumber(item.low, 2)}</td>
934
+ <td>${formatNumber(item.close, 2)}</td>
935
+ </tr>`;
936
+ });
937
+ ohlcTableHtml += '</tbody></table></div>';
938
+
939
+ // Create price line chart
940
+ const closePrices = stockData.map(item => {
941
+ // 处理numpy日期格式
942
+ let dateStr = item.date;
943
+ if (dateStr && typeof dateStr === 'object') {
944
+ dateStr = dateStr.toString().split('T')[0];
945
+ }
946
+
947
+ return {
948
+ x: new Date(dateStr + 'T00:00:00'), // 标准化日期
949
+ y: safeFormat(item.close)
950
+ };
951
+ });
952
+
953
+ const ma5Data = stockData.map(item => ({
954
+ x: new Date(item.date),
955
+ y: parseFloat(item.MA5 || 0)
956
+ }));
957
+
958
+ const ma20Data = stockData.map(item => ({
959
+ x: new Date(item.date),
960
+ y: parseFloat(item.MA20 || 0)
961
+ }));
962
+
963
+ const ma60Data = stockData.map(item => ({
964
+ x: new Date(item.date),
965
+ y: parseFloat(item.MA60 || 0)
966
+ }));
967
+
968
+ const priceOptions = {
969
+ series: [
970
+ {
971
+ name: '收盘价',
972
+ type: 'line',
973
+ data: closePrices
974
+ },
975
+ {
976
+ name: 'MA5',
977
+ type: 'line',
978
+ data: ma5Data
979
+ },
980
+ {
981
+ name: 'MA20',
982
+ type: 'line',
983
+ data: ma20Data
984
+ },
985
+ {
986
+ name: 'MA60',
987
+ type: 'line',
988
+ data: ma60Data
989
+ }
990
+ ],
991
+ chart: {
992
+ height: 350,
993
+ type: 'line',
994
+ toolbar: {
995
+ show: true
996
+ },
997
+ animations: {
998
+ enabled: false
999
+ }
1000
+ },
1001
+ stroke: {
1002
+ width: [3, 2, 2, 2]
1003
+ },
1004
+ colors: ['#FF4560', '#008FFB', '#00E396', '#775DD0'],
1005
+ title: {
1006
+ text: `价格走势图 (收盘价)`,
1007
+ align: 'left'
1008
+ },
1009
+ xaxis: {
1010
+ type: 'datetime'
1011
+ },
1012
+ yaxis: {
1013
+ labels: {
1014
+ formatter: function(value) {
1015
+ return formatNumber(value, 2);
1016
+ }
1017
+ }
1018
+ },
1019
+ tooltip: {
1020
+ enabled: true,
1021
+ shared: true,
1022
+ intersect: false,
1023
+ x: {
1024
+ format: 'yyyy-MM-dd'
1025
+ }
1026
+ },
1027
+ legend: {
1028
+ show: true
1029
+ }
1030
+ };
1031
+
1032
+
1033
+ // Clear the chart div and prepare it for the chart + table
1034
+ $('#price-chart').empty();
1035
+
1036
+ // Create a container for the chart
1037
+ $('#price-chart').append('<div id="price-line-chart"></div>');
1038
+
1039
+ const chart = new ApexCharts(document.querySelector("#price-line-chart"), priceOptions);
1040
+
1041
+ chart.render();
1042
+
1043
+ // Append the OHLC table below the chart
1044
+ $('#price-chart').append(ohlcTableHtml);
1045
+
1046
+ } catch (error) {
1047
+ $('#price-chart').html('<div class="alert alert-danger">图表加载失败: ' + error.message + '</div>');
1048
+ }
1049
+ }
1050
+
1051
+
1052
+ // 绘制技术指标图表
1053
+ function renderIndicatorsChart() {
1054
+
1055
+ try {
1056
+ // Create chart options inline without using variables
1057
+ const indicatorOptions = {
1058
+ series: [
1059
+ {
1060
+ name: 'MACD',
1061
+ type: 'line',
1062
+ data: stockData.map(item => ({
1063
+ x: new Date(item.date),
1064
+ y: parseFloat(item.MACD || 0)
1065
+ }))
1066
+ },
1067
+ {
1068
+ name: 'Signal',
1069
+ type: 'line',
1070
+ data: stockData.map(item => ({
1071
+ x: new Date(item.date),
1072
+ y: parseFloat(item.Signal || 0)
1073
+ }))
1074
+ },
1075
+ {
1076
+ name: 'Histogram',
1077
+ type: 'bar',
1078
+ data: stockData.map(item => ({
1079
+ x: new Date(item.date),
1080
+ y: parseFloat(item.MACD_hist || 0)
1081
+ }))
1082
+ },
1083
+ {
1084
+ name: 'RSI',
1085
+ type: 'line',
1086
+ data: stockData.map(item => ({
1087
+ x: new Date(item.date),
1088
+ y: parseFloat(item.RSI || 0)
1089
+ }))
1090
+ }
1091
+ ],
1092
+ chart: {
1093
+ height: 350,
1094
+ type: 'line',
1095
+ stacked: false,
1096
+ toolbar: {
1097
+ show: true
1098
+ },
1099
+ // Disable animations
1100
+ animations: {
1101
+ enabled: false
1102
+ }
1103
+ },
1104
+ stroke: {
1105
+ width: [3, 3, 0, 3],
1106
+ curve: 'smooth'
1107
+ },
1108
+ xaxis: {
1109
+ type: 'datetime'
1110
+ },
1111
+ yaxis: [
1112
+ {
1113
+ title: {
1114
+ text: 'MACD',
1115
+ },
1116
+ seriesName: 'MACD',
1117
+ labels: {
1118
+ formatter: function(value) {
1119
+ return formatNumber(value, 3);
1120
+ }
1121
+ }
1122
+ },
1123
+ {
1124
+ show: false,
1125
+ seriesName: 'Signal'
1126
+ },
1127
+ {
1128
+ show: false,
1129
+ seriesName: 'Histogram'
1130
+ },
1131
+ {
1132
+ opposite: true,
1133
+ title: {
1134
+ text: 'RSI'
1135
+ },
1136
+ min: 0,
1137
+ max: 100,
1138
+ seriesName: 'RSI',
1139
+ labels: {
1140
+ formatter: function(value) {
1141
+ return formatNumber(value, 2);
1142
+ }
1143
+ }
1144
+ }
1145
+ ],
1146
+ // Simplified tooltip configuration
1147
+ tooltip: {
1148
+ enabled: true,
1149
+ shared: true,
1150
+ intersect: false, // Important for preventing null element errors
1151
+ hideEmptySeries: true,
1152
+ x: {
1153
+ format: 'yyyy-MM-dd'
1154
+ },
1155
+ y: {
1156
+ formatter: function(value, { seriesIndex }) {
1157
+ // Simplified formatter function
1158
+ if (seriesIndex === 0) return `MACD: ${formatNumber(value, 3)}`;
1159
+ if (seriesIndex === 1) return `Signal: ${formatNumber(value, 3)}`;
1160
+ if (seriesIndex === 2) return `Histogram: ${formatNumber(value, 3)}`;
1161
+ if (seriesIndex === 3) return `RSI: ${formatNumber(value, 2)}`;
1162
+ return formatNumber(value, 2);
1163
+ }
1164
+ }
1165
+ },
1166
+ colors: ['#008FFB', '#00E396', '#CED4DC', '#FEB019'],
1167
+ legend: {
1168
+ show: true,
1169
+ position: 'top',
1170
+ horizontalAlign: 'left',
1171
+ floating: false
1172
+ },
1173
+ // Explicitly set marker options to prevent errors
1174
+ markers: {
1175
+ size: 4,
1176
+ strokeWidth: 0
1177
+ }
1178
+ };
1179
+
1180
+ $('#indicators-chart').empty();
1181
+
1182
+ const chart = new ApexCharts(document.querySelector("#indicators-chart"), indicatorOptions);
1183
+
1184
+ chart.render();
1185
+
1186
+ } catch (error) {
1187
+ $('#indicators-chart').html('<div class="alert alert-danger">指标图表加载失败: ' + error.message + '</div>');
1188
+ }
1189
+ }
1190
+
1191
+ // 添加到script部分
1192
+ $('#retry-button').click(function() {
1193
+ // 隐藏错误和重试区域
1194
+ $('#error-retry').hide();
1195
+ // 重新加载分析
1196
+ loadAnalysisResult();
1197
+ });
1198
+
1199
+ </script>
1200
+ {% endblock %}
us_stock_service.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 修改:熊猫大侠
5
+ 版本:v2.1.0
6
+ """
7
+ # us_stock_service.py
8
+ import akshare as ak
9
+ import pandas as pd
10
+ import logging
11
+
12
+
13
+ class USStockService:
14
+ def __init__(self):
15
+ logging.basicConfig(level=logging.INFO,
16
+ format='%(asctime)s - %(levelname)s - %(message)s')
17
+ self.logger = logging.getLogger(__name__)
18
+
19
+ def search_us_stocks(self, keyword):
20
+ """
21
+ 搜索美股代码
22
+ :param keyword: 搜索关键词
23
+ :return: 匹配的股票列表
24
+ """
25
+ try:
26
+ # 获取美股数据
27
+ df = ak.stock_us_spot_em()
28
+
29
+ # 转换列名
30
+ df = df.rename(columns={
31
+ "序号": "index",
32
+ "名称": "name",
33
+ "最新价": "price",
34
+ "涨跌额": "price_change",
35
+ "涨跌幅": "price_change_percent",
36
+ "开盘价": "open",
37
+ "最高价": "high",
38
+ "最低价": "low",
39
+ "昨收价": "pre_close",
40
+ "总市值": "market_value",
41
+ "市盈率": "pe_ratio",
42
+ "成交量": "volume",
43
+ "成交额": "turnover",
44
+ "振幅": "amplitude",
45
+ "换手率": "turnover_rate",
46
+ "代码": "symbol"
47
+ })
48
+
49
+ # 模糊匹配搜索
50
+ mask = df['name'].str.contains(keyword, case=False, na=False)
51
+ results = df[mask]
52
+
53
+ # 格式化返回结果并处理 NaN 值
54
+ formatted_results = []
55
+ for _, row in results.iterrows():
56
+ formatted_results.append({
57
+ 'name': row['name'] if pd.notna(row['name']) else '',
58
+ 'symbol': str(row['symbol']) if pd.notna(row['symbol']) else '',
59
+ 'price': float(row['price']) if pd.notna(row['price']) else 0.0,
60
+ 'market_value': float(row['market_value']) if pd.notna(row['market_value']) else 0.0
61
+ })
62
+
63
+ return formatted_results
64
+
65
+ except Exception as e:
66
+ self.logger.error(f"搜索美股代码时出错: {str(e)}")
67
+ raise Exception(f"搜索美股代码失败: {str(e)}")
web_server.py ADDED
@@ -0,0 +1,1538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 智能分析系统(股票) - 股票市场数据分析系统
4
+ 修改:熊猫大侠
5
+ 版本:v2.1.0
6
+ """
7
+ # web_server.py
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from flask import Flask, render_template, request, jsonify, redirect, url_for
12
+ from stock_analyzer import StockAnalyzer
13
+ from us_stock_service import USStockService
14
+ import threading
15
+ import logging
16
+ from logging.handlers import RotatingFileHandler
17
+ import traceback
18
+ import os
19
+ import json
20
+ from datetime import date, datetime, timedelta
21
+ from flask_cors import CORS
22
+ import time
23
+ from flask_caching import Cache
24
+ import threading
25
+ import sys
26
+ from flask_swagger_ui import get_swaggerui_blueprint
27
+ from database import get_session, StockInfo, AnalysisResult, Portfolio, USE_DATABASE
28
+ from dotenv import load_dotenv
29
+ from industry_analyzer import IndustryAnalyzer
30
+
31
+ # 加载环境变量
32
+ load_dotenv()
33
+
34
+ # 检查是否需要初始化数据库
35
+ if USE_DATABASE:
36
+ init_db()
37
+
38
+ # 配置Swagger
39
+ SWAGGER_URL = '/api/docs'
40
+ API_URL = '/static/swagger.json'
41
+ swaggerui_blueprint = get_swaggerui_blueprint(
42
+ SWAGGER_URL,
43
+ API_URL,
44
+ config={
45
+ 'app_name': "股票智能分析系统 API文档"
46
+ }
47
+ )
48
+
49
+ app = Flask(__name__)
50
+ CORS(app, resources={r"/*": {"origins": "*"}})
51
+ analyzer = StockAnalyzer()
52
+ us_stock_service = USStockService()
53
+
54
+ # 配置缓存
55
+ cache_config = {
56
+ 'CACHE_TYPE': 'SimpleCache',
57
+ 'CACHE_DEFAULT_TIMEOUT': 300
58
+ }
59
+
60
+ # 如果配置了Redis,使用Redis作为缓存后端
61
+ if os.getenv('USE_REDIS_CACHE', 'False').lower() == 'true' and os.getenv('REDIS_URL'):
62
+ cache_config = {
63
+ 'CACHE_TYPE': 'RedisCache',
64
+ 'CACHE_REDIS_URL': os.getenv('REDIS_URL'),
65
+ 'CACHE_DEFAULT_TIMEOUT': 300
66
+ }
67
+
68
+ cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
69
+ cache.init_app(app)
70
+
71
+ app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
72
+
73
+ # 确保全局变量在重新加载时不会丢失
74
+ if 'analyzer' not in globals():
75
+ try:
76
+ from stock_analyzer import StockAnalyzer
77
+
78
+ analyzer = StockAnalyzer()
79
+ print("成功初始化全局StockAnalyzer实例")
80
+ except Exception as e:
81
+ print(f"初始化StockAnalyzer时出错: {e}", file=sys.stderr)
82
+ raise
83
+
84
+ # 导入新模块
85
+ from fundamental_analyzer import FundamentalAnalyzer
86
+ from capital_flow_analyzer import CapitalFlowAnalyzer
87
+ from scenario_predictor import ScenarioPredictor
88
+ from stock_qa import StockQA
89
+ from risk_monitor import RiskMonitor
90
+ from index_industry_analyzer import IndexIndustryAnalyzer
91
+
92
+ # 初始化模块实例
93
+ fundamental_analyzer = FundamentalAnalyzer()
94
+ capital_flow_analyzer = CapitalFlowAnalyzer()
95
+ scenario_predictor = ScenarioPredictor(analyzer, os.getenv('OPENAI_API_KEY'), os.getenv('OPENAI_API_MODEL'))
96
+ stock_qa = StockQA(analyzer, os.getenv('OPENAI_API_KEY'), os.getenv('OPENAI_API_MODEL'))
97
+ risk_monitor = RiskMonitor(analyzer)
98
+ index_industry_analyzer = IndexIndustryAnalyzer(analyzer)
99
+ industry_analyzer = IndustryAnalyzer()
100
+
101
+ # 线程本地存储
102
+ thread_local = threading.local()
103
+
104
+
105
+ def get_analyzer():
106
+ """获取线程本地的分析器实例"""
107
+ # 如果线程本地存储中没有分析器实例,创建一个新的
108
+ if not hasattr(thread_local, 'analyzer'):
109
+ thread_local.analyzer = StockAnalyzer()
110
+ return thread_local.analyzer
111
+
112
+
113
+ # 配置日志
114
+ logging.basicConfig(level=logging.INFO)
115
+ handler = RotatingFileHandler('flask_app.log', maxBytes=10000000, backupCount=5)
116
+ handler.setFormatter(logging.Formatter(
117
+ '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
118
+ ))
119
+ app.logger.addHandler(handler)
120
+
121
+ # 扩展任务管理系统以支持不同类型的任务
122
+ task_types = {
123
+ 'scan': 'market_scan', # 市场扫描任务
124
+ 'analysis': 'stock_analysis' # 个股分析任务
125
+ }
126
+
127
+ # 任务数据存储
128
+ tasks = {
129
+ 'market_scan': {}, # 原来的scan_tasks
130
+ 'stock_analysis': {} # 新的个股分析任务
131
+ }
132
+
133
+
134
+ def get_task_store(task_type):
135
+ """获取指定类型的任务存储"""
136
+ return tasks.get(task_type, {})
137
+
138
+
139
+ def generate_task_key(task_type, **params):
140
+ """生成任务键"""
141
+ if task_type == 'stock_analysis':
142
+ # 对于个股分析,使用股票代码和市场类型作为键
143
+ return f"{params.get('stock_code')}_{params.get('market_type', 'A')}"
144
+ return None # 其他任务类型不使用预生成的键
145
+
146
+
147
+ def get_or_create_task(task_type, **params):
148
+ """获取或创建任务"""
149
+ store = get_task_store(task_type)
150
+ task_key = generate_task_key(task_type, **params)
151
+
152
+ # 检查是否有现有任务
153
+ if task_key and task_key in store:
154
+ task = store[task_key]
155
+ # 检查任务是否仍然有效
156
+ if task['status'] in [TASK_PENDING, TASK_RUNNING]:
157
+ return task['id'], task, False
158
+ if task['status'] == TASK_COMPLETED and 'result' in task:
159
+ # 任务已完成且有结果,重用它
160
+ return task['id'], task, False
161
+
162
+ # 创建新任务
163
+ task_id = generate_task_id()
164
+ task = {
165
+ 'id': task_id,
166
+ 'key': task_key, # 存储任务键以便以后查找
167
+ 'type': task_type,
168
+ 'status': TASK_PENDING,
169
+ 'progress': 0,
170
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
171
+ 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
172
+ 'params': params
173
+ }
174
+
175
+ with task_lock:
176
+ if task_key:
177
+ store[task_key] = task
178
+ store[task_id] = task
179
+
180
+ return task_id, task, True
181
+
182
+
183
+ # 添加到web_server.py顶部
184
+ # 任务管理系统
185
+ scan_tasks = {} # 存储扫描任务的状态和结果
186
+ task_lock = threading.Lock() # 用于线程安全操作
187
+
188
+ # 任务状态常量
189
+ TASK_PENDING = 'pending'
190
+ TASK_RUNNING = 'running'
191
+ TASK_COMPLETED = 'completed'
192
+ TASK_FAILED = 'failed'
193
+
194
+
195
+ def generate_task_id():
196
+ """生成唯一的任务ID"""
197
+ import uuid
198
+ return str(uuid.uuid4())
199
+
200
+
201
+ def start_market_scan_task_status(task_id, status, progress=None, result=None, error=None):
202
+ """更新任务状态 - 保持原有签名"""
203
+ with task_lock:
204
+ if task_id in scan_tasks:
205
+ task = scan_tasks[task_id]
206
+ task['status'] = status
207
+ if progress is not None:
208
+ task['progress'] = progress
209
+ if result is not None:
210
+ task['result'] = result
211
+ if error is not None:
212
+ task['error'] = error
213
+ task['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
214
+
215
+
216
+ def update_task_status(task_type, task_id, status, progress=None, result=None, error=None):
217
+ """更新任务状态"""
218
+ store = get_task_store(task_type)
219
+ with task_lock:
220
+ if task_id in store:
221
+ task = store[task_id]
222
+ task['status'] = status
223
+ if progress is not None:
224
+ task['progress'] = progress
225
+ if result is not None:
226
+ task['result'] = result
227
+ if error is not None:
228
+ task['error'] = error
229
+ task['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
230
+
231
+ # 更新键索引的任务
232
+ if 'key' in task and task['key'] in store:
233
+ store[task['key']] = task
234
+
235
+
236
+ analysis_tasks = {}
237
+
238
+
239
+ def get_or_create_analysis_task(stock_code, market_type='A'):
240
+ """获取或创建个股分析任务"""
241
+ # 创建一个键,用于查找现有任务
242
+ task_key = f"{stock_code}_{market_type}"
243
+
244
+ with task_lock:
245
+ # 检查是否有现有任务
246
+ for task_id, task in analysis_tasks.items():
247
+ if task.get('key') == task_key:
248
+ # 检查任务是否仍然有效
249
+ if task['status'] in [TASK_PENDING, TASK_RUNNING]:
250
+ return task_id, task, False
251
+ if task['status'] == TASK_COMPLETED and 'result' in task:
252
+ # 任务已完成且有结果,重用它
253
+ return task_id, task, False
254
+
255
+ # 创建新任务
256
+ task_id = generate_task_id()
257
+ task = {
258
+ 'id': task_id,
259
+ 'key': task_key,
260
+ 'status': TASK_PENDING,
261
+ 'progress': 0,
262
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
263
+ 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
264
+ 'params': {
265
+ 'stock_code': stock_code,
266
+ 'market_type': market_type
267
+ }
268
+ }
269
+
270
+ analysis_tasks[task_id] = task
271
+
272
+ return task_id, task, True
273
+
274
+
275
+ def update_analysis_task(task_id, status, progress=None, result=None, error=None):
276
+ """更新个股分析任务状态"""
277
+ with task_lock:
278
+ if task_id in analysis_tasks:
279
+ task = analysis_tasks[task_id]
280
+ task['status'] = status
281
+ if progress is not None:
282
+ task['progress'] = progress
283
+ if result is not None:
284
+ task['result'] = result
285
+ if error is not None:
286
+ task['error'] = error
287
+ task['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
288
+
289
+
290
+ # 定义自定义JSON编码器
291
+
292
+
293
+ # 在web_server.py中,更新convert_numpy_types函数以处理NaN值
294
+
295
+ # 将NumPy类型转换为Python原生类型的函数
296
+ def convert_numpy_types(obj):
297
+ """递归地将字典和列表中的NumPy类型转换为Python原生类型"""
298
+ try:
299
+ import numpy as np
300
+ import math
301
+
302
+ if isinstance(obj, dict):
303
+ return {key: convert_numpy_types(value) for key, value in obj.items()}
304
+ elif isinstance(obj, list):
305
+ return [convert_numpy_types(item) for item in obj]
306
+ elif isinstance(obj, np.integer):
307
+ return int(obj)
308
+ elif isinstance(obj, np.floating):
309
+ # Handle NaN and Infinity specifically
310
+ if np.isnan(obj):
311
+ return None
312
+ elif np.isinf(obj):
313
+ return None if obj < 0 else 1e308 # Use a very large number for +Infinity
314
+ return float(obj)
315
+ elif isinstance(obj, np.ndarray):
316
+ return obj.tolist()
317
+ elif isinstance(obj, np.bool_):
318
+ return bool(obj)
319
+ # Handle Python's own float NaN and Infinity
320
+ elif isinstance(obj, float):
321
+ if math.isnan(obj):
322
+ return None
323
+ elif math.isinf(obj):
324
+ return None
325
+ return obj
326
+ # 添加对date和datetime类型的处理
327
+ elif isinstance(obj, (date, datetime)):
328
+ return obj.isoformat()
329
+ else:
330
+ return obj
331
+ except ImportError:
332
+ # 如果没有安装numpy,但需要处理date和datetime
333
+ import math
334
+ if isinstance(obj, dict):
335
+ return {key: convert_numpy_types(value) for key, value in obj.items()}
336
+ elif isinstance(obj, list):
337
+ return [convert_numpy_types(item) for item in obj]
338
+ elif isinstance(obj, (date, datetime)):
339
+ return obj.isoformat()
340
+ # Handle Python's own float NaN and Infinity
341
+ elif isinstance(obj, float):
342
+ if math.isnan(obj):
343
+ return None
344
+ elif math.isinf(obj):
345
+ return None
346
+ return obj
347
+ return obj
348
+
349
+
350
+ # 同样更新 NumpyJSONEncoder 类
351
+ class NumpyJSONEncoder(json.JSONEncoder):
352
+ def default(self, obj):
353
+ # For NumPy data types
354
+ try:
355
+ import numpy as np
356
+ import math
357
+ if isinstance(obj, np.integer):
358
+ return int(obj)
359
+ elif isinstance(obj, np.floating):
360
+ # Handle NaN and Infinity specifically
361
+ if np.isnan(obj):
362
+ return None
363
+ elif np.isinf(obj):
364
+ return None
365
+ return float(obj)
366
+ elif isinstance(obj, np.ndarray):
367
+ return obj.tolist()
368
+ elif isinstance(obj, np.bool_):
369
+ return bool(obj)
370
+ # Handle Python's own float NaN and Infinity
371
+ elif isinstance(obj, float):
372
+ if math.isnan(obj):
373
+ return None
374
+ elif math.isinf(obj):
375
+ return None
376
+ return obj
377
+ except ImportError:
378
+ # Handle Python's own float NaN and Infinity if numpy is not available
379
+ import math
380
+ if isinstance(obj, float):
381
+ if math.isnan(obj):
382
+ return None
383
+ elif math.isinf(obj):
384
+ return None
385
+
386
+ # 添加对date和datetime类型的处理
387
+ if isinstance(obj, (date, datetime)):
388
+ return obj.isoformat()
389
+
390
+ return super(NumpyJSONEncoder, self).default(obj)
391
+
392
+
393
+ # 使用我们的编码器的自定义 jsonify 函数
394
+ def custom_jsonify(data):
395
+ return app.response_class(
396
+ json.dumps(convert_numpy_types(data), cls=NumpyJSONEncoder),
397
+ mimetype='application/json'
398
+ )
399
+
400
+
401
+ # 保持API兼容的路由
402
+ @app.route('/')
403
+ def index():
404
+ return render_template('index.html')
405
+
406
+
407
+ @app.route('/analyze', methods=['POST'])
408
+ def analyze():
409
+ try:
410
+ data = request.json
411
+ stock_codes = data.get('stock_codes', [])
412
+ market_type = data.get('market_type', 'A')
413
+
414
+ if not stock_codes:
415
+ return jsonify({'error': '请输入代码'}), 400
416
+
417
+ app.logger.info(f"分析股票请求: {stock_codes}, 市场类型: {market_type}")
418
+
419
+ # 设置最大处理时间,每只股票10秒
420
+ max_time_per_stock = 10 # 秒
421
+ max_total_time = max(30, min(60, len(stock_codes) * max_time_per_stock)) # 至少30秒,最多60秒
422
+
423
+ start_time = time.time()
424
+ results = []
425
+
426
+ for stock_code in stock_codes:
427
+ try:
428
+ # 检查是否已超时
429
+ if time.time() - start_time > max_total_time:
430
+ app.logger.warning(f"分析股票请求已超过{max_total_time}秒,提前返回已处理的{len(results)}只股票")
431
+ break
432
+
433
+ # 使用线程本地缓存的分析器实例
434
+ current_analyzer = get_analyzer()
435
+ result = current_analyzer.quick_analyze_stock(stock_code.strip(), market_type)
436
+
437
+ app.logger.info(
438
+ f"分析结果: 股票={stock_code}, 名称={result.get('stock_name', '未知')}, 行业={result.get('industry', '未知')}")
439
+ results.append(result)
440
+ except Exception as e:
441
+ app.logger.error(f"分析股票 {stock_code} 时出错: {str(e)}")
442
+ results.append({
443
+ 'stock_code': stock_code,
444
+ 'error': str(e),
445
+ 'stock_name': '分析失败',
446
+ 'industry': '未知'
447
+ })
448
+
449
+ return jsonify({'results': results})
450
+ except Exception as e:
451
+ app.logger.error(f"分析股票时出错: {traceback.format_exc()}")
452
+ return jsonify({'error': str(e)}), 500
453
+
454
+
455
+ @app.route('/api/north_flow_history', methods=['POST'])
456
+ def api_north_flow_history():
457
+ try:
458
+ data = request.json
459
+ stock_code = data.get('stock_code')
460
+ days = data.get('days', 10) # 默认为10天,对应前端的默认选项
461
+
462
+ # 计算 end_date 为当前时间
463
+ end_date = datetime.now().strftime('%Y%m%d')
464
+
465
+ # 计算 start_date 为 end_date 减去指定的天数
466
+ start_date = (datetime.now() - timedelta(days=int(days))).strftime('%Y%m%d')
467
+
468
+ if not stock_code:
469
+ return jsonify({'error': '请提供股票代码'}), 400
470
+
471
+ # 调用北向资金历史数据方法
472
+ from capital_flow_analyzer import CapitalFlowAnalyzer
473
+
474
+ analyzer = CapitalFlowAnalyzer()
475
+ result = analyzer.get_north_flow_history(stock_code, start_date, end_date)
476
+
477
+ return custom_jsonify(result)
478
+ except Exception as e:
479
+ app.logger.error(f"获取北向资金历史数据出错: {traceback.format_exc()}")
480
+ return jsonify({'error': str(e)}), 500
481
+
482
+
483
+ @app.route('/search_us_stocks', methods=['GET'])
484
+ def search_us_stocks():
485
+ try:
486
+ keyword = request.args.get('keyword', '')
487
+ if not keyword:
488
+ return jsonify({'error': '请输入搜索关键词'}), 400
489
+
490
+ results = us_stock_service.search_us_stocks(keyword)
491
+ return jsonify({'results': results})
492
+
493
+ except Exception as e:
494
+ app.logger.error(f"搜索美股代码时出错: {str(e)}")
495
+ return jsonify({'error': str(e)}), 500
496
+
497
+
498
+ # 新增可视化分析页面路由
499
+ @app.route('/dashboard')
500
+ def dashboard():
501
+ return render_template('dashboard.html')
502
+
503
+
504
+ @app.route('/stock_detail/<string:stock_code>')
505
+ def stock_detail(stock_code):
506
+ market_type = request.args.get('market_type', 'A')
507
+ return render_template('stock_detail.html', stock_code=stock_code, market_type=market_type)
508
+
509
+
510
+ @app.route('/portfolio')
511
+ def portfolio():
512
+ return render_template('portfolio.html')
513
+
514
+
515
+ @app.route('/market_scan')
516
+ def market_scan():
517
+ return render_template('market_scan.html')
518
+
519
+
520
+ # 基本面分析页面
521
+ @app.route('/fundamental')
522
+ def fundamental():
523
+ return render_template('fundamental.html')
524
+
525
+
526
+ # 资金流向页面
527
+ @app.route('/capital_flow')
528
+ def capital_flow():
529
+ return render_template('capital_flow.html')
530
+
531
+
532
+ # 情景预测页面
533
+ @app.route('/scenario_predict')
534
+ def scenario_predict():
535
+ return render_template('scenario_predict.html')
536
+
537
+
538
+ # 风险监控页面
539
+ @app.route('/risk_monitor')
540
+ def risk_monitor_page():
541
+ return render_template('risk_monitor.html')
542
+
543
+
544
+ # 智能问答页面
545
+ @app.route('/qa')
546
+ def qa_page():
547
+ return render_template('qa.html')
548
+
549
+
550
+ # 行业分析页面
551
+ @app.route('/industry_analysis')
552
+ def industry_analysis():
553
+ return render_template('industry_analysis.html')
554
+
555
+
556
+ def make_cache_key_with_stock():
557
+ """创建包含股票代码的自定义缓存键"""
558
+ path = request.path
559
+
560
+ # 从请求体中获取股票代码
561
+ stock_code = None
562
+ if request.is_json:
563
+ stock_code = request.json.get('stock_code')
564
+
565
+ # 构建包含股票代码的键
566
+ if stock_code:
567
+ return f"{path}_{stock_code}"
568
+ else:
569
+ return path
570
+
571
+
572
+ @app.route('/api/start_stock_analysis', methods=['POST'])
573
+ def start_stock_analysis():
574
+ """启动个股分析任务"""
575
+ try:
576
+ data = request.json
577
+ stock_code = data.get('stock_code')
578
+ market_type = data.get('market_type', 'A')
579
+
580
+ if not stock_code:
581
+ return jsonify({'error': '请输入股票代码'}), 400
582
+
583
+ app.logger.info(f"准备分析股票: {stock_code}")
584
+
585
+ # 获取或创建任务
586
+ task_id, task, is_new = get_or_create_task(
587
+ 'stock_analysis',
588
+ stock_code=stock_code,
589
+ market_type=market_type
590
+ )
591
+
592
+ # 如果是已完成的任务,直接返回结果
593
+ if task['status'] == TASK_COMPLETED and 'result' in task:
594
+ app.logger.info(f"使用缓存的分析结果: {stock_code}")
595
+ return jsonify({
596
+ 'task_id': task_id,
597
+ 'status': task['status'],
598
+ 'result': task['result']
599
+ })
600
+
601
+ # 如果是新创建的任务,启动后台处理
602
+ if is_new:
603
+ app.logger.info(f"创建新的分析任务: {task_id}")
604
+
605
+ # 启动后台线程执行分析
606
+ def run_analysis():
607
+ try:
608
+ update_task_status('stock_analysis', task_id, TASK_RUNNING, progress=10)
609
+
610
+ # 执行分析
611
+ result = analyzer.perform_enhanced_analysis(stock_code, market_type)
612
+
613
+ # 更新任务状态为完成
614
+ update_task_status('stock_analysis', task_id, TASK_COMPLETED, progress=100, result=result)
615
+ app.logger.info(f"分析任务 {task_id} 完成")
616
+
617
+ except Exception as e:
618
+ app.logger.error(f"分析任务 {task_id} 失败: {str(e)}")
619
+ app.logger.error(traceback.format_exc())
620
+ update_task_status('stock_analysis', task_id, TASK_FAILED, error=str(e))
621
+
622
+ # 启动后台线程
623
+ thread = threading.Thread(target=run_analysis)
624
+ thread.daemon = True
625
+ thread.start()
626
+
627
+ # 返回任务ID和状态
628
+ return jsonify({
629
+ 'task_id': task_id,
630
+ 'status': task['status'],
631
+ 'message': f'已启动分析任务: {stock_code}'
632
+ })
633
+
634
+ except Exception as e:
635
+ app.logger.error(f"启动个股分析任务时出错: {traceback.format_exc()}")
636
+ return jsonify({'error': str(e)}), 500
637
+
638
+
639
+ @app.route('/api/analysis_status/<task_id>', methods=['GET'])
640
+ def get_analysis_status(task_id):
641
+ """获取个股分析任务状态"""
642
+ store = get_task_store('stock_analysis')
643
+ with task_lock:
644
+ if task_id not in store:
645
+ return jsonify({'error': '找不到指定的分析任务'}), 404
646
+
647
+ task = store[task_id]
648
+
649
+ # 基本状态信息
650
+ status = {
651
+ 'id': task['id'],
652
+ 'status': task['status'],
653
+ 'progress': task.get('progress', 0),
654
+ 'created_at': task['created_at'],
655
+ 'updated_at': task['updated_at']
656
+ }
657
+
658
+ # 如果任务完成,包含结果
659
+ if task['status'] == TASK_COMPLETED and 'result' in task:
660
+ status['result'] = task['result']
661
+
662
+ # 如果任务失败,包含错误信息
663
+ if task['status'] == TASK_FAILED and 'error' in task:
664
+ status['error'] = task['error']
665
+
666
+ return custom_jsonify(status)
667
+
668
+
669
+ @app.route('/api/cancel_analysis/<task_id>', methods=['POST'])
670
+ def cancel_analysis(task_id):
671
+ """取消个股分析任务"""
672
+ store = get_task_store('stock_analysis')
673
+ with task_lock:
674
+ if task_id not in store:
675
+ return jsonify({'error': '找不到指定的分析任务'}), 404
676
+
677
+ task = store[task_id]
678
+
679
+ if task['status'] in [TASK_COMPLETED, TASK_FAILED]:
680
+ return jsonify({'message': '任务已完成或失败,无法取消'})
681
+
682
+ # 更新状态为失败
683
+ task['status'] = TASK_FAILED
684
+ task['error'] = '用户取消任务'
685
+ task['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
686
+
687
+ # 更新键索引的任务
688
+ if 'key' in task and task['key'] in store:
689
+ store[task['key']] = task
690
+
691
+ return jsonify({'message': '任务已取消'})
692
+
693
+
694
+ # 保留原有API用于向后兼容
695
+ @app.route('/api/enhanced_analysis', methods=['POST'])
696
+ def enhanced_analysis():
697
+ """原增强分析API的向后兼容版本"""
698
+ try:
699
+ data = request.json
700
+ stock_code = data.get('stock_code')
701
+ market_type = data.get('market_type', 'A')
702
+
703
+ if not stock_code:
704
+ return custom_jsonify({'error': '请输入股票代码'}), 400
705
+
706
+ # 调用新的任务系统,但模拟同步行为
707
+ # 这会导致和之前一样的超时问题,但保持兼容
708
+ timeout = 300
709
+ start_time = time.time()
710
+
711
+ # 获取或创建任务
712
+ task_id, task, is_new = get_or_create_task(
713
+ 'stock_analysis',
714
+ stock_code=stock_code,
715
+ market_type=market_type
716
+ )
717
+
718
+ # 如果是已完成的任务,直接返回结果
719
+ if task['status'] == TASK_COMPLETED and 'result' in task:
720
+ app.logger.info(f"使用缓存的分析结果: {stock_code}")
721
+ return custom_jsonify({'result': task['result']})
722
+
723
+ # 启动分析(如果是新任务)
724
+ if is_new:
725
+ # 同步执行分析
726
+ try:
727
+ result = analyzer.perform_enhanced_analysis(stock_code, market_type)
728
+ update_task_status('stock_analysis', task_id, TASK_COMPLETED, progress=100, result=result)
729
+ app.logger.info(f"分析完成: {stock_code},耗时 {time.time() - start_time:.2f} 秒")
730
+ return custom_jsonify({'result': result})
731
+ except Exception as e:
732
+ app.logger.error(f"分析过程中出错: {str(e)}")
733
+ update_task_status('stock_analysis', task_id, TASK_FAILED, error=str(e))
734
+ return custom_jsonify({'error': f'分析过程中出错: {str(e)}'}), 500
735
+ else:
736
+ # 已存在正在处理��任务,等待其完成
737
+ max_wait = timeout - (time.time() - start_time)
738
+ wait_interval = 0.5
739
+ waited = 0
740
+
741
+ while waited < max_wait:
742
+ with task_lock:
743
+ current_task = store[task_id]
744
+ if current_task['status'] == TASK_COMPLETED and 'result' in current_task:
745
+ return custom_jsonify({'result': current_task['result']})
746
+ if current_task['status'] == TASK_FAILED:
747
+ error = current_task.get('error', '任务失败,无详细信息')
748
+ return custom_jsonify({'error': error}), 500
749
+
750
+ time.sleep(wait_interval)
751
+ waited += wait_interval
752
+
753
+ # 超时
754
+ return custom_jsonify({'error': '处理超时,请稍后重试'}), 504
755
+
756
+ except Exception as e:
757
+ app.logger.error(f"执行增强版分析时出错: {traceback.format_exc()}")
758
+ return custom_jsonify({'error': str(e)}), 500
759
+
760
+
761
+ # 添加在web_server.py主代码中
762
+ @app.errorhandler(404)
763
+ def not_found(error):
764
+ """处理404错误"""
765
+ if request.path.startswith('/api/'):
766
+ # 为API请求返回JSON格式的错误
767
+ return jsonify({
768
+ 'error': '找不到请求的API端点',
769
+ 'path': request.path,
770
+ 'method': request.method
771
+ }), 404
772
+ # 为网页请求返回HTML错误页
773
+ return render_template('error.html', error_code=404, message="找不到请求的页面"), 404
774
+
775
+
776
+ @app.errorhandler(500)
777
+ def server_error(error):
778
+ """处理500错误"""
779
+ app.logger.error(f"服务器错误: {str(error)}")
780
+ if request.path.startswith('/api/'):
781
+ # 为API请求返回JSON格式的错误
782
+ return jsonify({
783
+ 'error': '服务器内部错误',
784
+ 'message': str(error)
785
+ }), 500
786
+ # 为网页请求返回HTML错误页
787
+ return render_template('error.html', error_code=500, message="服务器内部错误"), 500
788
+
789
+
790
+ # Update the get_stock_data function in web_server.py to handle date formatting properly
791
+ @app.route('/api/stock_data', methods=['GET'])
792
+ @cache.cached(timeout=300, query_string=True)
793
+ def get_stock_data():
794
+ try:
795
+ stock_code = request.args.get('stock_code')
796
+ market_type = request.args.get('market_type', 'A')
797
+ period = request.args.get('period', '1y') # 默认1年
798
+
799
+ if not stock_code:
800
+ return custom_jsonify({'error': '请提供股票代码'}), 400
801
+
802
+ # 根据period计算start_date
803
+ end_date = datetime.now().strftime('%Y%m%d')
804
+ if period == '1m':
805
+ start_date = (datetime.now() - timedelta(days=30)).strftime('%Y%m%d')
806
+ elif period == '3m':
807
+ start_date = (datetime.now() - timedelta(days=90)).strftime('%Y%m%d')
808
+ elif period == '6m':
809
+ start_date = (datetime.now() - timedelta(days=180)).strftime('%Y%m%d')
810
+ elif period == '1y':
811
+ start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
812
+ else:
813
+ start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
814
+
815
+ # 获取股票历史数据
816
+ app.logger.info(
817
+ f"获取股票 {stock_code} 的历史数据,市场: {market_type}, 起始日期: {start_date}, 结束日期: {end_date}")
818
+ df = analyzer.get_stock_data(stock_code, market_type, start_date, end_date)
819
+
820
+ # 计算技术指标
821
+ app.logger.info(f"计算股票 {stock_code} 的技术指标")
822
+ df = analyzer.calculate_indicators(df)
823
+
824
+ # 检查数据是否为空
825
+ if df.empty:
826
+ app.logger.warning(f"股票 {stock_code} 的数据为空")
827
+ return custom_jsonify({'error': '未找到股票数据'}), 404
828
+
829
+ # 将DataFrame转为JSON格式
830
+ app.logger.info(f"将数据转换为JSON格式,行数: {len(df)}")
831
+
832
+ # 确保日期列是字符串格式 - 修复缓存问题
833
+ if 'date' in df.columns:
834
+ try:
835
+ if pd.api.types.is_datetime64_any_dtype(df['date']):
836
+ df['date'] = df['date'].dt.strftime('%Y-%m-%d')
837
+ else:
838
+ df = df.copy()
839
+ df['date'] = pd.to_datetime(df['date'], errors='coerce')
840
+ df['date'] = df['date'].dt.strftime('%Y-%m-%d')
841
+ except Exception as e:
842
+ app.logger.error(f"处理日期列时出错: {str(e)}")
843
+ df['date'] = df['date'].astype(str)
844
+
845
+ # 将NaN值替换为None
846
+ df = df.replace({np.nan: None, np.inf: None, -np.inf: None})
847
+
848
+ records = df.to_dict('records')
849
+
850
+ app.logger.info(f"数据处理完成,返回 {len(records)} 条记录")
851
+ return custom_jsonify({'data': records})
852
+ except Exception as e:
853
+ app.logger.error(f"获取股票数据时出错: {str(e)}")
854
+ app.logger.error(traceback.format_exc())
855
+ return custom_jsonify({'error': str(e)}), 500
856
+
857
+
858
+ # @app.route('/api/market_scan', methods=['POST'])
859
+ # def api_market_scan():
860
+ # try:
861
+ # data = request.json
862
+ # stock_list = data.get('stock_list', [])
863
+ # min_score = data.get('min_score', 60)
864
+ # market_type = data.get('market_type', 'A')
865
+
866
+ # if not stock_list:
867
+ # return jsonify({'error': '请提供股票列表'}), 400
868
+
869
+ # # 限制股票数量,避免过长处理时间
870
+ # if len(stock_list) > 100:
871
+ # app.logger.warning(f"股票列表过长 ({len(stock_list)}只),截取前100只")
872
+ # stock_list = stock_list[:100]
873
+
874
+ # # 执行市场扫描
875
+ # app.logger.info(f"开始扫描 {len(stock_list)} 只股票,最低分数: {min_score}")
876
+
877
+ # # 使用线程池优化处理
878
+ # results = []
879
+ # max_workers = min(10, len(stock_list)) # 最多10个工作线程
880
+
881
+ # # 设置较长的超时时间
882
+ # timeout = 300 # 5分钟
883
+
884
+ # def scan_thread():
885
+ # try:
886
+ # return analyzer.scan_market(stock_list, min_score, market_type)
887
+ # except Exception as e:
888
+ # app.logger.error(f"扫描线程出错: {str(e)}")
889
+ # return []
890
+
891
+ # thread = threading.Thread(target=lambda: results.append(scan_thread()))
892
+ # thread.start()
893
+ # thread.join(timeout)
894
+
895
+ # if thread.is_alive():
896
+ # app.logger.error(f"市场扫描超时,已扫描 {len(stock_list)} 只股票超过 {timeout} 秒")
897
+ # return custom_jsonify({'error': '扫描超时,请减少股票数量或稍后再试'}), 504
898
+
899
+ # if not results or not results[0]:
900
+ # app.logger.warning("扫描结果为空")
901
+ # return custom_jsonify({'results': []})
902
+
903
+ # scan_results = results[0]
904
+ # app.logger.info(f"扫描完成,找到 {len(scan_results)} 只符合条件的股票")
905
+
906
+ # # 使用自定义JSON格式处理NumPy数据类型
907
+ # return custom_jsonify({'results': scan_results})
908
+ # except Exception as e:
909
+ # app.logger.error(f"执行市场扫描时出错: {traceback.format_exc()}")
910
+ # return custom_jsonify({'error': str(e)}), 500
911
+
912
+ @app.route('/api/start_market_scan', methods=['POST'])
913
+ def start_market_scan():
914
+ """启动市场扫描任务"""
915
+ try:
916
+ data = request.json
917
+ stock_list = data.get('stock_list', [])
918
+ min_score = data.get('min_score', 60)
919
+ market_type = data.get('market_type', 'A')
920
+
921
+ if not stock_list:
922
+ return jsonify({'error': '请提供股票列表'}), 400
923
+
924
+ # 限制股票数量,避免过长处理时间
925
+ if len(stock_list) > 100:
926
+ app.logger.warning(f"股票列表过长 ({len(stock_list)}只),截取前100只")
927
+ stock_list = stock_list[:100]
928
+
929
+ # 创建新任务
930
+ task_id = generate_task_id()
931
+ task = {
932
+ 'id': task_id,
933
+ 'status': TASK_PENDING,
934
+ 'progress': 0,
935
+ 'total': len(stock_list),
936
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
937
+ 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
938
+ 'params': {
939
+ 'stock_list': stock_list,
940
+ 'min_score': min_score,
941
+ 'market_type': market_type
942
+ }
943
+ }
944
+
945
+ with task_lock:
946
+ scan_tasks[task_id] = task
947
+
948
+ # 启动后台线程执行扫描
949
+ def run_scan():
950
+ try:
951
+ start_market_scan_task_status(task_id, TASK_RUNNING)
952
+
953
+ # 执行分批处理
954
+ results = []
955
+ total = len(stock_list)
956
+ batch_size = 10
957
+
958
+ for i in range(0, total, batch_size):
959
+ if task_id not in scan_tasks or scan_tasks[task_id]['status'] != TASK_RUNNING:
960
+ # 任务被取消
961
+ app.logger.info(f"扫描任务 {task_id} 被取消")
962
+ return
963
+
964
+ batch = stock_list[i:i + batch_size]
965
+ batch_results = []
966
+
967
+ for stock_code in batch:
968
+ try:
969
+ report = analyzer.quick_analyze_stock(stock_code, market_type)
970
+ if report['score'] >= min_score:
971
+ batch_results.append(report)
972
+ except Exception as e:
973
+ app.logger.error(f"分析股票 {stock_code} 时出错: {str(e)}")
974
+ continue
975
+
976
+ results.extend(batch_results)
977
+
978
+ # 更新进度
979
+ progress = min(100, int((i + len(batch)) / total * 100))
980
+ start_market_scan_task_status(task_id, TASK_RUNNING, progress=progress)
981
+
982
+ # 按得分排序
983
+ results.sort(key=lambda x: x['score'], reverse=True)
984
+
985
+ # 更新任务状态为完成
986
+ start_market_scan_task_status(task_id, TASK_COMPLETED, progress=100, result=results)
987
+ app.logger.info(f"扫描任务 {task_id} 完成,找到 {len(results)} 只符合条件的股票")
988
+
989
+ except Exception as e:
990
+ app.logger.error(f"扫描任务 {task_id} 失败: {str(e)}")
991
+ app.logger.error(traceback.format_exc())
992
+ start_market_scan_task_status(task_id, TASK_FAILED, error=str(e))
993
+
994
+ # 启动后台线程
995
+ thread = threading.Thread(target=run_scan)
996
+ thread.daemon = True
997
+ thread.start()
998
+
999
+ return jsonify({
1000
+ 'task_id': task_id,
1001
+ 'status': 'pending',
1002
+ 'message': f'已启动扫描任务,正在处理 {len(stock_list)} 只股票'
1003
+ })
1004
+
1005
+ except Exception as e:
1006
+ app.logger.error(f"启动市场扫描任务时出错: {traceback.format_exc()}")
1007
+ return jsonify({'error': str(e)}), 500
1008
+
1009
+
1010
+ @app.route('/api/scan_status/<task_id>', methods=['GET'])
1011
+ def get_scan_status(task_id):
1012
+ """获取扫描任务状态"""
1013
+ with task_lock:
1014
+ if task_id not in scan_tasks:
1015
+ return jsonify({'error': '找不到指定的扫描任务'}), 404
1016
+
1017
+ task = scan_tasks[task_id]
1018
+
1019
+ # 基本状态信息
1020
+ status = {
1021
+ 'id': task['id'],
1022
+ 'status': task['status'],
1023
+ 'progress': task.get('progress', 0),
1024
+ 'total': task.get('total', 0),
1025
+ 'created_at': task['created_at'],
1026
+ 'updated_at': task['updated_at']
1027
+ }
1028
+
1029
+ # 如果任务完成,包含结果
1030
+ if task['status'] == TASK_COMPLETED and 'result' in task:
1031
+ status['result'] = task['result']
1032
+
1033
+ # 如果任务失败,包含错误信息
1034
+ if task['status'] == TASK_FAILED and 'error' in task:
1035
+ status['error'] = task['error']
1036
+
1037
+ return custom_jsonify(status)
1038
+
1039
+
1040
+ @app.route('/api/cancel_scan/<task_id>', methods=['POST'])
1041
+ def cancel_scan(task_id):
1042
+ """取消扫描任务"""
1043
+ with task_lock:
1044
+ if task_id not in scan_tasks:
1045
+ return jsonify({'error': '找不到指定的扫描任务'}), 404
1046
+
1047
+ task = scan_tasks[task_id]
1048
+
1049
+ if task['status'] in [TASK_COMPLETED, TASK_FAILED]:
1050
+ return jsonify({'message': '任务已完成或失败,无法取消'})
1051
+
1052
+ # 更新状态为失败
1053
+ task['status'] = TASK_FAILED
1054
+ task['error'] = '用户取消任务'
1055
+ task['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1056
+
1057
+ return jsonify({'message': '任务已取消'})
1058
+
1059
+
1060
+ @app.route('/api/index_stocks', methods=['GET'])
1061
+ def get_index_stocks():
1062
+ """获取指数成分股"""
1063
+ try:
1064
+ import akshare as ak
1065
+ index_code = request.args.get('index_code', '000300') # 默认沪深300
1066
+
1067
+ # 获取指数成分股
1068
+ app.logger.info(f"获取指数 {index_code} 成分股")
1069
+ if index_code == '000300':
1070
+ # 沪深300成分股
1071
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000300")
1072
+ elif index_code == '000905':
1073
+ # 中证500成分股
1074
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000905")
1075
+ elif index_code == '000852':
1076
+ # 中证1000成分股
1077
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000852")
1078
+ elif index_code == '000001':
1079
+ # 上证指数
1080
+ stocks = ak.index_stock_cons_weight_csindex(symbol="000001")
1081
+ else:
1082
+ return jsonify({'error': '不支持的指数代码'}), 400
1083
+
1084
+ # 提取股票代码列表
1085
+ stock_list = stocks['成分券代码'].tolist() if '成分券代码' in stocks.columns else []
1086
+ app.logger.info(f"找到 {len(stock_list)} 只成分股")
1087
+
1088
+ return jsonify({'stock_list': stock_list})
1089
+ except Exception as e:
1090
+ app.logger.error(f"获取指数成分股时出错: {traceback.format_exc()}")
1091
+ return jsonify({'error': str(e)}), 500
1092
+
1093
+
1094
+ @app.route('/api/industry_stocks', methods=['GET'])
1095
+ def get_industry_stocks():
1096
+ """获取行业成分股"""
1097
+ try:
1098
+ import akshare as ak
1099
+ industry = request.args.get('industry', '')
1100
+
1101
+ if not industry:
1102
+ return jsonify({'error': '请提供行业名称'}), 400
1103
+
1104
+ # 获取行业成分股
1105
+ app.logger.info(f"获取 {industry} 行业成分股")
1106
+ stocks = ak.stock_board_industry_cons_em(symbol=industry)
1107
+
1108
+ # 提取股票代码列表
1109
+ stock_list = stocks['代码'].tolist() if '代码' in stocks.columns else []
1110
+ app.logger.info(f"找到 {len(stock_list)} 只 {industry} 行业股票")
1111
+
1112
+ return jsonify({'stock_list': stock_list})
1113
+ except Exception as e:
1114
+ app.logger.error(f"获取行业成分股时出错: {traceback.format_exc()}")
1115
+ return jsonify({'error': str(e)}), 500
1116
+
1117
+
1118
+ # 添加到web_server.py
1119
+ def clean_old_tasks():
1120
+ """清理旧的扫描任务"""
1121
+ with task_lock:
1122
+ now = datetime.now()
1123
+ to_delete = []
1124
+
1125
+ for task_id, task in scan_tasks.items():
1126
+ # 解析更新时间
1127
+ try:
1128
+ updated_at = datetime.strptime(task['updated_at'], '%Y-%m-%d %H:%M:%S')
1129
+ # 如果任务完成或失败且超过1小时,或者任务状态异常且超过3小时,清理它
1130
+ if ((task['status'] in [TASK_COMPLETED, TASK_FAILED] and
1131
+ (now - updated_at).total_seconds() > 3600) or
1132
+ ((now - updated_at).total_seconds() > 10800)):
1133
+ to_delete.append(task_id)
1134
+ except:
1135
+ # 日期解析错误,添加到删除列表
1136
+ to_delete.append(task_id)
1137
+
1138
+ # 删除旧任务
1139
+ for task_id in to_delete:
1140
+ del scan_tasks[task_id]
1141
+
1142
+ return len(to_delete)
1143
+
1144
+
1145
+ # 修改 run_task_cleaner 函数,使其每 5 分钟运行一次并在 16:30 左右清理所有缓存
1146
+ def run_task_cleaner():
1147
+ """定期运行任务清理,并在每天 16:30 左右清理所有缓存"""
1148
+ while True:
1149
+ try:
1150
+ now = datetime.now()
1151
+ # 判断是否在收盘时间附近(16:25-16:35)
1152
+ is_market_close_time = (now.hour == 16 and 25 <= now.minute <= 35)
1153
+
1154
+ cleaned = clean_old_tasks()
1155
+
1156
+ # 如果是收盘时间,清理所有缓存
1157
+ if is_market_close_time:
1158
+ # 清理分析器的数据缓存
1159
+ analyzer.data_cache.clear()
1160
+
1161
+ # 清理 Flask 缓存
1162
+ cache.clear()
1163
+
1164
+ # 清理任务存储
1165
+ with task_lock:
1166
+ for task_type in tasks:
1167
+ task_store = tasks[task_type]
1168
+ completed_tasks = [task_id for task_id, task in task_store.items()
1169
+ if task['status'] == TASK_COMPLETED]
1170
+ for task_id in completed_tasks:
1171
+ del task_store[task_id]
1172
+
1173
+ app.logger.info("市场收盘时间检测到,已清理所有缓存数据")
1174
+
1175
+ if cleaned > 0:
1176
+ app.logger.info(f"清理了 {cleaned} 个旧的扫描任务")
1177
+ except Exception as e:
1178
+ app.logger.error(f"任务清理出错: {str(e)}")
1179
+
1180
+ # 每 5 分钟运行一次,而不是每小时
1181
+ time.sleep(600)
1182
+
1183
+
1184
+ # 基本面分析路由
1185
+ @app.route('/api/fundamental_analysis', methods=['POST'])
1186
+ def api_fundamental_analysis():
1187
+ try:
1188
+ data = request.json
1189
+ stock_code = data.get('stock_code')
1190
+
1191
+ if not stock_code:
1192
+ return jsonify({'error': '请提供股票代码'}), 400
1193
+
1194
+ # 获取基本面分析结果
1195
+ result = fundamental_analyzer.calculate_fundamental_score(stock_code)
1196
+
1197
+ return custom_jsonify(result)
1198
+ except Exception as e:
1199
+ app.logger.error(f"基本面分析出错: {traceback.format_exc()}")
1200
+ return jsonify({'error': str(e)}), 500
1201
+
1202
+
1203
+ # 资金流向分析路由
1204
+ # Add to web_server.py
1205
+
1206
+ # 获取概念资金流向的API端点
1207
+ @app.route('/api/concept_fund_flow', methods=['GET'])
1208
+ def api_concept_fund_flow():
1209
+ try:
1210
+ period = request.args.get('period', '10日排行') # Default to 10-day ranking
1211
+
1212
+ # Get concept fund flow data
1213
+ result = capital_flow_analyzer.get_concept_fund_flow(period)
1214
+
1215
+ return custom_jsonify(result)
1216
+ except Exception as e:
1217
+ app.logger.error(f"Error getting concept fund flow: {traceback.format_exc()}")
1218
+ return jsonify({'error': str(e)}), 500
1219
+
1220
+
1221
+ # 获取个股资金流向排名的API端点
1222
+ @app.route('/api/individual_fund_flow_rank', methods=['GET'])
1223
+ def api_individual_fund_flow_rank():
1224
+ try:
1225
+ period = request.args.get('period', '10日') # Default to today
1226
+
1227
+ # Get individual fund flow ranking data
1228
+ result = capital_flow_analyzer.get_individual_fund_flow_rank(period)
1229
+
1230
+ return custom_jsonify(result)
1231
+ except Exception as e:
1232
+ app.logger.error(f"Error getting individual fund flow ranking: {traceback.format_exc()}")
1233
+ return jsonify({'error': str(e)}), 500
1234
+
1235
+
1236
+ # 获取个股资金流向的API端点
1237
+ @app.route('/api/individual_fund_flow', methods=['GET'])
1238
+ def api_individual_fund_flow():
1239
+ try:
1240
+ stock_code = request.args.get('stock_code')
1241
+ market_type = request.args.get('market_type', '') # Auto-detect if not provided
1242
+ re_date = request.args.get('period-select')
1243
+
1244
+ if not stock_code:
1245
+ return jsonify({'error': 'Stock code is required'}), 400
1246
+
1247
+ # Get individual fund flow data
1248
+ result = capital_flow_analyzer.get_individual_fund_flow(stock_code, market_type, re_date)
1249
+ return custom_jsonify(result)
1250
+ except Exception as e:
1251
+ app.logger.error(f"Error getting individual fund flow: {traceback.format_exc()}")
1252
+ return jsonify({'error': str(e)}), 500
1253
+
1254
+
1255
+ # 获取板块内股票的API端点
1256
+ @app.route('/api/sector_stocks', methods=['GET'])
1257
+ def api_sector_stocks():
1258
+ try:
1259
+ sector = request.args.get('sector')
1260
+
1261
+ if not sector:
1262
+ return jsonify({'error': 'Sector name is required'}), 400
1263
+
1264
+ # Get sector stocks data
1265
+ result = capital_flow_analyzer.get_sector_stocks(sector)
1266
+
1267
+ return custom_jsonify(result)
1268
+ except Exception as e:
1269
+ app.logger.error(f"Error getting sector stocks: {traceback.format_exc()}")
1270
+ return jsonify({'error': str(e)}), 500
1271
+
1272
+
1273
+ # Update the existing capital flow API endpoint
1274
+ @app.route('/api/capital_flow', methods=['POST'])
1275
+ def api_capital_flow():
1276
+ try:
1277
+ data = request.json
1278
+ stock_code = data.get('stock_code')
1279
+ market_type = data.get('market_type', '') # Auto-detect if not provided
1280
+
1281
+ if not stock_code:
1282
+ return jsonify({'error': 'Stock code is required'}), 400
1283
+
1284
+ # Calculate capital flow score
1285
+ result = capital_flow_analyzer.calculate_capital_flow_score(stock_code, market_type)
1286
+
1287
+ return custom_jsonify(result)
1288
+ except Exception as e:
1289
+ app.logger.error(f"Error calculating capital flow score: {traceback.format_exc()}")
1290
+ return jsonify({'error': str(e)}), 500
1291
+
1292
+
1293
+ # 情景预测路由
1294
+ @app.route('/api/scenario_predict', methods=['POST'])
1295
+ def api_scenario_predict():
1296
+ try:
1297
+ data = request.json
1298
+ stock_code = data.get('stock_code')
1299
+ market_type = data.get('market_type', 'A')
1300
+ days = data.get('days', 60)
1301
+
1302
+ if not stock_code:
1303
+ return jsonify({'error': '请提供股票代码'}), 400
1304
+
1305
+ # 获取情景预测结果
1306
+ result = scenario_predictor.generate_scenarios(stock_code, market_type, days)
1307
+
1308
+ return custom_jsonify(result)
1309
+ except Exception as e:
1310
+ app.logger.error(f"情景预测出错: {traceback.format_exc()}")
1311
+ return jsonify({'error': str(e)}), 500
1312
+
1313
+
1314
+ # 智能问答路由
1315
+ @app.route('/api/qa', methods=['POST'])
1316
+ def api_qa():
1317
+ try:
1318
+ data = request.json
1319
+ stock_code = data.get('stock_code')
1320
+ question = data.get('question')
1321
+ market_type = data.get('market_type', 'A')
1322
+
1323
+ if not stock_code or not question:
1324
+ return jsonify({'error': '请提供股票代码和问题'}), 400
1325
+
1326
+ # 获取智能问答结果
1327
+ result = stock_qa.answer_question(stock_code, question, market_type)
1328
+
1329
+ return custom_jsonify(result)
1330
+ except Exception as e:
1331
+ app.logger.error(f"智能问答出错: {traceback.format_exc()}")
1332
+ return jsonify({'error': str(e)}), 500
1333
+
1334
+
1335
+ # 风险分析路由
1336
+ @app.route('/api/risk_analysis', methods=['POST'])
1337
+ def api_risk_analysis():
1338
+ try:
1339
+ data = request.json
1340
+ stock_code = data.get('stock_code')
1341
+ market_type = data.get('market_type', 'A')
1342
+
1343
+ if not stock_code:
1344
+ return jsonify({'error': '请提供股票代码'}), 400
1345
+
1346
+ # 获取风险分析结果
1347
+ result = risk_monitor.analyze_stock_risk(stock_code, market_type)
1348
+
1349
+ return custom_jsonify(result)
1350
+ except Exception as e:
1351
+ app.logger.error(f"风险分析出错: {traceback.format_exc()}")
1352
+ return jsonify({'error': str(e)}), 500
1353
+
1354
+
1355
+ # 投资组合风险分析路由
1356
+ @app.route('/api/portfolio_risk', methods=['POST'])
1357
+ def api_portfolio_risk():
1358
+ try:
1359
+ data = request.json
1360
+ portfolio = data.get('portfolio', [])
1361
+
1362
+ if not portfolio:
1363
+ return jsonify({'error': '请提供投资组合'}), 400
1364
+
1365
+ # 获取投资组合风险分析结果
1366
+ result = risk_monitor.analyze_portfolio_risk(portfolio)
1367
+
1368
+ return custom_jsonify(result)
1369
+ except Exception as e:
1370
+ app.logger.error(f"投资组合风险分析出错: {traceback.format_exc()}")
1371
+ return jsonify({'error': str(e)}), 500
1372
+
1373
+
1374
+ # 指数分析路由
1375
+ @app.route('/api/index_analysis', methods=['GET'])
1376
+ def api_index_analysis():
1377
+ try:
1378
+ index_code = request.args.get('index_code')
1379
+ limit = int(request.args.get('limit', 30))
1380
+
1381
+ if not index_code:
1382
+ return jsonify({'error': '请提供指数代码'}), 400
1383
+
1384
+ # 获取指数分析结果
1385
+ result = index_industry_analyzer.analyze_index(index_code, limit)
1386
+
1387
+ return custom_jsonify(result)
1388
+ except Exception as e:
1389
+ app.logger.error(f"指数分析出错: {traceback.format_exc()}")
1390
+ return jsonify({'error': str(e)}), 500
1391
+
1392
+
1393
+ # 行业分析路由
1394
+ @app.route('/api/industry_analysis', methods=['GET'])
1395
+ def api_industry_analysis():
1396
+ try:
1397
+ industry = request.args.get('industry')
1398
+ limit = int(request.args.get('limit', 30))
1399
+
1400
+ if not industry:
1401
+ return jsonify({'error': '请提供行业名称'}), 400
1402
+
1403
+ # 获取行业分析结果
1404
+ result = index_industry_analyzer.analyze_industry(industry, limit)
1405
+
1406
+ return custom_jsonify(result)
1407
+ except Exception as e:
1408
+ app.logger.error(f"行业分析出错: {traceback.format_exc()}")
1409
+ return jsonify({'error': str(e)}), 500
1410
+
1411
+
1412
+ @app.route('/api/industry_fund_flow', methods=['GET'])
1413
+ def api_industry_fund_flow():
1414
+ """获取行业资金流向数据"""
1415
+ try:
1416
+ symbol = request.args.get('symbol', '即时')
1417
+
1418
+ result = industry_analyzer.get_industry_fund_flow(symbol)
1419
+
1420
+ return custom_jsonify(result)
1421
+ except Exception as e:
1422
+ app.logger.error(f"获取行业资金流向数据出错: {traceback.format_exc()}")
1423
+ return jsonify({'error': str(e)}), 500
1424
+
1425
+
1426
+ @app.route('/api/industry_detail', methods=['GET'])
1427
+ def api_industry_detail():
1428
+ """获取行业详细信息"""
1429
+ try:
1430
+ industry = request.args.get('industry')
1431
+
1432
+ if not industry:
1433
+ return jsonify({'error': '请提供行业名称'}), 400
1434
+
1435
+ result = industry_analyzer.get_industry_detail(industry)
1436
+
1437
+ app.logger.info(f"返回前 (result):{result}")
1438
+ if not result:
1439
+ return jsonify({'error': f'未找到行业 {industry} 的详细信息'}), 404
1440
+
1441
+ return custom_jsonify(result)
1442
+ except Exception as e:
1443
+ app.logger.error(f"获取行业详细信息出错: {traceback.format_exc()}")
1444
+ return jsonify({'error': str(e)}), 500
1445
+
1446
+
1447
+ # 行业比较路由
1448
+ @app.route('/api/industry_compare', methods=['GET'])
1449
+ def api_industry_compare():
1450
+ try:
1451
+ limit = int(request.args.get('limit', 10))
1452
+
1453
+ # 获取行业比较结果
1454
+ result = index_industry_analyzer.compare_industries(limit)
1455
+
1456
+ return custom_jsonify(result)
1457
+ except Exception as e:
1458
+ app.logger.error(f"行业比较出错: {traceback.format_exc()}")
1459
+ return jsonify({'error': str(e)}), 500
1460
+
1461
+
1462
+ # 保存股票分析结果到数据库
1463
+ def save_analysis_result(stock_code, market_type, result):
1464
+ """保存分析结果到数据库"""
1465
+ if not USE_DATABASE:
1466
+ return
1467
+
1468
+ try:
1469
+ session = get_session()
1470
+
1471
+ # 创建新的分析结果记录
1472
+ analysis = AnalysisResult(
1473
+ stock_code=stock_code,
1474
+ market_type=market_type,
1475
+ score=result.get('scores', {}).get('total', 0),
1476
+ recommendation=result.get('recommendation', {}).get('action', ''),
1477
+ technical_data=result.get('technical_analysis', {}),
1478
+ fundamental_data=result.get('fundamental_data', {}),
1479
+ capital_flow_data=result.get('capital_flow_data', {}),
1480
+ ai_analysis=result.get('ai_analysis', '')
1481
+ )
1482
+
1483
+ session.add(analysis)
1484
+ session.commit()
1485
+
1486
+ except Exception as e:
1487
+ app.logger.error(f"保存分析结果到数据库时出错: {str(e)}")
1488
+ if session:
1489
+ session.rollback()
1490
+ finally:
1491
+ if session:
1492
+ session.close()
1493
+
1494
+
1495
+ # 从数据库获取历史分析结果
1496
+ @app.route('/api/history_analysis', methods=['GET'])
1497
+ def get_history_analysis():
1498
+ """获取股票的历史分析结果"""
1499
+ if not USE_DATABASE:
1500
+ return jsonify({'error': '数据库功能未启用'}), 400
1501
+
1502
+ stock_code = request.args.get('stock_code')
1503
+ limit = int(request.args.get('limit', 10))
1504
+
1505
+ if not stock_code:
1506
+ return jsonify({'error': '请提供股票代码'}), 400
1507
+
1508
+ try:
1509
+ session = get_session()
1510
+
1511
+ # 查询历史分析结果
1512
+ results = session.query(AnalysisResult) \
1513
+ .filter(AnalysisResult.stock_code == stock_code) \
1514
+ .order_by(AnalysisResult.analysis_date.desc()) \
1515
+ .limit(limit) \
1516
+ .all()
1517
+
1518
+ # 转换为字典列表
1519
+ history = [result.to_dict() for result in results]
1520
+
1521
+ return jsonify({'history': history})
1522
+
1523
+ except Exception as e:
1524
+ app.logger.error(f"获取历史分析结果时出错: {str(e)}")
1525
+ return jsonify({'error': str(e)}), 500
1526
+ finally:
1527
+ if session:
1528
+ session.close()
1529
+
1530
+
1531
+ # 在应用启动时启动清理线程(保持原有代码不变)
1532
+ cleaner_thread = threading.Thread(target=run_task_cleaner)
1533
+ cleaner_thread.daemon = True
1534
+ cleaner_thread.start()
1535
+
1536
+ if __name__ == '__main__':
1537
+ # 将 host 设置为 '0.0.0.0' 使其支持所有网络接口访问
1538
+ app.run(host='0.0.0.0', port=8888, debug=False)