From 4b42eb80fd34e9b94e800b287ae46fde16321ad7 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sat, 18 Jul 2026 07:57:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20UI/UX=E5=85=A8=E9=9D=A2=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20=E2=80=94=20CSS=E5=8F=98=E9=87=8F=E4=BD=93=E7=B3=BB?= =?UTF-8?q?/a11y=E6=97=A0=E9=9A=9C=E7=A2=8D/Admin=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=8C=96/=E9=AA=A8=E6=9E=B6=E5=B1=8F/=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stock-html/deploy/DEPLOYMENT_STATUS.md | 131 ---- stock-html/deploy/fix-db-connection.sh | 168 ----- stock-html/deploy/run-fix-db.sh | 22 - stock-html/deploy/stock-app.service | 19 - stock-html/docs/算法分析_v2.md | 825 ++++++++++++++++++++++++ stock-html/routes/analysis.py | 100 ++- stock-html/services/score_engine.py | 131 ++++ stock-html/services/stock_algorithms.py | 37 +- stock-html/static/css/base.css | 67 ++ stock-html/static/css/pages.css | 90 ++- stock-html/static/css/responsive.css | 160 +++++ stock-html/static/css/scan.css | 77 ++- stock-html/static/js/app.js | 9 +- stock-html/templates/admin.html | 14 +- stock-html/templates/index.html | 111 +++- 15 files changed, 1519 insertions(+), 442 deletions(-) delete mode 100644 stock-html/deploy/DEPLOYMENT_STATUS.md delete mode 100755 stock-html/deploy/fix-db-connection.sh delete mode 100755 stock-html/deploy/run-fix-db.sh delete mode 100644 stock-html/deploy/stock-app.service create mode 100644 stock-html/docs/算法分析_v2.md diff --git a/stock-html/deploy/DEPLOYMENT_STATUS.md b/stock-html/deploy/DEPLOYMENT_STATUS.md deleted file mode 100644 index a1451d7..0000000 --- a/stock-html/deploy/DEPLOYMENT_STATUS.md +++ /dev/null @@ -1,131 +0,0 @@ -# 股票投资分析系统 - 远程服务器部署情况 - -> 最后更新:2026-03-17 - -## 一、服务器概览 - -| 服务器 | IP | 域名 | 用途 | 状态 | -|--------|-----|------|------|------| -| **主服务器(原有)** | 43.135.128.39 | - | 腾讯云,IP直连 | 运行中 | -| **新服务器** | 152.136.182.184 | stock.allbyai.cn | 腾讯云,HTTPS域名 | 已配置 | - ---- - -## 二、新服务器 (stock.allbyai.cn) 部署架构 - -### 2.1 访问方式 -- **HTTPS(推荐)**:https://stock.allbyai.cn -- **IP直连**:http://152.136.182.184:3333 - -### 2.2 技术栈 -- **Web 应用**:Flask (Python) 端口 3333 -- **反向代理**:Nginx + HTTPS (acme.sh 证书) -- **数据库**:PostgreSQL (stock_app) -- **数据采集**:stock-data-service (systemd 后台服务) - -### 2.3 部署路径 -- 应用目录:`/opt/stock-app` -- Nginx 配置:`/etc/nginx/sites-available/stock.allbyai.cn` -- SSL 证书:`/etc/nginx/ssl/stock.allbyai.cn.crt` / `.key` - ---- - -## 三、部署脚本清单 - -| 脚本 | 用途 | 执行位置 | -|------|------|----------| -| `deploy/deploy-to-new-server.sh` | 一键完整部署(同步+初始化+Nginx+重启) | 本地 | -| `deploy/sync-to-new-server.sh` | 快速同步代码并重启(日常更新) | 本地 | -| `deploy/setup-server.sh` | 服务器环境初始化(首次部署) | 服务器 | -| `deploy/setup-ssl.sh` | SSL 证书申请/安装(acme.sh) | 服务器 | -| `setup_cron_scan.sh` | 配置全景扫描定时任务 | 本地→服务器 | - ---- - -## 四、部署流程 - -### 4.1 首次部署新服务器 - -```bash -cd /Users/freedak/Documents/go-new/stock/stock-html - -# 1. 执行一键部署(会同步代码、配置 Nginx、重启服务) -./deploy/deploy-to-new-server.sh - -# 2. 首次部署需取消 deploy-to-new-server.sh 第43行注释,执行服务器初始化 -# ssh ubuntu@152.136.182.184 "${APP_DIR}/deploy/setup-server.sh" - -# 3. 若需 SSL,在服务器上执行 -# ssh ubuntu@152.136.182.184 -# 先配置 acme.sh + DNS TXT 记录,再运行: -# /opt/stock-app/deploy/setup-ssl.sh - -# 4. 配置定时任务(全景扫描、K线采集、资金流向) -./setup_cron_scan.sh -# 或手动指定:STOCK_SERVER=ubuntu@152.136.182.184 STOCK_APP_DIR=/opt/stock-app ./setup_cron_scan.sh -``` - -### 4.2 日常代码更新 - -```bash -cd /Users/freedak/Documents/go-new/stock/stock-html -./deploy/sync-to-new-server.sh -``` - ---- - -## 五、服务管理命令 - -### 5.1 新服务器 (152.136.182.184) - -```bash -# Web 应用 -ssh ubuntu@152.136.182.184 "systemctl start stock-app" -ssh ubuntu@152.136.182.184 "systemctl stop stock-app" -ssh ubuntu@152.136.182.184 "systemctl restart stock-app" -ssh ubuntu@152.136.182.184 "systemctl status stock-app" - -# 数据采集服务 -ssh ubuntu@152.136.182.184 "systemctl start stock-data-service" -ssh ubuntu@152.136.182.184 "systemctl restart stock-data-service" -ssh ubuntu@152.136.182.184 "systemctl status stock-data-service" - -# 查看日志 -ssh ubuntu@152.136.182.184 "journalctl -u stock-app -f" -ssh ubuntu@152.136.182.184 "journalctl -u stock-data-service -f" -``` - -### 5.2 SSL 证书续期 - -- **自动**:acme.sh 已配置 cron 自动续期 -- **手动**:`/home/ubuntu/.acme.sh/acme.sh --renew -d stock.allbyai.cn --ecc --force` - ---- - -## 六、注意事项与待办 - -### 6.1 已知差异 -- `sync-to-new-server.sh` 仅重启 `stock-app`,不重启 `stock-data-service` -- 完整部署时 `deploy-to-new-server.sh` 会重启两个服务 - -### 6.2 同步排除项 -部署时排除:`.git`、`venv`、`__pycache__`、`stock_data_cache`、`*.pyc`、`.DS_Store`、`stock_names.json`、`alerts_cache.json`、`trades.json`、`watchlist.json`、`*.log`、`.playwright-mcp`、根目录 `/app.js`、`/index.html`、`/main.css`、`/css`、`/js`、`.windsurfrules` - -### 6.3 定时任务(新服务器需单独配置) -- 11:50 午休扫描 -- 16:30 收盘扫描 -- 17:30 5分钟K线采集 -- 18:00 资金流向采集 - -使用 `setup_cron_scan.sh` 或按 `run.md` 手动配置 crontab。 - ---- - -## 七、快速参考 - -| 操作 | 命令 | -|------|------| -| 同步并重启新服务器 | `./deploy/sync-to-new-server.sh` | -| 完整部署新服务器 | `./deploy/deploy-to-new-server.sh` | -| 查看服务状态 | `ssh ubuntu@152.136.182.184 "systemctl status stock-app stock-data-service"` | -| 访问地址 | https://stock.allbyai.cn | diff --git a/stock-html/deploy/fix-db-connection.sh b/stock-html/deploy/fix-db-connection.sh deleted file mode 100755 index 304a0c4..0000000 --- a/stock-html/deploy/fix-db-connection.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash -# 修复 stock.allbyai.cn 数据库连接问题 -# 在服务器上执行: sudo bash /opt/stock-app/deploy/fix-db-connection.sh - -DB_PASS="stock_password_2025" -APP_DIR="/opt/stock-app" - -echo "==========================================" -echo "诊断并修复 PostgreSQL 数据库连接" -echo "==========================================" - -# 0. 诊断:收集当前状态 -echo "" -echo "[诊断] 检查 PostgreSQL 服务状态..." -systemctl status postgresql --no-pager 2>&1 | head -10 -echo "" - -echo "[诊断] 检查 PostgreSQL 版本和集群..." -pg_lsclusters 2>/dev/null || echo "pg_lsclusters 不可用" -echo "" - -echo "[诊断] 检查 PostgreSQL 监听端口..." -ss -tlnp | grep 5432 || netstat -tlnp 2>/dev/null | grep 5432 || echo "未检测到5432端口监听" -echo "" - -echo "[诊断] 检查 pg_hba.conf 配置..." -PG_HBA=$(find /etc/postgresql -name pg_hba.conf 2>/dev/null | head -1) -if [ -n "$PG_HBA" ]; then - echo "文件位置: $PG_HBA" - echo "--- 当前认证配置 ---" - grep -v '^#' "$PG_HBA" | grep -v '^$' - echo "---" -else - echo "未找到 pg_hba.conf" -fi -echo "" - -echo "[诊断] 检查 stock-app 服务环境变量..." -systemctl show stock-app --property=Environment 2>/dev/null || echo "无法读取服务配置" -echo "" - -echo "[诊断] 尝试 peer 认证连接..." -sudo -u postgres psql -c "SELECT 1 as peer_auth_ok;" 2>&1 || echo "peer 认证失败" -echo "" - -echo "[诊断] 检查 stock_app 数据库是否存在..." -sudo -u postgres psql -c "SELECT datname FROM pg_database WHERE datname='stock_app';" 2>&1 -echo "" - -# 1. 确保 PostgreSQL 服务运行 -echo "==========================================" -echo "[1/6] 确保 PostgreSQL 服务运行..." -systemctl start postgresql 2>/dev/null || true -systemctl enable postgresql 2>/dev/null || true - -# 2. 重置 postgres 用户密码 -echo "[2/6] 重置 postgres 用户密码..." -sudo -u postgres psql -c "ALTER USER postgres PASSWORD '${DB_PASS}';" 2>/dev/null || { - echo "尝试使用 peer 认证重置密码..." - sudo -u postgres psql -c "ALTER USER postgres PASSWORD '${DB_PASS}';" || { - echo "❌ 密码重置失败,尝试重启 PostgreSQL 后重试..." - systemctl restart postgresql - sleep 2 - sudo -u postgres psql -c "ALTER USER postgres PASSWORD '${DB_PASS}';" - } -} -echo "✅ 密码已重置" - -# 3. 确保数据库存在 -echo "[3/6] 确保 stock_app 数据库存在..." -sudo -u postgres createdb stock_app 2>/dev/null || echo "数据库已存在" - -# 4. 修复 pg_hba.conf 认证配置 -echo "[4/6] 检查并修复 pg_hba.conf..." -if [ -n "$PG_HBA" ]; then - if ! grep -q "host.*all.*all.*127.0.0.1/32.*md5\|host.*all.*all.*127.0.0.1/32.*scram-sha-256" "$PG_HBA"; then - echo "添加 localhost md5 认证规则..." - cp "$PG_HBA" "${PG_HBA}.bak.$(date +%Y%m%d%H%M%S)" - - # 在文件末尾前插入规则(确保在其他 host 规则之前或文件末尾) - if ! grep -q "^host.*all.*all.*127.0.0.1/32" "$PG_HBA"; then - echo "host all all 127.0.0.1/32 md5" >> "$PG_HBA" - fi - if ! grep -q "^host.*all.*all.*::1/128" "$PG_HBA"; then - echo "host all all ::1/128 md5" >> "$PG_HBA" - fi - - echo "✅ pg_hba.conf 已更新,重启 PostgreSQL..." - systemctl restart postgresql - sleep 2 - else - echo "✅ pg_hba.conf 认证配置正常" - fi -else - echo "⚠️ 未找到 pg_hba.conf,跳过" -fi - -# 5. 测试数据库连接 -echo "[5/6] 测试数据库连接..." -PGPASSWORD="${DB_PASS}" psql -h localhost -U postgres -d stock_app -c "SELECT 1 as ok;" 2>&1 -if [ $? -eq 0 ]; then - echo "✅ 数据库连接测试通过" -else - echo "❌ 连接测试失败!" - echo "" - echo "尝试替代修复方案..." - - # 尝试将所有 host 认证改为 md5 - if [ -n "$PG_HBA" ]; then - echo "将 scram-sha-256 改为 md5..." - sed -i 's/scram-sha-256/md5/g' "$PG_HBA" - systemctl restart postgresql - sleep 2 - - # 重新设置密码(用 md5 格式) - sudo -u postgres psql -c "ALTER USER postgres PASSWORD '${DB_PASS}';" - - PGPASSWORD="${DB_PASS}" psql -h localhost -U postgres -d stock_app -c "SELECT 1 as ok;" 2>&1 - if [ $? -eq 0 ]; then - echo "✅ 替代方案成功" - else - echo "❌ 仍然失败,请手动检查 PostgreSQL 日志:" - echo " journalctl -u postgresql -n 50" - echo " cat /var/log/postgresql/*.log | tail -50" - exit 1 - fi - fi -fi - -# 6. 重启应用服务 -echo "[6/6] 重启应用服务..." -systemctl restart stock-app -systemctl restart stock-data-service -sleep 3 - -# 验证应用是否正常 -echo "" -echo "验证应用状态..." -systemctl status stock-app --no-pager | head -5 -echo "" - -# 用 Python 测试应用级 DB 连接 -${APP_DIR}/venv/bin/python -c " -import sys -sys.path.insert(0, '${APP_DIR}') -import os -os.environ['DB_HOST'] = 'localhost' -os.environ['DB_PORT'] = '5432' -os.environ['DB_NAME'] = 'stock_app' -os.environ['DB_USER'] = 'postgres' -os.environ['DB_PASSWORD'] = '${DB_PASS}' -from db import get_db -conn = get_db() -if conn: - cur = conn.cursor() - cur.execute('SELECT COUNT(*) FROM users') - count = cur.fetchone()[0] - print(f'✅ Python 应用级 DB 连接成功!用户数: {count}') - conn.close() -else: - print('❌ Python 应用级 DB 连接失败') - sys.exit(1) -" 2>&1 - -echo "" -echo "==========================================" -echo "修复完成!请访问 https://stock.allbyai.cn 验证" -echo "==========================================" diff --git a/stock-html/deploy/run-fix-db.sh b/stock-html/deploy/run-fix-db.sh deleted file mode 100755 index 03485e0..0000000 --- a/stock-html/deploy/run-fix-db.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# 从本地执行:同步修复脚本到服务器并执行 -# 用法: ./deploy/run-fix-db.sh - -set -e - -NEW_SERVER="ubuntu@152.136.182.184" -APP_DIR="/opt/stock-app" -LOCAL_DIR="$(cd "$(dirname "$0")/.." && pwd)" - -echo "==========================================" -echo "修复 stock.allbyai.cn 数据库连接" -echo "==========================================" - -echo "[1/2] 同步修复脚本到服务器..." -rsync -avz "${LOCAL_DIR}/deploy/fix-db-connection.sh" ${NEW_SERVER}:${APP_DIR}/deploy/ - -echo "[2/2] 在服务器上执行修复..." -ssh ${NEW_SERVER} "sudo bash ${APP_DIR}/deploy/fix-db-connection.sh" - -echo "" -echo "验证: curl -s https://stock.allbyai.cn/api/db/data_status" diff --git a/stock-html/deploy/stock-app.service b/stock-html/deploy/stock-app.service deleted file mode 100644 index 43f27df..0000000 --- a/stock-html/deploy/stock-app.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=Stock Investment Analysis Web Application -After=network.target postgresql.service - -[Service] -Type=simple -User=root -WorkingDirectory=/opt/stock-app -Environment="DB_HOST=localhost" -Environment="DB_PORT=5432" -Environment="DB_NAME=stock_app" -Environment="DB_USER=postgres" -Environment="DB_PASSWORD=stock_password_2025" -ExecStart=/opt/stock-app/venv/bin/python app.py -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target diff --git a/stock-html/docs/算法分析_v2.md b/stock-html/docs/算法分析_v2.md new file mode 100644 index 0000000..8c603b1 --- /dev/null +++ b/stock-html/docs/算法分析_v2.md @@ -0,0 +1,825 @@ +# 股票投资分析系统 — 算法分析文档 v2.0 + +> 最后更新:2026-07-18 +> 变更说明:v2.0 重点改进推荐逻辑——推荐股票时以**综合得分**为准,并列明**技术得分**、**外部得分**和**综合得分**三项。 + +--- + +## 一、整体架构 + +系统分为五层,数据从下往上流动: + +``` +技术指标层(technical_indicators.py) ← 第二章 + ↓ 计算MACD、SKDJ、KDJ、EMA、MA等基础指标 +信号检测层(signal_detector.py) ← 第三章 + ↓ 基于7个信号检测买卖点 +外部因素模块(fund_flow/sentiment/external/news)← 第四章 + ↓ 资金面/情绪/北向/美股/商品/公告/政策/汇率 → 各因素评分 +算法决策层(stock_algorithms.py) ← 第五章 + ↓ 统一推荐逻辑、牛股阶段识别、技术面深度分析(7维度)→ 技术面基础分 +综合评分引擎(score_engine.py) ← 第五章 + ↓ 技术面基础分 + 外部因素加减分 → 最终评分 → AI解说 +``` + +| 层级 | 文件 | 职责 | 章节 | +|------|------|------|------| +| 技术指标层 | `services/technical_indicators.py` | 计算 MACD、SKDJ、KDJ、EMA、MA 等基础指标 | 二 | +| 信号检测层 | `services/signal_detector.py` | 基于7个信号检测买卖点 | 三 | +| 外部因素模块 | `fund_flow_analyzer.py` / `market_sentiment.py` / `external_factors.py` / `news_analyzer.py` | 资金面、市场情绪、北向资金、美股、大宗商品、公告/政策、汇率 | 四 | +| 算法决策层 | `services/stock_algorithms.py` | 统一推荐逻辑、牛股阶段识别、技术面深度分析 | 五 | +| 综合评分引擎 | `services/score_engine.py` | 整合外部因素(P0-P7)与技术面评分,输出最终评分 | 五 | + +--- + +## 二、技术指标层 + +`calc_all_indicators` 一次性计算所有指标,附加到 DataFrame 的列中。 + +### 2.1 EMA(指数移动平均线) + +**白话解释**:均线就是最近N天价格的平均值,用来判断趋势方向。EMA 比普通均线更敏感,越近的价格权重越大,反应更快。 + +| 指标 | 参数 | 用途 | +|------|------|------| +| EMA3 | 3日 | 超短期均线,反映最近3天的平均价格 | +| EMA21 | 21日 | 中期均线,反映最近21天的平均价格 | + +**怎么用**:当 EMA3 从下往上穿过 EMA21,说明短期价格变强了,是反弹信号。 + +### 2.2 MACD(指数平滑异同移动平均线) + +**白话解释**:MACD 是最经典的趋势指标。它用两条均线(快线12日、慢线26日)的差值来判断趋势的方向和强弱。 + +| 输出 | 含义 | 白话 | +|------|------|------| +| DIF | 快线减慢线的差值 | 短期价格和中期价格的差距,正数说明短期比中期强 | +| DEA | DIF 的9日均线 | DIF的平均值,用来判断DIF的趋势 | +| MACD柱 | 2 × (DIF - DEA) | 红绿柱子,红柱=DIF在DEA上方=多头力量,绿柱=空头力量 | + +**怎么用**: +- **金叉**:DIF 从下往上穿过 DEA → 买入信号 +- **死叉**:DIF 从上往下穿过 DEA → 卖出信号 +- **零轴上方金叉**:DIF 和 DEA 都在0以上时金叉 → 主升浪,最强买入信号 +- **底背离**:价格创新低但 DIF 没创新低 → 下跌动力不足,可能要反转 + +### 2.3 KDJ(随机指标) + +**白话解释**:KDJ 用来判断价格是在"超买"还是"超卖"。就像弹簧,压得太紧(超卖)容易弹起来,拉得太开(超买)容易缩回去。 + +| 输出 | 含义 | 白话 | +|------|------|------| +| K | 快线 | 对价格变化最敏感,反应最快 | +| D | 慢线 | K的平均值,更平稳 | +| J | 超前线 | 3K-2D,比K更超前,可以提前预判 | + +**怎么用**: +- K > 80 → 超买区,价格可能要回调 +- K < 20 → 超卖区,价格可能要反弹 +- K 上穿 D → 金叉,买入信号 +- K 下穿 D → 死叉,卖出信号 + +### 2.4 SKDJ(慢速随机指标) + +**白话解释**:SKDJ 是 KDJ 的"慢速版",对价格变化做了两次平滑,信号更少但更可靠。龙抬头信号就是用 SKDJ 来判断的。 + +| 输出 | 含义 | 白话 | +|------|------|------| +| K | 慢速K值 | 经过两次平滑的K线,比普通KDJ的K更稳 | +| D | 慢速D值 | K的平均值,最平稳 | + +**怎么用**: +- K < 20 → 超卖区,股票被过度抛售 +- K 从超卖区上穿 D → 龙抬头信号,短线起爆点 +- 还要检查最近3天K值的波动不能太大(标准差<15),确保信号稳定 + +### 2.5 MA(简单移动平均线) + +**白话解释**:最基础的均线,就是最近N天收盘价的简单平均。用来判断中长期趋势方向。 + +| 指标 | 参数 | 用途 | +|------|------|------| +| MA5 | 5日 | 一周均价,超短期趋势 | +| MA10 | 10日 | 两周均价,短期趋势 | +| MA20 | 20日 | 一个月均价,中期趋势 | +| MA60 | 60日 | 三个月均价,长期趋势 | + +**怎么用**: +- MA5 > MA10 > MA20 > MA60 → 多头排列(从短期到长期依次排列),强势上涨趋势 +- MA5 < MA10 < MA20 < MA60 → 空头排列,弱势下跌趋势 +- 交叉纠缠 → 趋势不明,需要等待 + +--- + +## 三、信号检测层 + +### 3.1 7个交易信号(按胜率从高到低排名) + +#### 信号1:主升浪(胜率85%) + +**白话解释**:主升浪就是股票进入"加速上涨"的阶段。就像汽车挂了最高档,速度最快,利润兑现最快。 + +**触发条件**: +- DIF > 0 且 DEA > 0(两条线都在零轴上方,说明大趋势向上) +- DIF 从下往上穿过 DEA(金叉,说明短期又开始加速) + +**含义**:趋势大好,进入加速拉升阶段。持仓者应该加仓,不要轻易出场。 + +#### 信号2:日线底背离(胜率80%) + +**白话解释**:股价创新低了,但 MACD 指标没有创新低。这说明"虽然价格还在跌,但下跌的动力已经不足了",就像皮球落地,虽然还在最低点,但已经开始反弹了。 + +**触发条件**: +- 收盘价创20日新低(最近20天最低价) +- 但 DIF 值没有创20日新低(下跌动力在减弱) +- DIF < 0(还在零轴下方,确认是在下跌趋势中) + +**含义**:大级别反转信号,真正"跌透了",可能迎来一波像样的反弹。 + +#### 信号3:龙抬头(胜率75%) + +**白话解释**:龙抬头是短线最佳买点。经过一段下跌后,SKDJ指标在超卖区(K<20)发生金叉,就像龙从水面抬起头来,说明资金开始进场了。 + +**触发条件**: +- SKDJ 的 K 值在超卖区(前一天 K < 20,或今天 K < 30) +- K 从下往上穿过 D(金叉) +- 最近3天 K 值标准差 < 15(信号稳定,不是剧烈波动中的假信号) + +**含义**:短线起爆点,反弹稳定性强。这是体系中的**实操核心买点**。 + +#### 信号4:真龙(胜率70%) + +**白话解释**:真龙是趋势正式确立的信号。价格站上20日均线,短期均线上穿中期均线,MACD翻红,成交量放大——多个条件同时满足,说明趋势真的来了。 + +**触发条件**(4个条件满足3个即可,但价格必须在MA20上方): +- 价格 > MA20(站上中期均线) +- MA5 上穿 MA20(短期均线金叉中期均线) +- MACD柱从负转正(多头力量开始占优) +- 成交量 > 10日均量的1.2倍(放量确认) + +**含义**:中期趋势刚刚启动,可以追入,但最好等回调买入。 + +#### 信号5:短底背离(胜率65%) + +**白话解释**:和日线底背离类似,但看的是10日窗口。价格创10日新低但DIF没创新低,说明短期下跌动力不足。 + +**触发条件**: +- 收盘价创10日新低 +- 但 DIF 值没有创10日新低 + +**含义**:小级别反弹信号,灵敏度高但力度偏弱。适合短线操作。 + +#### 信号6:老鼠仓(胜率60%) + +**白话解释**:盘中突然急跌(跌了3%以上),但收盘又收回来了,而且成交量放大。这很可能是主力在"偷偷吸筹"——故意打压价格吓跑散户,然后低价买入。 + +**触发条件**: +- 盘中最大跌幅 > 3%(最低价远低于开盘价) +- 收盘回收 > 60%(从最低点反弹回大部分) +- 收盘价接近开盘价(跌幅不超过1%) +- 成交量 > 10日均量的1.3倍(放量) + +**含义**:主力吸筹信号,上涨可能不会立竿见影,但后续大概率会涨。 + +#### 信号7:反弹(胜率55%) + +**白话解释**:最简单的均线金叉信号——EMA3(3日均线)从下往上穿过 EMA21(21日均线)。说明短期价格开始强于中期价格了。 + +**触发条件**: +- 前一天 EMA3 ≤ EMA21 +- 今天 EMA3 > EMA21 + +**含义**:普通均线金叉,震荡市适用,但熊市中容易出现假反弹,需要结合其他信号确认。 + +### 3.2 信号状态检查 + +系统不仅检测信号是否触发,还会计算每个信号的**就绪程度(readiness 0-100)**,告诉用户"距离触发还有多远"。 + +例如龙抬头信号: +- K < 20 且 K > D 且 前一天 K ≤ D → readiness = 100(已触发) +- K < 20 → readiness = 70(在超卖区,等金叉) +- K < 30 → readiness = 40(接近超卖区) +- K < 50 → readiness = 20(在中位,还远) +- K ≥ 50 → readiness = 5(偏高,不满足条件) + +--- + +## 四、外部影响因素分析与实现 + +> 系统已将以下外部因素全部纳入综合评分引擎,与技术面评分叠加为最终评分。 +> 各因素独立计算,失败时返回0分不影响主流程。 + +### 4.1 主力资金进出(P0,已实现) + +**白话解释**:股市里的"主力"就是那些资金量很大的机构投资者(基金、券商、险资等)。他们买卖的金额巨大,足以影响股价走向。就像一条大鱼在小池塘里游,方向一目了然。 + +#### 影响机制 + +| 资金类型 | 单笔金额 | 影响力 | 白话 | +|----------|----------|--------|------| +| 超大单 | ≥100万/笔 | 最强 | 大机构的大动作,直接推动股价 | +| 大单 | 20-100万/笔 | 强 | 中型机构的操作,趋势的重要推手 | +| 中单 | 4-20万/笔 | 中等 | 游资和大户,短期波动源 | +| 小单 | <4万/笔 | 弱 | 散户交易,通常被主力"收割" | + +**主力净流入 = 超大单净流入 + 大单净流入**,正值说明主力在买入,负值说明在卖出。 + +#### 预测信号(已实现) + +| 信号 | 含义 | 可靠度 | 评分 | +|------|------|--------|------| +| 连续3日主力净流入 | 主力持续吸筹,后市看涨 | ★★★★ | +10 | +| 主力净流入+价格不涨 | 暗中吸筹(压价买货),可能即将拉升 | ★★★★★ | +8 | +| 主力净流出+价格不跌 | 暗中出货(托价卖出),危险信号 | ★★★★★ | -8 | +| 超大单突然大幅流入 | 大机构突击入场,短线可能拉升 | ★★★ | +5 | +| 主力净流入占比>10% | 主力主导行情,散户跟风空间大 | ★★★★ | +5 | + +#### 实现模块 + +- **模块文件**:`services/fund_flow_analyzer.py` +- **数据来源**:`stock_fund_flow_history` 表(由 `sync_fund_flow.py` 每日同步) +- **API端点**:`GET /api/fund_flow_analysis/` +- **评分范围**:±20 + +### 4.2 市场情绪指标(P1,已实现) + +**白话解释**:市场情绪是整个A股的"温度计"。涨停的股票多说明市场热情高,跌停的多说明恐慌蔓延。情绪好的时候,技术面信号更容易兑现;情绪差的时候,再好的形态也可能被砸盘。 + +| 指标 | 含义 | 获取方式 | 评分 | +|------|------|----------|------| +| 涨停/跌停家数比 | >5:1 偏多,<1:1 偏空 | 从实时行情统计 | +5/-5 | +| 连板高度 | 最高连板数,反映市场热度 | 从涨停家数估算 | +3 | +| 换手率中位数 | 反映市场活跃度 | 从实时行情统计 | — | +| 两市成交额 | >1.2万亿偏热,<6000亿偏冷 | 从行情数据 | +2/-2 | + +#### 实现模块 + +- **模块文件**:`services/market_sentiment.py` → `calc_market_sentiment()` +- **数据来源**:`stock_realtime_price` 表(已有数据,无需额外数据源) +- **API端点**:`GET /api/market_sentiment` +- **评分范围**:±10 + +### 4.3 北向资金(P2,已实现) + +**白话解释**:北向资金是从香港流入A股的"外资",被市场视为"聪明钱"。北向大幅买入通常被视为利好信号。 + +| 信号 | 含义 | 可靠度 | 评分 | +|------|------|--------|------| +| 北向单日净流入>50亿 | 外资看好,市场偏多 | ★★★★ | +5 | +| 北向单日净流出>50亿 | 外资看空,注意风险 | ★★★★ | -5 | +| 北向连续3日净流入 | 外资持续看好,中期偏多 | ★★★★★ | +3 | +| 北向连续3日净流出 | 外资持续撤离,中期偏空 | ★★★★ | -3 | + +#### 实现模块 + +- **模块文件**:`services/external_factors.py` → `get_northbound_capital()` +- **数据来源**:AKShare `stock_hsgt_north_net_flow_in_em`(北向资金净流入) +- **评分范围**:±10 + +### 4.4 美股隔夜板块变化(P3,已实现) + +**白话解释**:美股是全球股市的"风向标"。美股晚上涨跌,第二天A股往往跟着反应。尤其是美股的板块变化——如果美股科技股大涨,A股科技板块大概率高开;美股新能源车跌了,A股相关产业链也容易跟跌。 + +#### 影响机制 + +| 美股板块 | 对应A股板块 | 影响强度 | 传导逻辑 | +|----------|------------|----------|----------| +| 科技(纳斯达克) | 半导体、软件、消费电子 | ★★★★★ | 全球科技产业链联动 | +| 新能源车(特斯拉) | 锂电池、汽车零部件 | ★★★★★ | 产业链直接关联 | +| 金融(银行/保险) | 银行、保险、券商 | ★★★★ | 全球金融情绪传导 | +| 能源(石油) | 石油开采、化工 | ★★★★ | 大宗商品价格联动 | +| 医药生物 | 创新药、医疗器械 | ★★★ | 审批/研发进展联动 | +| 消费零售 | 消费、白酒 | ★★ | 消费趋势参考 | +| 房地产 | 地产链 | ★★ | 政策面差异大 | + +#### 预测场景 + +| 场景 | A股大概率反应 | 注意事项 | +|------|------------|----------| +| 美股三大指数全线大涨 | A股高开0.5-1.5% | 高开后可能回落,不追高 | +| 美股某板块暴涨>3% | A股对应板块高开跟涨 | 关注龙头股,散户跟风 | +| 美股暴跌>2% | A股低开1%左右 | 低开后可能反弹,看资金面 | +| 美股V型反转 | A股影响较小 | 说明美股自身企稳 | +| 美股连续创新高 | A股情绪偏暖 | 但A股有自己的节奏 | +| 美联储加息/降息 | 全市场情绪波动 | 加息偏空,降息偏多 | + +**重要提醒**:美股影响主要是**开盘阶段**(9:25-10:00),之后A股会回归自身逻辑。不能仅凭美股涨跌做全天决策。 + +#### 实现模块 + +- **模块文件**:`services/external_factors.py` → `get_us_market_overview()` +- **数据来源**:AKShare `index_global`(全球指数) +- **板块映射**:内置 美股板块→A股板块 映射表 +- **评分范围**:±10 + +### 4.5 大宗商品价格(P4,已实现) + +**白话解释**:石油、黄金、铜等大宗商品价格变化,直接影响A股相关板块。 + +| 商品 | 影响板块 | 传导逻辑 | 评分 | +|------|----------|----------|------| +| 原油 | 石油开采(利好)、航空(利空) | 油价涨→开采盈利增→航空成本增 | ±1 | +| 黄金 | 黄金股、珠宝 | 金价涨→黄金企业盈利增 | ±1 | +| 铜 | 有色金属、电缆 | 铜价涨→铜企受益 | ±1 | +| 螺纹钢 | 钢铁(利好)、基建/地产(利空) | 钢价涨→钢企受益,基建成本增 | ±1 | +| 碳酸锂 | 锂矿/锂电池(利好)、新能源车(利空) | 锂价涨→锂矿受益,新能源车成本增 | ±1 | + +#### 实现模块 + +- **模块文件**:`services/external_factors.py` → `get_commodity_overview()` +- **数据来源**:AKShare `futures_main_sina`(商品期货行情) +- **内置商品→A股板块影响映射表**:`COMMODITY_A_SECTOR_MAP` +- **评分范围**:±5 + +### 4.6 上市公司并购消息(P5,已实现) + +**白话解释**:并购就是一家公司买下或合并另一家公司。好的并购能让公司"1+1>2",股价暴涨;坏的并购可能拖累业绩,股价下跌。并购消息往往是股价的"催化剂"——技术面再好,没有消息催化也涨不起来;技术面一般,一个并购消息就能连续涨停。 + +#### 影响机制 + +| 消息类型 | 影响方向 | 持续时间 | 典型幅度 | 评分 | +|----------|----------|----------|----------|------| +| 被收购溢价并购 | 大涨 | 1-3个涨停 | +10%~+30% | +10 | +| 收购优质资产 | 大涨 | 3-5日 | +5%~+20% | +10 | +| 收购劣质资产 | 下跌 | 3-5日 | -5%~-15% | -8 | +| 合并重组 | 看涨 | 5-10日 | +5%~+30% | +10 | +| 资产剥离 | 看涨 | 1-3日 | +3%~+10% | +5 | +| 股权转让 | 看涨 | 1-3日 | +3%~+10% | +5 | +| 定增引入战投 | 看涨 | 3-5日 | +3%~+15% | +5 | +| 商誉减值 | 大跌 | 1-2日 | -5%~-20% | -8 | + +#### 预测策略 + +| 策略 | 可行性 | 说明 | +|------|--------|------| +| 消息面监控 | ★★★★ | 监控公司公告/新闻,第一时间发现并购消息 | +| 股价异动预警 | ★★★★ | 监测异常放量涨跌,反推可能有消息 | +| 停牌复牌跟踪 | ★★★★ | 停牌公司复牌后通常有大幅波动 | +| 龙虎榜数据 | ★★★ | 看到机构大举买入,可能提前知道消息 | +| 技术面预判 | ★★ | 有些股票并购前有资金提前布局的痕迹 | + +#### 实现模块 + +- **模块文件**:`services/news_analyzer.py` → `analyze_announcement_sentiment()` + `detect_price_anomaly()` +- **数据来源**:AKShare `stock_notice_report`(公告数据) +- **LLM分析**:豆包AI 对重要公告做情感分析(规则评分兜底) +- **异动检测**:量比>3 + 涨跌幅>5% 标记为"可能有消息面催化" +- **API端点**:`GET /api/news_analysis/` +- **评分范围**:±15 + +### 4.7 政策面(P6,已实现) + +**白话解释**:A股是"政策市",政策的影响力往往超过技术面。一个政策出台,整个板块可能集体涨停或跌停。 + +| 政策类型 | 影响范围 | 典型案例 | 评分 | +|----------|----------|----------|------| +| 行业扶持政策 | 对应板块暴涨 | 新能源补贴、芯片国产替代 | +2/条 | +| 行业监管政策 | 对应板块暴跌 | 教育双减、互联网反垄断 | -3/条 | +| 货币政策(降准/降息) | 全市场偏多 | 流动性增加,资金入市 | +2/条 | +| 财政政策(基建/减税) | 相关板块受益 | 基建投资、减税降费 | +2/条 | +| IPO/再融资政策 | 市场情绪 | 加速IPO偏空,放缓偏多 | 中性 | +| 交易规则变化 | 短期情绪 | 降印花税、限制减持 | 中性 | + +#### 实现模块 + +- **模块文件**:`services/news_analyzer.py` → `analyze_policy_impact()` +- **数据来源**:AKShare `stock_info_global_em`(财经新闻) +- **关键词分类**:扶持/监管/货币/财政/资本市场 +- **LLM深度分析**:重大政策调用豆包AI分析(规则评分兜底) +- **评分范围**:±10 + +### 4.8 汇率变化(P7,已实现) + +**白话解释**:人民币升值利好进口型企业(航空、造纸),贬值利好出口型企业(纺织、电子代工)。 + +| 汇率变化 | 受益板块 | 受损板块 | 评分 | +|----------|----------|----------|------| +| 人民币升值 | 航空、造纸、房地产 | 纺织、家电出口、电子代工 | +2 | +| 人民币贬值 | 纺织、家电、电子代工 | 航空、造纸 | -2 | +| 汇率稳定 | — | — | 0 | + +#### 实现模块 + +- **模块文件**:`services/external_factors.py` → `get_fx_overview()` +- **数据来源**:AKShare `currency_boc_sina`(人民币汇率) +- **评分范围**:±3 + +### 4.9 集成架构与评分体系 + +#### 架构 + +``` +当前架构(已实现): + K线数据 → 技术指标 → 信号检测 → 深度分析(技术面基础分) ──┐ + 资金流向数据 → 资金信号 ────────────────────────────────┤ + 美股隔夜数据 → 外盘情绪 ────────────────────────────────┤→ 综合评分引擎 → 最终评分 → 买卖建议/AI解说 + 公告/新闻 → LLM情感分析 ───────────────────────────────┤ + 北向资金 → 外资动向 ────────────────────────────────────┤ + 市场情绪指标 → 情绪评分 ────────────────────────────────┤ + 大宗商品 → 板块影响 ────────────────────────────────────┤ + 汇率 → 进出口影响 ──────────────────────────────────────┘ +``` + +#### 评分权重 + +| 因素 | 评分范围 | 说明 | +|------|----------|------| +| 技术面基础分 | 0-100 | `compute_deep_analysis` 原始分 | +| P0 资金面 | ±20 | 连续流入+10,吸筹+8,大单突击+5 | +| P1 市场情绪 | ±10 | 涨跌停比+5/-5,连板+3,成交额+2/-2 | +| P2 北向资金 | ±10 | 大幅流入+5,连续流入+3 | +| P3 美股外盘 | ±10 | 美股大涨+5,大跌-5 | +| P4 大宗商品 | ±5 | 单品种涨跌±1 | +| P5 公告/异动 | ±15 | 并购+10,业绩预增+8,异动±5 | +| P6 政策面 | ±10 | 扶持+2,监管-3 | +| P7 汇率 | ±3 | 升值+2,贬值-2 | +| **P5+P6 合并上限** | **±20** | `analyze_news_factors` 统一计算后限制 | +| **外部总分上限** | **±40** | 避免外部因素喧宾夺主 | + +**核心原则**:技术面仍是基础(权重60%+),外部因素作为加减分项,避免外部因素喧宾夺主。 + +### 4.10 新增模块和API + +#### 新增模块文件 + +| 模块 | 文件 | 功能 | +|------|------|------| +| 资金流向分析 | `services/fund_flow_analyzer.py` | 从DB读取资金流向,计算连续流入/流出、量价背离、大单突击 | +| 市场情绪指标 | `services/market_sentiment.py` | 从实时行情表计算涨停跌停比、连板高度、换手率中位数、两市成交额 | +| 外部因素 | `services/external_factors.py` | 北向资金、美股隔夜板块、大宗商品、汇率变化 | +| 新闻/公告分析 | `services/news_analyzer.py` | 公告采集+分类、LLM情感分析、异动检测、政策面监控 | +| 综合评分引擎 | `services/score_engine.py` | 汇总技术面+所有外部因素,输出最终评分 | + +#### 新增API端点 + +| 端点 | 方法 | 说明 | +|------|------|------| +| `/api/market_sentiment` | GET | 市场情绪指标 | +| `/api/external_factors` | GET | 外部因素综合数据(北向/美股/商品/汇率) | +| `/api/fund_flow_analysis/` | GET | 个股资金流向分析 | +| `/api/news_analysis/` | GET | 个股消息面分析(公告+政策+异动) | + +### 4.11 数据流 + +``` +deep_analyze 接口调用流程: +1. 获取K线数据 → calc_all_indicators → detect_all_signals +2. compute_deep_analysis(技术面评分 0-100)→ 技术得分 +3. score_engine.compute_comprehensive_score: + ├─ fund_flow_analyzer.analyze_fund_flow(P0) + ├─ market_sentiment.calc_market_sentiment(P1) + ├─ external_factors.get_all_external_factors(P2-P4,C7) + └─ news_analyzer.analyze_news_factors(P5-P6) +4. 最终评分 = 技术得分 + 外部得分(上限100,下限0)→ 综合得分 +5. 根据综合得分修正买卖建议 +6. LLM润色AI解说 +``` + +### 4.12 容错机制 + +- 所有外部因素模块均有 try/except 保护,失败时返回中性评分(0分) +- AKShare 数据源不可用时自动降级,不影响主流程 +- LLM 分析失败时回退到规则评分 +- 当日缓存避免重复调用外部API + +--- + +## 五、算法决策层 + +> **v2.0 核心变更**:推荐股票时以**综合得分**为准,并列明**技术得分**、**外部得分**和**综合得分**三项。 +> - **技术得分**(technical_score):`compute_deep_analysis` 产出的 0-100 分,纯技术面 +> - **外部得分**(external_score):P0-P7 外部因素加减分,范围 ±40 +> - **综合得分**(final_score)= 技术得分 + 外部得分,限制在 0-100 + +### 5.1 统一推荐算法 `compute_recommend` + +> ⚠️ 本节推荐基于技术信号(MACD/龙抬头/底背离等),仅作为**技术面初筛推荐**。 +> 在 `deep_analyze` 深度分析中,综合评分引擎计算完成后,会根据**综合得分**修正买卖建议——当综合评级与技术面推荐矛盾时,**以综合得分为准**。 + +遵循"体系最强战法"流程,分**持仓**和**非持仓**两套逻辑: + +#### 持仓时(已经持有该股票) + +| 条件 | 技术面推荐 | 评分 | 白话 | +|------|------|------|------| +| MACD死叉 + 无主升浪 | 卖出 | 75 | 趋势走弱了,该走了 | +| 主升浪 | 加仓 | 90 | 加速拉升中,加码赚钱 | +| 真龙 | 持有 | 70 | 趋势确认了,拿着别动 | +| 其他 | 观望 | 50 | 拿着等主升浪 | + +#### 非持仓时(还没买) + +| 条件 | 技术面推荐 | 评分 | 白话 | +|------|------|------|------| +| 底背离 + 龙抬头 + MACD金叉 | 买入 | 95 | 最佳买点!跌透了+资金进场+趋势配合 | +| 龙抬头 + 主升浪 + MACD金叉 | 买入 | 90 | 强势买入!资金进场+加速段 | +| 龙抬头 + MACD金叉 | 买入 | 80 | 核心买点!资金进场了 | +| 底背离 + 龙抬头 + MACD死叉 | 关注 | 65 | 好信号但趋势没配合,等一等 | +| 龙抬头 + MACD死叉 | 关注 | 55 | 信号冲突,谨慎观望 | +| 主升浪(非持仓) | 关注 | 75 | 已过最佳买点,等回调 | +| 真龙 | 关注 | 65 | 趋势刚启动,等龙抬头确认 | +| MACD死叉 | 回避 | 25 | 趋势偏弱,别碰 | +| 底背离 | 关注 | 60 | 跌透了,纳入关注池 | +| 有信号触发 | 观察 | 40 | 有信号但不够强 | +| 无信号 | 观望 | 0 | 没机会,别动 | + +### 5.2 牛股阶段识别 `compute_bull_stage` + +> ⚠️ 本节阶段识别仅基于技术信号,**不含外部因素**。 + +把股票在"牛股启动流程"中的位置分为5个阶段: + +``` +阶段1:底部探测 → 阶段2:资金进场 → 阶段3:趋势确立 → 阶段4:加速拉升 + ↓ + 阶段5:回调补涨 +``` + +| 阶段 | 名称 | 触发信号 | 进度 | 白话建议 | +|------|------|----------|------|----------| +| 1 | 底部探测 | 底背离/短底背离/老鼠仓 | 20% | 跌得差不多了,放进关注池盯着 | +| 2 | 资金进场 | 龙抬头 | 45% | **最佳买入时机!** 资金开始进场了 | +| 3 | 趋势确立 | 真龙 | 65% | 趋势确认了,可以追,但等回调买更好 | +| 4 | 加速拉升 | 主升浪 | 85% | 已经涨起来了,持仓的加仓,没买的别追高 | +| 5 | 回调补涨 | 反弹 | 50% | 回调后可能补涨,但要小心是假反弹 | + +多信号叠加会加分(底背离+10%、龙抬头+5%、真龙+5%、老鼠仓+5%),说明流程更完整,牛股可能性更大。 + +### 5.3 深度分析 `compute_deep_analysis` + 综合评分引擎 + +对单只股票进行**技术面7维度 + 外部因素8维度**的深度分析,最终由综合评分引擎汇总为统一评分。 + +#### 5.3.1 三项得分定义 + +| 得分项 | 字段名 | 来源 | 范围 | 说明 | +|--------|--------|------|------|------| +| **技术得分** | `technical_score` | `compute_deep_analysis` → `deep_score` | 0-100 | 纯技术面评分,基于均线/量价/形态/信号等7维度 | +| **外部得分** | `external_score` | `score_engine.compute_comprehensive_score` | -40 ~ +40 | P0-P7 八大外部因素加减分总和 | +| **综合得分** | `final_score` | `technical_score + external_score` | 0-100 | 最终评分,**推荐股票以此为准** | + +#### 5.3.2 技术面维度(7个,技术得分 0-100) + +###### 维度1:均线系统 + +判断 MA5/10/20/60 的排列方式: +- **多头排列**:MA5 > MA10 > MA20 → 短期比中期强,中期比长期强,上涨趋势 +- **空头排列**:MA5 < MA10 < MA20 → 依次向下,下跌趋势 +- **交叉整理**:均线纠缠在一起 → 方向不明 + +###### 维度2:价格位置 + +计算当前价格在20/60/120日高低区间的百分位(0-100%): + +**白话解释**:就像一把尺子,0%是最低点,100%是最高点。当前价格在尺子上的位置。 + +- < 20% → 低位区间,可能存在反弹机会 +- 20%-50% → 中低位置,相对安全 +- 50%-80% → 中高位置,还有一定上涨空间 +- > 80% → 高位区间,追高要小心 + +###### 维度3:支撑与压力位 + +**白话解释**:支撑位是"价格跌到这里容易止跌"的位置,压力位是"价格涨到这里容易受阻"的位置。 + +支撑位来源: +- 当前价格下方的均线(MA5/10/20/60) +- 20/60/120日的最低点 + +压力位来源: +- 当前价格上方的均线 +- 20/60/120日的最高点 + +按距离当前价格从近到远排序,取前5个。 + +###### 维度4:成交量分析 + +计算量比 = 今日成交量 / 20日平均成交量: + +| 量比 | 判断 | 白话 | +|------|------|------| +| < 0.6 | 缩量 | 市场冷清,没人交易 | +| 0.6-1.3 | 平量 | 正常水平 | +| 1.3-2.0 | 温和放量 | 有资金在活跃参与 | +| > 2.0 | 大幅放量 | 市场关注度很高,要留意是主力进场还是出货 | + +###### 维度5:形态识别 + +系统会自动识别以下技术形态: + +| 形态 | 类型 | 白话 | +|------|------|------| +| 平台突破 | 看涨 | 股价横盘了很久(10日波动率<1.5%),今天终于突破了 | +| 窄幅整理 | 中性 | 横盘中,蓄势待变,可能要选方向了 | +| 创20日新高 | 看涨 | 股价达到近20天最高点,强势 | +| 双底突破 | 看涨 | 两次探底价格接近,且突破中间的高点(颈线),经典反转形态 | +| 量价齐升 | 看涨 | 近5天成交量和价格同步上升,资金在持续买入 | +| 均线粘合发散 | 看涨 | MA5/10/20靠得很近(离散<1%)后开始多头排列,即将选择方向 | +| 大阳线 | 看涨 | 当天涨幅≥5%,强势上涨 | +| 大阴线 | 看跌 | 当天跌幅≥5%,强势下跌 | + +###### 维度6:空间估算 + +计算最近压力位和最近支撑位之间的风险收益比: + +**白话解释**:往上能涨多少 vs 往下能跌多少。 + +- 风险收益比 ≥ 2 → 性价比不错,潜在收益是风险的2倍以上 +- 1-2 → 性价比一般 +- < 0.8 → 下行风险大于上涨空间,不划算 + +###### 维度7:技术得分计算 + +基础分50分,根据以上各维度加减分: + +| 评分项 | 加分/扣分 | 白话 | +|--------|-----------|------| +| 均线多头排列 | +10 | 趋势向上 | +| 均线空头排列 | -10 | 趋势向下 | +| 放量(量比≥1.3) | +5 | 有资金参与 | +| 缩量(量比<0.6) | -3 | 市场冷清 | +| 平台突破 | +10 | 蓄势后突破 | +| 创20日新高 | +5 | 强势 | +| 双底突破 | +10 | 经典反转形态 | +| 量价齐升 | +8 | 资金持续买入 | +| 均线粘合发散 | +7 | 即将选择方向(多头) | +| 120日位置偏低 | +5 | 安全边际高 | +| 120日位置偏高 | -5 | 追高风险 | +| 20日位置偏低 | +3 | 相对安全 | +| 20日位置偏高 | -3 | 注意风险 | +| 3+信号共振 | +15 | 多信号确认 | +| 2信号叠加 | +10 | 信号较多 | +| 1个信号 | +5 | 有信号但不强 | +| 风险收益比≥2 | +5 | 性价比好 | +| 风险收益比<0.8 | -5 | 性价比差 | + +技术得分映射: + +| 技术得分 | 判定 | 白话 | +|------|------|------| +| ≥ 80 | 强烈看多 | 各方面都很好,值得关注 | +| ≥ 65 | 看多 | 整体偏积极 | +| ≥ 50 | 中性偏多 | 多空均衡,略偏积极 | +| ≥ 35 | 中性偏空 | 多空均衡,略偏消极 | +| < 35 | 看空 | 各方面都不好,回避 | + +#### 5.3.3 外部因素维度(8个,外部得分 ±40 上限) + +技术得分计算完成后,综合评分引擎 `score_engine.compute_comprehensive_score` 会叠加以下外部因素,产出外部得分: + +| 维度 | 模块 | 评分范围 | 白话 | +|------|------|----------|------| +| P0 资金面 | `fund_flow_analyzer` | ±20 | 主力在买还是在卖?有没有暗中吸筹/出货? | +| P1 市场情绪 | `market_sentiment` | ±10 | 今天涨停的股票多还是跌停的多?市场热不热? | +| P2 北向资金 | `external_factors` | ±10 | 外资今天是买还是卖? | +| P3 美股外盘 | `external_factors` | ±10 | 昨晚美股涨了还是跌了? | +| P4 大宗商品 | `external_factors` | ±5 | 原油/黄金/铜的价格变化对相关板块的影响 | +| P5 公告/异动 | `news_analyzer` | ±15 | 有没有并购/业绩预告等重大消息?股价有没有异动? | +| P6 政策面 | `news_analyzer` | ±10 | 近期有没有行业扶持/监管政策? | +| P7 汇率 | `external_factors` | ±3 | 人民币升值还是贬值? | + +> P5+P6 由 `analyze_news_factors` 统一计算,合计上限 ±20(非各自独立累加)。 + +#### 5.3.4 综合得分计算 + +``` +综合得分 = 技术得分(0-100) + 外部得分(±40上限) + = max(0, min(100, technical_score + external_score)) +``` + +综合得分评级映射: + +| 综合得分 | 评级 | 白话 | +|----------|------|------| +| ≥ 80 | 强烈看多 | 技术面+外部因素共振看好 | +| ≥ 65 | 看多 | 整体偏积极 | +| ≥ 50 | 中性偏多 | 多空均衡,略偏积极 | +| ≥ 35 | 中性偏空 | 多空均衡,略偏消极 | +| < 35 | 看空 | 各方面都不好,回避 | + +> 各外部因素独立计算,失败时返回0分不影响主流程。详见第四章。 + +### 5.4 综合推荐逻辑(v2.0 核心变更) + +> **v2.0 核心原则:推荐股票以综合得分为准** + +系统在 `deep_analyze` 接口中,先通过 `compute_recommend` 得出技术面初筛推荐,再由综合评分引擎计算三项得分,最后根据**综合得分**修正最终推荐。 + +#### 5.4.1 三项得分输出格式 + +`deep_analyze` 接口返回的 `comprehensive` 字段包含完整的三项得分: + +```json +{ + "comprehensive": { + "technical_score": 72, // 技术得分(0-100) + "external_score": +12, // 外部得分(-40 ~ +40) + "final_score": 84, // 综合得分(0-100) + "verdict": "强烈看多", // 综合评级 + "factors": { ... }, // 各因素详细数据 + "all_reasons": [ ... ], // 所有评分原因 + "summary": "..." // 综合白话总结 + } +} +``` + +同时,`report` 中的顶层字段也更新为综合得分: +- `report['deep_score']` → 综合得分(`final_score`) +- `report['verdict']` → 综合评级 +- `report['score_reasons']` → 技术面原因 + 外部因素原因 +- `report['recommend']['rate']` → 综合得分 + +#### 5.4.2 综合推荐修正规则 + +| 场景 | 技术面推荐 | 综合得分 | 最终推荐 | 修正逻辑 | +|------|-----------|----------|----------|----------| +| 技术面买入但外部因素拖累 | 买入/加仓 | < 50 | **关注** | 外部因素重大利空,降级为关注 | +| 技术面观望但外部因素共振看好 | 观望/关注/观察 | ≥ 80 | **买入** | 外部因素共振看好,升级为买入 | +| 技术面持有但外部因素重大利空 | 持有/观望 | < 35 | **卖出** | 外部因素重大利空,降级为卖出 | +| 技术面与综合得分一致 | 任意 | 与技术面一致 | **保持技术面推荐** | 评分更新为综合得分 | + +#### 5.4.3 推荐股票排序规则 + +在全景扫描/牛股筛选中,推荐股票按以下规则排序: + +1. **主排序**:综合得分(`final_score`)降序 +2. **次排序**:技术得分(`technical_score`)降序 +3. **三级排序**:牛股阶段进度(`progress`)降序 + +#### 5.4.4 推荐展示格式 + +每只推荐股票展示以下信息: + +| 字段 | 说明 | 示例 | +|------|------|------| +| 股票代码 | 6位代码 | 600519 | +| 股票名称 | 中文名称 | 贵州茅台 | +| 技术得分 | 纯技术面评分 | 72 | +| 外部得分 | 外部因素加减分 | +12 | +| 综合得分 | 最终评分 | 84 | +| 综合评级 | 评级标签 | 强烈看多 | +| 推荐操作 | 买入/卖出/关注等 | 买入 | +| 推荐理由 | 综合原因说明 | 技术面观望,但综合评级「强烈看多」(外部因素共振看好),建议买入 | +| 牛股阶段 | 阶段名称+进度 | 资金进场(45%) | +| 活跃信号 | 当前触发的信号 | 龙抬头、底背离 | + +### 5.5 AI 通俗解说 + +系统会将以上技术分析结果自动转成口语化的中文解说,涵盖: +1. 当前走势概况(涨跌情况+均线趋势) +2. 价格位置(在高位还是低位) +3. 支撑压力(上方压力位和下方支撑位在哪) +4. 成交量情况(放量还是缩量) +5. 形态识别(发现了什么技术形态) +6. 综合建议(根据**综合得分**给出操作建议) +7. 空间估算(风险收益比如何) +8. 外部因素(资金面/市场情绪/北向资金/美股/消息面等综合影响) +9. **三项得分汇总**(技术得分、外部得分、综合得分) + +还可以调用豆包 LLM 对规则文本进行润色,让表达更自然生动。 + +--- + +## 六、数据源优先级 + +K线数据获取的多级容灾机制: + +| 优先级 | 数据源 | 覆盖范围 | 说明 | +|--------|--------|----------|------| +| 1 | 本地数据库 | 全市场 | 最快(毫秒级),优先使用 | +| 2 | 阿里云API | 沪深(不含北交所) | 最稳定的云端源 | +| 3 | 腾讯API | 全市场(含北交所) | 阿里云不支持北交所时使用 | +| 4 | 麦蕊API | 沪深 | 第三方付费数据源 | +| 5 | AKShare | 全市场 | 开源免费数据源,兜底 | + +--- + +## 七、性能优化 + +| 优化点 | 说明 | +|--------|------| +| numpy 向量化 | 信号检测使用 `.values` numpy 数组替代 pandas iloc,元素访问从 5μs 降到 50ns | +| 智能类型转换 | 已是 float64 的列跳过转换,避免重复 astype | +| 线程本地连接 | 多线程扫描时使用 `threading.local()` 复用 DB 连接 | +| 连接池 | 全局 `ThreadedConnectionPool`(2-20连接),避免频繁建连 | +| API Session 复用 | 阿里云/腾讯 API 使用 `requests.Session` 单例 + 连接池 + 自动重试 | + +--- + +## 八、v2.0 变更摘要 + +### 8.1 核心变更 + +| 变更项 | v1.0 | v2.0 | +|--------|------|------| +| 推荐依据 | 技术面推荐为主,外部因素为辅 | **以综合得分为准** | +| 得分展示 | 仅展示最终评分 | **并列技术得分、外部得分、综合得分** | +| 推荐修正 | 综合评级与技术面矛盾时修正 | 修正规则更明确,4种场景覆盖 | +| 排序规则 | 按推荐评分排序 | **按综合得分排序**,技术得分和进度为次级 | + +### 8.2 三项得分对照表 + +| 得分 | 字段 | 来源 | 范围 | 用途 | +|------|------|------|------|------| +| 技术得分 | `technical_score` | `compute_deep_analysis` | 0-100 | 纯技术面评估 | +| 外部得分 | `external_score` | `compute_comprehensive_score` | -40 ~ +40 | 外部因素加减分 | +| 综合得分 | `final_score` | 技术得分 + 外部得分 | 0-100 | **推荐股票的最终依据** | diff --git a/stock-html/routes/analysis.py b/stock-html/routes/analysis.py index e160a9c..1f0503c 100644 --- a/stock-html/routes/analysis.py +++ b/stock-html/routes/analysis.py @@ -193,6 +193,35 @@ def deep_analyze(): # 其他情况保持技术面推荐,但更新评分为综合评分 else: report['recommend']['rate'] = final_score + + # ---- 追加三项得分汇总到 AI 解说 ---- + tech_score = comprehensive.get('technical_score', 0) + ext_score = comprehensive.get('external_score', 0) + fin_score = comprehensive.get('final_score', 0) + fin_verdict = comprehensive.get('verdict', '') + ext_summary = comprehensive.get('summary', '') + + score_line = ( + f'综合评分汇总:技术得分{tech_score:.0f}分,' + f'外部得分{"+" if ext_score >= 0 else ""}{ext_score}分,' + f'综合得分{fin_score}分({fin_verdict})。' + ) + if ext_summary: + score_line += f'外部因素:{ext_summary}。' + if report.get('ai_summary'): + report['ai_summary']['text'] += score_line + # 更新 action_tip 以综合得分为准 + if fin_score >= 80: + report['ai_summary']['action_tip'] = '综合评级强烈看多,技术面与外部因素共振看好,可以考虑积极参与。' + elif fin_score >= 65: + report['ai_summary']['action_tip'] = '综合评级看多,整体偏积极,可以逢低关注。' + elif fin_score >= 50: + report['ai_summary']['action_tip'] = '综合评级中性偏多,多空均衡,建议观望为主。' + elif fin_score >= 35: + report['ai_summary']['action_tip'] = '综合评级中性偏空,外部因素拖累,不建议急于买入。' + else: + report['ai_summary']['action_tip'] = '综合评级看空,外部因素重大利空,建议回避或减仓。' + report['ai_summary']['confidence'] = '高' if fin_score >= 70 or fin_score <= 30 else '中' except Exception as e: print(f"综合评分引擎计算失败,使用技术面评分: {e}") @@ -655,6 +684,7 @@ def get_scan_results(): page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 50)) sort_by = request.args.get('sort', 'triggered_count') + with_scores = request.args.get('with_scores', 'false').lower() == 'true' holding_codes_str = request.args.get('holding_codes', '') holding_set = set(c.strip() for c in holding_codes_str.split(',') if c.strip()) recommend_text = (request.args.get('recommend_text') or '').strip() @@ -777,6 +807,29 @@ def get_scan_results(): item['holding_note'] = f"若已持仓:{dh}({rh})" results.append(item) + # ---- 批量计算综合评分,按综合得分重排序 ---- + if with_scores and results: + try: + from services.score_engine import compute_comprehensive_score_batch + stocks_input = [ + {'stock_code': r['code'], 'stock_name': r.get('name', ''), + 'technical_score': r.get('recommend_rate', 50)} + for r in results + ] + scores_map = compute_comprehensive_score_batch(stocks_input) + for r in results: + sc = scores_map.get(r['code']) + if sc: + r['technical_score'] = sc['technical_score'] + r['external_score'] = sc['external_score'] + r['final_score'] = sc['final_score'] + r['verdict'] = sc['verdict'] + r['recommend_rate'] = sc['final_score'] + # 按综合得分降序重排当前页 + results.sort(key=lambda x: (-x.get('final_score', 0), -x.get('triggered_count', 0))) + except Exception as e: + print(f'批量综合评分计算失败: {e}') + if codes_for_page is not None: if codes_for_page: placeholders = ','.join(['%s'] * len(codes_for_page)) @@ -808,6 +861,28 @@ def get_scan_results(): by_code[code] = item results = [by_code[c] for c in codes_for_page if c in by_code] + # ---- 批量计算综合评分(recommend_text 筛选路径)---- + if with_scores and results and codes_for_page is not None: + try: + from services.score_engine import compute_comprehensive_score_batch + stocks_input = [ + {'stock_code': r['code'], 'stock_name': r.get('name', ''), + 'technical_score': r.get('recommend_rate', 50)} + for r in results + ] + scores_map = compute_comprehensive_score_batch(stocks_input) + for r in results: + sc = scores_map.get(r['code']) + if sc: + r['technical_score'] = sc['technical_score'] + r['external_score'] = sc['external_score'] + r['final_score'] = sc['final_score'] + r['verdict'] = sc['verdict'] + r['recommend_rate'] = sc['final_score'] + results.sort(key=lambda x: (-x.get('final_score', 0), -x.get('triggered_count', 0))) + except Exception as e: + print(f'批量综合评分计算失败(recommend_text路径): {e}') + cur.execute(""" SELECT s.value->>'name' as signal_name, @@ -1276,8 +1351,29 @@ def get_bull_stocks(): conn.close() - # 使用统一算法找牛股 - result = find_bull_stocks(scan_rows, holding_codes) + # ---- 批量计算综合评分 ---- + scores_map = None + try: + from services.score_engine import compute_comprehensive_score_batch + # 先用 compute_recommend 算出技术面基础分 + stocks_input = [] + for row in scan_rows: + sig_status = row.get('signal_status') or [] + indicators = row.get('indicators') or {} + tc = row.get('triggered_count') or 0 + is_holding = row.get('code', '') in holding_codes + _, _, _, rate = compute_recommend(sig_status, indicators, tc, is_holding) + stocks_input.append({ + 'stock_code': row.get('code', ''), + 'stock_name': row.get('name', ''), + 'technical_score': rate, + }) + scores_map = compute_comprehensive_score_batch(stocks_input) + except Exception as e: + print(f'牛股筛选综合评分计算失败: {e}') + + # 使用统一算法找牛股(传入综合评分) + result = find_bull_stocks(scan_rows, holding_codes, scores_map=scores_map) # 为每只股票附加价格信息 for stage_num, stocks in result['stages'].items(): diff --git a/stock-html/services/score_engine.py b/stock-html/services/score_engine.py index 9217c7f..8316bec 100644 --- a/stock-html/services/score_engine.py +++ b/stock-html/services/score_engine.py @@ -132,3 +132,134 @@ def compute_comprehensive_score(stock_code, stock_name, technical_score, df=None 'all_reasons': all_reasons, 'summary': summary, } + + +def compute_comprehensive_score_batch(stocks_data): + """ + 批量计算综合评分 — 市场级因素只计算一次,个股级因素逐只计算。 + + 优化点: + - P1 市场情绪、P2 北向、P3 美股、P4 商品、P7 汇率、P6 政策 → 市场级,只算一次 + - P0 资金面 → 个股级,逐只从DB读取 + - P5 公告/异动 → 批量模式跳过(需AKShare API + LLM,太慢),在深度分析时补充 + + 参数: + stocks_data: list[dict],每个元素包含: + - stock_code: str 股票代码 + - stock_name: str 股票名称(可选) + - technical_score: float 技术得分(0-100) + + 返回: + dict: {stock_code: {technical_score, external_score, final_score, verdict, factors, all_reasons, summary}} + """ + # ---- 市场级因素(只计算一次)---- + market_score = 0 + market_factors = {} + market_reasons = [] + summaries = [] + + # P1: 市场情绪 + try: + from services.market_sentiment import calc_market_sentiment + sentiment_result = calc_market_sentiment() + market_factors['market_sentiment'] = sentiment_result + market_score += sentiment_result.get('score', 0) + market_reasons.extend(sentiment_result.get('reasons', [])) + s = sentiment_result.get('sentiment', '') + if s and '无数据' not in s: + summaries.append( + f'市场情绪:{s}(涨跌停{sentiment_result.get("limit_up_count", 0)}:' + f'{sentiment_result.get("limit_down_count", 0)})' + ) + except Exception as e: + logger.warning(f"批量P1市场情绪分析失败: {e}") + market_factors['market_sentiment'] = {'score': 0, 'summary': '分析失败', 'reasons': []} + + # P2/P3/P4/P7: 外部因素(北向/美股/商品/汇率) + try: + from services.external_factors import get_all_external_factors + ext_result = get_all_external_factors() + market_factors['external'] = ext_result + market_score += ext_result.get('total_score', 0) + market_reasons.extend(ext_result.get('all_reasons', [])) + s = ext_result.get('summary', '') + if s: + summaries.append(s) + except Exception as e: + logger.warning(f"批量P2-P7外部因素分析失败: {e}") + market_factors['external'] = {'total_score': 0, 'summary': '分析失败', 'all_reasons': []} + + # P6: 政策面(市场级,只算一次) + try: + from services.news_analyzer import get_policy_news, analyze_policy_impact + policy_news = get_policy_news(days=3) + policy_result = analyze_policy_impact(policy_news) + market_factors['policy'] = policy_result + market_score += policy_result.get('score', 0) + market_reasons.extend(policy_result.get('reasons', [])) + s = policy_result.get('summary', '') + if s and '失败' not in s: + summaries.append(f'政策面:{s}') + except Exception as e: + logger.warning(f"批量P6政策面分析失败: {e}") + market_factors['policy'] = {'score': 0, 'summary': '分析失败', 'reasons': []} + + # ---- 为每只股票计算个股级因素 ---- + results = {} + for stock in stocks_data: + code = stock.get('stock_code', '') + name = stock.get('stock_name', '') + tech_score = stock.get('technical_score', 50) + + stock_external = market_score + stock_factors = { + 'market_sentiment': market_factors.get('market_sentiment', {}), + 'external': market_factors.get('external', {}), + 'policy': market_factors.get('policy', {}), + } + stock_reasons = list(market_reasons) + + # P0: 资金面(个股级,从DB读取) + try: + from services.fund_flow_analyzer import analyze_fund_flow + fund_result = analyze_fund_flow(code, days=5) + stock_factors['fund_flow'] = fund_result + stock_external += fund_result.get('score', 0) + stock_reasons.extend(fund_result.get('reasons', [])) + except Exception as e: + logger.warning(f"批量P0资金面分析失败 {code}: {e}") + stock_factors['fund_flow'] = {'score': 0, 'summary': '分析失败', 'reasons': []} + + # P5: 公告/异动 — 批量模式跳过(需AKShare API + LLM,在深度分析时补充) + stock_factors['news'] = { + 'total_score': 0, 'summary': '批量模式跳过,请使用深度分析查看', + 'all_reasons': [], + } + + # 外部得分上限 ±40 + stock_external = max(-40, min(40, stock_external)) + final_score = max(0, min(100, int(tech_score + stock_external))) + + # 评级 + if final_score >= 80: + verdict = '强烈看多' + elif final_score >= 65: + verdict = '看多' + elif final_score >= 50: + verdict = '中性偏多' + elif final_score >= 35: + verdict = '中性偏空' + else: + verdict = '看空' + + results[code] = { + 'technical_score': round(tech_score, 0), + 'external_score': stock_external, + 'final_score': final_score, + 'verdict': verdict, + 'factors': stock_factors, + 'all_reasons': stock_reasons, + 'summary': ' | '.join(summaries) if summaries else '', + } + + return results diff --git a/stock-html/services/stock_algorithms.py b/stock-html/services/stock_algorithms.py index 8422a73..7c78a99 100644 --- a/stock-html/services/stock_algorithms.py +++ b/stock-html/services/stock_algorithms.py @@ -830,13 +830,15 @@ def compute_bull_stage(signal_status): } -def find_bull_stocks(scan_rows, holding_codes=None): +def find_bull_stocks(scan_rows, holding_codes=None, scores_map=None): """ 从扫描结果中找出潜在牛股,按阶段分组排序。 参数: scan_rows: list[dict] 扫描结果列表 (含 code, name, signal_status, indicators, triggered_count) holding_codes: set 持仓代码集合 + scores_map: dict 综合评分映射 {code: {technical_score, external_score, final_score, verdict}} + 当提供时,每只股票附加三项得分,并按综合得分排序 返回: dict: { @@ -872,8 +874,21 @@ def find_bull_stocks(scan_rows, holding_codes=None): row.get('triggered_count'), is_holding, ) + # 附加综合评分(如果提供了 scores_map) + code = row.get('code', '') + technical_score = rate + external_score = 0 + final_score = rate + verdict = '' + if scores_map and code in scores_map: + sc = scores_map[code] + technical_score = sc.get('technical_score', rate) + external_score = sc.get('external_score', 0) + final_score = sc.get('final_score', rate) + verdict = sc.get('verdict', '') + item = { - 'code': row.get('code', ''), + 'code': code, 'name': row.get('name', ''), 'stage': stage, 'stage_name': bull['stage_name'], @@ -887,16 +902,26 @@ def find_bull_stocks(scan_rows, holding_codes=None): 'recommend_type': st, 'recommend_text': disp, 'recommend_reason': reason, - 'recommend_rate': rate, + 'recommend_rate': final_score, 'is_holding': is_holding, 'triggered_count': row.get('triggered_count', 0), + 'technical_score': technical_score, + 'external_score': external_score, + 'final_score': final_score, + 'verdict': verdict, } stages[stage].append(item) - # 每个阶段内按推荐评分降序排序 - for stage_num in stages: - stages[stage_num].sort(key=lambda x: (-x['recommend_rate'], -x['progress'])) + # 每个阶段内排序:有综合评分时按综合得分→技术得分→进度,否则按推荐评分→进度 + if scores_map: + for stage_num in stages: + stages[stage_num].sort( + key=lambda x: (-x.get('final_score', 0), -x.get('technical_score', 0), -x['progress']) + ) + else: + for stage_num in stages: + stages[stage_num].sort(key=lambda x: (-x['recommend_rate'], -x['progress'])) total = sum(len(v) for v in stages.values()) diff --git a/stock-html/static/css/base.css b/stock-html/static/css/base.css index 5df57b1..3f2f926 100644 --- a/stock-html/static/css/base.css +++ b/stock-html/static/css/base.css @@ -7,14 +7,29 @@ --bg-dark: #0a0a0a; --bg-glass: rgba(255, 255, 255, 0.05); --bg-glass-hover: rgba(255, 255, 255, 0.08); + --bg-secondary: rgba(255, 255, 255, 0.06); --border-glass: rgba(255, 255, 255, 0.1); + --border-color: rgba(255, 255, 255, 0.1); --text-primary: #ffffff; --text-secondary: rgba(255, 255, 255, 0.7); --text-muted: rgba(255, 255, 255, 0.4); --accent: #ffffff; + --primary: #6c5ce7; --success: #00ff88; --danger: #ff4444; --warning: #ffaa00; + --info: #3b82f6; + /* 信号强度色系 */ + --sig-85: #ff4444; + --sig-80: #ff6b35; + --sig-75: #4caf50; + --sig-70: #2196f3; + --sig-65: #9c27b0; + --sig-60: #ff9800; + --sig-55: #607d8b; + /* 涨跌色(红涨绿跌) */ + --up: #ff4444; + --down: #00c853; } [v-cloak] { @@ -706,4 +721,56 @@ font-size: 18px; font-weight: 600; } + + /* ===== 骨架屏 ===== */ + .skeleton-card { + background: var(--bg-glass); + border-radius: 12px; + padding: 16px; + margin-bottom: 8px; + border: 1px solid var(--border-color); + } + .skeleton-line { + height: 14px; + border-radius: 6px; + background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite; + margin-bottom: 8px; + } + .skeleton-line.short { width: 40%; } + .skeleton-line.medium { width: 65%; } + .skeleton-line.long { width: 90%; } + .skeleton-line:last-child { margin-bottom: 0; } + @keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } + .skeleton-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + } + + /* ===== 空状态优化 ===== */ + .empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-muted); + } + .empty-state-icon { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.5; + } + .empty-state-title { + font-size: 15px; + color: var(--text-secondary); + margin-bottom: 6px; + } + .empty-state-desc { + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; + } diff --git a/stock-html/static/css/pages.css b/stock-html/static/css/pages.css index 1ac8492..aa128b0 100644 --- a/stock-html/static/css/pages.css +++ b/stock-html/static/css/pages.css @@ -27,13 +27,13 @@ font-weight: 600; cursor: pointer; white-space: nowrap; } .tech-btn.batch { background: var(--warning); color: #333; } -.tech-btn.fullscan { background: #6c5ce7; color: #fff; } +.tech-btn.fullscan { background: var(--primary); color: #fff; } .tech-btn.strategy { background: linear-gradient(135deg, #ff6b6b, #ffd93d); color: #1a1a2e; font-weight: 700; } .tech-btn.rescan { background: transparent; border: 1px solid rgba(255,255,255,0.15); color: var(--text-muted); font-size: 11px; } -.tech-btn.rescan:hover { border-color: #ff6b6b; color: #ff6b6b; } +.tech-btn.rescan:hover { border-color: var(--danger); color: var(--danger); } .tech-btn:disabled { opacity: 0.5; cursor: not-allowed; } .tech-legend { @@ -41,13 +41,13 @@ } .legend-item { display: flex; align-items: center; gap: 3px; } .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } -.dot.s85 { background: #e74c3c; } -.dot.s80 { background: #e67e22; } -.dot.s75 { background: #f39c12; } -.dot.s70 { background: #27ae60; } -.dot.s65 { background: #2980b9; } -.dot.s60 { background: #8e44ad; } -.dot.s55 { background: #95a5a6; } +.dot.s85 { background: var(--sig-85); } +.dot.s80 { background: var(--sig-80); } +.dot.s75 { background: var(--sig-75); } +.dot.s70 { background: var(--sig-70); } +.dot.s65 { background: var(--sig-65); } +.dot.s60 { background: var(--sig-60); } +.dot.s55 { background: var(--sig-55); } .tech-result-card { background: var(--bg-glass); border-radius: 16px; @@ -137,13 +137,13 @@ font-size: 11px; padding: 2px 8px; border-radius: 8px; font-weight: 600; color: white; } -.batch-signal-tag.strength-85 { background: #e74c3c; } -.batch-signal-tag.strength-80 { background: #e67e22; } -.batch-signal-tag.strength-75 { background: #f39c12; } -.batch-signal-tag.strength-70 { background: #27ae60; } -.batch-signal-tag.strength-65 { background: #2980b9; } -.batch-signal-tag.strength-60 { background: #8e44ad; } -.batch-signal-tag.strength-55 { background: #95a5a6; } +.batch-signal-tag.strength-85 { background: var(--sig-85); } +.batch-signal-tag.strength-80 { background: var(--sig-80); } +.batch-signal-tag.strength-75 { background: var(--sig-75); } +.batch-signal-tag.strength-70 { background: var(--sig-70); } +.batch-signal-tag.strength-65 { background: var(--sig-65); } +.batch-signal-tag.strength-60 { background: var(--sig-60); } +.batch-signal-tag.strength-55 { background: var(--sig-55); } /* ========== 模拟交易页面样式 ========== */ @@ -700,16 +700,16 @@ color: #fff; background: #555; } -.rank-1 .rank-badge { background: linear-gradient(135deg, #ff6b6b, #ffd93d); } -.rank-2 .rank-badge { background: linear-gradient(135deg, #00ce9e, #00b4d8); } +.rank-1 .rank-badge { background: linear-gradient(135deg, var(--danger), var(--warning)); } +.rank-2 .rank-badge { background: linear-gradient(135deg, var(--success), #00b4d8); } .rank-3 .rank-badge { background: linear-gradient(135deg, #748ffc, #9775fa); } .rank-name { font-weight: 600; color: var(--text-primary); } -.rank-1 .rank-name { color: #ffd93d; } -.rank-2 .rank-name { color: #00ce9e; } +.rank-1 .rank-name { color: var(--warning); } +.rank-2 .rank-name { color: var(--success); } .rank-3 .rank-name { color: #748ffc; } .rank-rate { font-weight: 700; - color: #00ce9e; + color: var(--success); text-align: right; } .rank-desc { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -1588,8 +1588,8 @@ .deep-stock-code { font-size: 12px; color: rgba(255,255,255,0.5); margin-top: 2px; } .deep-price { font-size: 22px; font-weight: 700; color: #fff; text-align: center; } .deep-change { font-size: 14px; text-align: center; margin-top: 2px; } -.deep-change.up { color: #f44336; } -.deep-change.down { color: #4caf50; } +.deep-change.up { color: var(--up); } +.deep-change.down { color: var(--down); } .deep-score-circle { width: 50px; height: 50px; border-radius: 50%; @@ -1598,11 +1598,49 @@ font-weight: 700; } .score-num { font-size: 20px; color: #fff; } -.deep-score-circle.score-high { background: linear-gradient(135deg, #f44336, #ff5722); } -.deep-score-circle.score-mid { background: linear-gradient(135deg, #ff9800, #ffc107); } -.deep-score-circle.score-low { background: linear-gradient(135deg, #607d8b, #78909c); } +.deep-score-circle.score-high { background: linear-gradient(135deg, var(--danger), #ff5722); } +.deep-score-circle.score-mid { background: linear-gradient(135deg, var(--warning), #ffc107); } +.deep-score-circle.score-low { background: linear-gradient(135deg, var(--sig-55), #78909c); } .deep-verdict { text-align: center; font-size: 12px; color: rgba(255,255,255,0.6); margin-top: 4px; } +/* 三项得分明细 */ +.deep-score-breakdown { + display: flex; + gap: 8px; + padding: 10px 14px; + background: rgba(255,255,255,0.04); + border-radius: 10px; +} +.score-breakdown-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} +.score-breakdown-item .breakdown-label { + font-size: 11px; + color: var(--text-muted); + font-weight: 500; +} +.score-breakdown-item .breakdown-value { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + font-variant-numeric: tabular-nums; +} +.score-breakdown-item .breakdown-value.pos { color: var(--danger); } +.score-breakdown-item .breakdown-value.neg { color: var(--success); } +.score-breakdown-item.highlight { + background: rgba(108,92,231,0.12); + border-radius: 8px; + padding: 4px 0; +} +.score-breakdown-item.highlight .breakdown-value { + color: var(--primary); + font-size: 20px; +} + .deep-section { background: var(--card-bg, #1e1e2e); border-radius: 12px; diff --git a/stock-html/static/css/responsive.css b/stock-html/static/css/responsive.css index bb4b9cb..78719f3 100644 --- a/stock-html/static/css/responsive.css +++ b/stock-html/static/css/responsive.css @@ -235,3 +235,163 @@ grid-template-columns: repeat(4, 1fr); } } + +/* 平板过渡断点 (500-768px) */ +@media (min-width: 500px) and (max-width: 767px) { + .container { + max-width: 100%; + padding: 20px 24px; + } + + .nav-tabs-compact .nav-tab { + padding: 10px 16px; + font-size: 13px; + } + + .sub-tab { + padding: 10px 14px; + font-size: 13px; + } + + .card, + .input-section { + padding: 18px 20px; + border-radius: 16px; + } + + .alerts-summary.top-summary { + padding: 14px 20px; + gap: 10px 14px; + } + + .alerts-summary.top-summary .summary-value { + font-size: 18px; + } + + .signal-details { + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + + .summary-grid { + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + + .scan-summary-header { + padding: 12px 16px; + } + + .deep-score-breakdown { + padding: 12px 18px; + } + + .score-breakdown-item .breakdown-value { + font-size: 20px; + } +} + +/* 超大屏幕 (1440px+) 限制最大宽度避免过宽 */ +@media (min-width: 1440px) { + .container { + max-width: 1100px; + } +} + +/* ===== 打印样式 ===== */ +@media print { + /* 隐藏导航、操作按钮、非打印元素 */ + .nav-tabs, + .sub-tabs, + .title-bar .user-info, + .toast-container, + .confirm-overlay, + .modal-overlay, + .deep-input-card, + .deep-analyze-btn, + .alert-action-btn, + .scan-control, + .scan-btn, + .analyze-btn, + .trade-actions-bar, + .add-trade-btn, + .refresh-price-btn, + .buy-card-arrow, + .skeleton-card, + .no-print { + display: none !important; + } + + /* 重置背景为白色 */ + body { + background: #fff !important; + color: #000 !important; + } + + .container { + max-width: 100% !important; + padding: 0 !important; + } + + /* 卡片去阴影、改为黑白边框 */ + .deep-report, + .deep-header-card, + .deep-section, + .deep-ai-summary, + .buy-analysis-card, + .buy-card-detail, + .card, + .glass-card { + background: #fff !important; + border: 1px solid #ccc !important; + box-shadow: none !important; + border-radius: 4px !important; + margin-bottom: 8px !important; + padding: 10px 12px !important; + } + + /* 文字改为黑色 */ + .deep-stock-name, + .deep-stock-code, + .deep-price, + .score-num, + .deep-section-title, + .deep-ai-title, + .deep-ai-text, + .ai-action-tip, + .buy-card-name, + .buy-card-code, + .breakdown-label, + .breakdown-value { + color: #000 !important; + } + + /* 涨跌色保持可区分(改为深色) */ + .deep-change.up, .batch-change.up, .alert-change.up, + .positive, .trade-profit.profit, .holding-profit.profit { + color: #c0392b !important; + } + .deep-change.down, .batch-change.down, .alert-change.down, + .negative, .trade-profit.loss, .holding-profit.loss { + color: #27ae60 !important; + } + + /* 评分圆环保持色差 */ + .deep-score-circle.score-high { background: #c0392b !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } + .deep-score-circle.score-mid { background: #e67e22 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } + .deep-score-circle.score-low { background: #7f8c8d !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } + .score-num { color: #fff !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } + + /* SVG 保色 */ + svg { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + /* 避免分页截断 */ + .deep-header-card, + .deep-section, + .buy-analysis-card { + break-inside: avoid; + } +} diff --git a/stock-html/static/css/scan.css b/stock-html/static/css/scan.css index a1766c8..d332716 100644 --- a/stock-html/static/css/scan.css +++ b/stock-html/static/css/scan.css @@ -81,8 +81,8 @@ font-weight: 600; font-variant-numeric: tabular-nums; } -.batch-change.up { color: #ff4444; } -.batch-change.down { color: #00c853; } +.batch-change.up { color: var(--up); } +.batch-change.down { color: var(--down); } /* 信号计数徽章 */ .batch-count-badge { @@ -134,13 +134,13 @@ font-size: 10px; opacity: 0.9; } -.pill-s85 { color: #ff6666; background: rgba(255,68,68,0.12); } -.pill-s80 { color: #ff8a50; background: rgba(255,107,53,0.12); } -.pill-s75 { color: #66bb6a; background: rgba(76,175,80,0.12); } -.pill-s70 { color: #64b5f6; background: rgba(33,150,243,0.12); } -.pill-s65 { color: #ce93d8; background: rgba(156,39,176,0.10); } -.pill-s60 { color: #ffb74d; background: rgba(255,152,0,0.12); } -.pill-s55 { color: #90a4ae; background: rgba(96,125,139,0.12); } +.pill-s85 { color: var(--sig-85); background: rgba(255,68,68,0.12); } +.pill-s80 { color: var(--sig-80); background: rgba(255,107,53,0.12); } +.pill-s75 { color: var(--sig-75); background: rgba(76,175,80,0.12); } +.pill-s70 { color: var(--sig-70); background: rgba(33,150,243,0.12); } +.pill-s65 { color: var(--sig-65); background: rgba(156,39,176,0.10); } +.pill-s60 { color: var(--sig-60); background: rgba(255,152,0,0.12); } +.pill-s55 { color: var(--sig-55); background: rgba(96,125,139,0.12); } /* 描述区域 */ .batch-desc-area { @@ -160,13 +160,13 @@ flex-shrink: 0; margin-top: 6px; } -.dot-s85 { background: #ff4444; } -.dot-s80 { background: #ff6b35; } -.dot-s75 { background: #4CAF50; } -.dot-s70 { background: #2196F3; } -.dot-s65 { background: #9C27B0; } -.dot-s60 { background: #FF9800; } -.dot-s55 { background: #607D8B; } +.dot-s85 { background: var(--sig-85); } +.dot-s80 { background: var(--sig-80); } +.dot-s75 { background: var(--sig-75); } +.dot-s70 { background: var(--sig-70); } +.dot-s65 { background: var(--sig-65); } +.dot-s60 { background: var(--sig-60); } +.dot-s55 { background: var(--sig-55); } .desc-text { font-size: 11.5px; color: var(--text-secondary); @@ -198,11 +198,11 @@ .batch-rec-tag.rec-buy, .batch-rec-tag.rec-add { background: rgba(0,206,158,0.18); - color: #00ce9e; + color: var(--success); } .batch-rec-tag.rec-sell { background: rgba(255,68,68,0.18); - color: #ff4444; + color: var(--danger); } .batch-rec-tag.rec-watch, .batch-rec-tag.rec-hold { @@ -229,8 +229,8 @@ .scan-holding-note, .strategy-holding-note { font-size: 11.5px; - color: #ff9800; - background: rgba(255,152,0,0.08); + color: var(--warning); + background: rgba(255,170,0,0.08); padding: 4px 12px; margin: 4px 16px 0; border-radius: 6px; @@ -272,13 +272,13 @@ border-radius: 50%; flex-shrink: 0; } -.ss-dot-sm.s85 { background: #ff4444; } -.ss-dot-sm.s80 { background: #ff6b35; } -.ss-dot-sm.s75 { background: #4CAF50; } -.ss-dot-sm.s70 { background: #2196F3; } -.ss-dot-sm.s65 { background: #9C27B0; } -.ss-dot-sm.s60 { background: #FF9800; } -.ss-dot-sm.s55 { background: #607D8B; } +.ss-dot-sm.s85 { background: var(--sig-85); } +.ss-dot-sm.s80 { background: var(--sig-80); } +.ss-dot-sm.s75 { background: var(--sig-75); } +.ss-dot-sm.s70 { background: var(--sig-70); } +.ss-dot-sm.s65 { background: var(--sig-65); } +.ss-dot-sm.s60 { background: var(--sig-60); } +.ss-dot-sm.s55 { background: var(--sig-55); } /* 全量扫描触发区域 */ .full-scan-trigger-section { @@ -1016,3 +1016,26 @@ min-width: 28px; text-align: right; } + +/* 扫描卡片综合得分胶囊 */ +.scan-score-pill { + flex-shrink: 0; + font-size: 12px; + font-weight: 700; + padding: 2px 8px; + border-radius: 10px; + font-variant-numeric: tabular-nums; + cursor: help; +} +.scan-score-pill.score-high { + color: var(--danger); + background: rgba(255,68,68,0.12); +} +.scan-score-pill.score-mid { + color: var(--warning); + background: rgba(255,170,0,0.12); +} +.scan-score-pill.score-low { + color: var(--text-muted); + background: rgba(255,255,255,0.06); +} diff --git a/stock-html/static/js/app.js b/stock-html/static/js/app.js index 4eaf4f9..ed3c52b 100644 --- a/stock-html/static/js/app.js +++ b/stock-html/static/js/app.js @@ -1589,7 +1589,7 @@ try { const amount = parseFloat(this.cashInputValue); if (isNaN(amount)) { - alert('请输入有效金额'); + this.showToast('请输入有效金额', 'warning'); return; } const response = await axios.put('/api/available_cash', { amount }); @@ -1597,11 +1597,11 @@ this.availableCash = response.data.available_cash; this.editingCash = false; } else { - alert('保存失败: ' + (response.data.error || '未知错误')); + this.showToast('保存失败: ' + (response.data.error || '未知错误'), 'error'); } } catch (err) { console.error('保存可用资金失败:', err); - alert('保存失败'); + this.showToast('保存失败', 'error'); } }, @@ -2168,7 +2168,7 @@ async batchTechSignals() { if (!this.searchHistory || this.searchHistory.length === 0) { - alert('请先添加关注股票'); + this.showToast('请先添加关注股票', 'warning'); return; } this.techLoading = true; @@ -2220,6 +2220,7 @@ signal_type: signalTypes, holding_codes: (this.holdingStocks || []).join(','), recommend_text: this.fullScanFilterRecommend === 'all' ? '' : this.fullScanFilterRecommend, + with_scores: true, } }); if (resp.data.success) { diff --git a/stock-html/templates/admin.html b/stock-html/templates/admin.html index 322312e..ff73198 100644 --- a/stock-html/templates/admin.html +++ b/stock-html/templates/admin.html @@ -8,17 +8,19 @@