Compare commits
19 Commits
79e869eeda
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b4b66f0508 | |||
| c79f968ad7 | |||
| 88bc583f4d | |||
| 05a1dcc704 | |||
| cf7341fc63 | |||
| 037d9cd26c | |||
| 2697345670 | |||
| cf1023754a | |||
| cfeec11929 | |||
| 6a33fd534e | |||
| c5df676af6 | |||
| 4b2d75c7ef | |||
| 5f5fa7ae80 | |||
| 4e35ab852f | |||
| 40ce519188 | |||
| 9a5e24ccd6 | |||
| 488ef6b8e2 | |||
| b330fbb3e5 | |||
| 7235bbe130 |
+6
-3
@@ -552,7 +552,7 @@ def db_save_fundamental(code, data):
|
|||||||
|
|
||||||
# ========== 资金流向历史数据操作(数据库版) ==========
|
# ========== 资金流向历史数据操作(数据库版) ==========
|
||||||
|
|
||||||
def db_get_fund_flow_history(code):
|
def db_get_fund_flow_history(code, limit=60):
|
||||||
"""获取股票的资金流向历史数据"""
|
"""获取股票的资金流向历史数据"""
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if not conn:
|
if not conn:
|
||||||
@@ -564,11 +564,14 @@ def db_get_fund_flow_history(code):
|
|||||||
SELECT code, trade_date::text, close_price, change_pct,
|
SELECT code, trade_date::text, close_price, change_pct,
|
||||||
main_net_inflow, main_net_inflow_pct,
|
main_net_inflow, main_net_inflow_pct,
|
||||||
super_net_inflow, super_net_inflow_pct,
|
super_net_inflow, super_net_inflow_pct,
|
||||||
big_net_inflow, big_net_inflow_pct
|
big_net_inflow, big_net_inflow_pct,
|
||||||
|
mid_net_inflow, mid_net_inflow_pct,
|
||||||
|
small_net_inflow, small_net_inflow_pct
|
||||||
FROM stock_fund_flow_history
|
FROM stock_fund_flow_history
|
||||||
WHERE code = %s
|
WHERE code = %s
|
||||||
ORDER BY trade_date DESC
|
ORDER BY trade_date DESC
|
||||||
""", (code,))
|
LIMIT %s
|
||||||
|
""", (code, limit))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
# 获取最新日期
|
# 获取最新日期
|
||||||
|
|||||||
+16
-16
@@ -14,7 +14,7 @@
|
|||||||
信号检测层(signal_detector.py) ← 第三章
|
信号检测层(signal_detector.py) ← 第三章
|
||||||
↓ 基于7个信号检测买卖点
|
↓ 基于7个信号检测买卖点
|
||||||
外部因素模块(fund_flow/sentiment/external/news)← 第四章
|
外部因素模块(fund_flow/sentiment/external/news)← 第四章
|
||||||
↓ 资金面/情绪/北向/美股/商品/公告/政策/汇率 → 各因素评分
|
↓ 资金面/情绪/南向/美股/商品/公告/政策/汇率 → 各因素评分
|
||||||
算法决策层(stock_algorithms.py) ← 第五章
|
算法决策层(stock_algorithms.py) ← 第五章
|
||||||
↓ 统一推荐逻辑、牛股阶段识别、技术面深度分析(7维度)→ 技术面基础分
|
↓ 统一推荐逻辑、牛股阶段识别、技术面深度分析(7维度)→ 技术面基础分
|
||||||
综合评分引擎(score_engine.py)
|
综合评分引擎(score_engine.py)
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 技术指标层 | `services/technical_indicators.py` | 计算 MACD、SKDJ、KDJ、EMA、MA 等基础指标 | 二 |
|
| 技术指标层 | `services/technical_indicators.py` | 计算 MACD、SKDJ、KDJ、EMA、MA 等基础指标 | 二 |
|
||||||
| 信号检测层 | `services/signal_detector.py` | 基于7个信号检测买卖点 | 三 |
|
| 信号检测层 | `services/signal_detector.py` | 基于7个信号检测买卖点 | 三 |
|
||||||
| 外部因素模块 | `fund_flow_analyzer.py` / `market_sentiment.py` / `external_factors.py` / `news_analyzer.py` | 资金面、市场情绪、北向资金、美股、大宗商品、公告/政策、汇率 | 四 |
|
| 外部因素模块 | `fund_flow_analyzer.py` / `market_sentiment.py` / `external_factors.py` / `news_analyzer.py` | 资金面、市场情绪、南向资金、美股、大宗商品、公告/政策、汇率 | 四 |
|
||||||
| 算法决策层 | `services/stock_algorithms.py` | 统一推荐逻辑、牛股阶段识别、技术面深度分析 | 五 |
|
| 算法决策层 | `services/stock_algorithms.py` | 统一推荐逻辑、牛股阶段识别、技术面深度分析 | 五 |
|
||||||
| 综合评分引擎 | `services/score_engine.py` | 整合外部因素(P0-P7)与技术面评分,输出最终评分 | 五 |
|
| 综合评分引擎 | `services/score_engine.py` | 整合外部因素(P0-P7)与技术面评分,输出最终评分 | 五 |
|
||||||
|
|
||||||
@@ -258,21 +258,21 @@
|
|||||||
- **API端点**:`GET /api/market_sentiment`
|
- **API端点**:`GET /api/market_sentiment`
|
||||||
- **评分范围**:±10
|
- **评分范围**:±10
|
||||||
|
|
||||||
### 4.3 北向资金(P2,已实现)
|
### 4.3 南向资金(P2,已实现)
|
||||||
|
|
||||||
**白话解释**:北向资金是从香港流入A股的"外资",被市场视为"聪明钱"。北向大幅买入通常被视为利好信号。
|
**白话解释**:南向资金是内地通过港股通配置港股的资金,反映跨境资金情绪。南向大幅流入说明内地资金积极配置港股,大中华区risk-on,对A股偏正面。注:2024年8月起北向资金实时数据已停止公布,改用南向资金作为替代指标。
|
||||||
|
|
||||||
| 信号 | 含义 | 可靠度 | 评分 |
|
| 信号 | 含义 | 可靠度 | 评分 |
|
||||||
|------|------|--------|------|
|
|------|------|--------|------|
|
||||||
| 北向单日净流入>50亿 | 外资看好,市场偏多 | ★★★★ | +5 |
|
| 南向单日净流入>80亿 | 跨境资金情绪偏暖 | ★★★★ | +5 |
|
||||||
| 北向单日净流出>50亿 | 外资看空,注意风险 | ★★★★ | -5 |
|
| 南向单日净流出>80亿 | 跨境资金情绪偏冷 | ★★★★ | -5 |
|
||||||
| 北向连续3日净流入 | 外资持续看好,中期偏多 | ★★★★★ | +3 |
|
| 南向连续3日净流入 | 资金持续流入,中期偏多 | ★★★★★ | +3 |
|
||||||
| 北向连续3日净流出 | 外资持续撤离,中期偏空 | ★★★★ | -3 |
|
| 南向连续3日净流出 | 资金持续流出,中期偏空 | ★★★★ | -3 |
|
||||||
|
|
||||||
#### 实现模块
|
#### 实现模块
|
||||||
|
|
||||||
- **模块文件**:`services/external_factors.py` → `get_northbound_capital()`
|
- **模块文件**:`services/external_factors.py` → `get_southbound_capital()`
|
||||||
- **数据来源**:AKShare `stock_hsgt_north_net_flow_in_em`(北向资金净流入)
|
- **数据来源**:AKShare `stock_hsgt_fund_flow_summary_em`(今日汇总)+ `stock_hsgt_hist_em`(南向资金历史)
|
||||||
- **评分范围**:±10
|
- **评分范围**:±10
|
||||||
|
|
||||||
### 4.4 美股隔夜板块变化(P3,已实现)
|
### 4.4 美股隔夜板块变化(P3,已实现)
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
资金流向数据 → 资金信号 ────────────────────────────────┤
|
资金流向数据 → 资金信号 ────────────────────────────────┤
|
||||||
美股隔夜数据 → 外盘情绪 ────────────────────────────────┤→ 综合评分引擎 → 最终评分 → 买卖建议/AI解说
|
美股隔夜数据 → 外盘情绪 ────────────────────────────────┤→ 综合评分引擎 → 最终评分 → 买卖建议/AI解说
|
||||||
公告/新闻 → LLM情感分析 ───────────────────────────────┤
|
公告/新闻 → LLM情感分析 ───────────────────────────────┤
|
||||||
北向资金 → 外资动向 ────────────────────────────────────┤
|
南向资金 → 跨境资金情绪 ────────────────────────────────┤
|
||||||
市场情绪指标 → 情绪评分 ────────────────────────────────┤
|
市场情绪指标 → 情绪评分 ────────────────────────────────┤
|
||||||
大宗商品 → 板块影响 ────────────────────────────────────┤
|
大宗商品 → 板块影响 ────────────────────────────────────┤
|
||||||
汇率 → 进出口影响 ──────────────────────────────────────┘
|
汇率 → 进出口影响 ──────────────────────────────────────┘
|
||||||
@@ -426,7 +426,7 @@
|
|||||||
| 技术面基础分 | 0-100 | `compute_deep_analysis` 原始分 |
|
| 技术面基础分 | 0-100 | `compute_deep_analysis` 原始分 |
|
||||||
| P0 资金面 | ±20 | 连续流入+10,吸筹+8,大单突击+5 |
|
| P0 资金面 | ±20 | 连续流入+10,吸筹+8,大单突击+5 |
|
||||||
| P1 市场情绪 | ±10 | 涨跌停比+5/-5,连板+3,成交额+2/-2 |
|
| P1 市场情绪 | ±10 | 涨跌停比+5/-5,连板+3,成交额+2/-2 |
|
||||||
| P2 北向资金 | ±10 | 大幅流入+5,连续流入+3 |
|
| P2 南向资金 | ±10 | 大幅流入+5,连续流入+3 |
|
||||||
| P3 美股外盘 | ±10 | 美股大涨+5,大跌-5 |
|
| P3 美股外盘 | ±10 | 美股大涨+5,大跌-5 |
|
||||||
| P4 大宗商品 | ±5 | 单品种涨跌±1 |
|
| P4 大宗商品 | ±5 | 单品种涨跌±1 |
|
||||||
| P5 公告/异动 | ±15 | 并购+10,业绩预增+8,异动±5 |
|
| P5 公告/异动 | ±15 | 并购+10,业绩预增+8,异动±5 |
|
||||||
@@ -445,7 +445,7 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 资金流向分析 | `services/fund_flow_analyzer.py` | 从DB读取资金流向,计算连续流入/流出、量价背离、大单突击 |
|
| 资金流向分析 | `services/fund_flow_analyzer.py` | 从DB读取资金流向,计算连续流入/流出、量价背离、大单突击 |
|
||||||
| 市场情绪指标 | `services/market_sentiment.py` | 从实时行情表计算涨停跌停比、连板高度、换手率中位数、两市成交额 |
|
| 市场情绪指标 | `services/market_sentiment.py` | 从实时行情表计算涨停跌停比、连板高度、换手率中位数、两市成交额 |
|
||||||
| 外部因素 | `services/external_factors.py` | 北向资金、美股隔夜板块、大宗商品、汇率变化 |
|
| 外部因素 | `services/external_factors.py` | 南向资金、美股隔夜板块、大宗商品、汇率变化 |
|
||||||
| 新闻/公告分析 | `services/news_analyzer.py` | 公告采集+分类、LLM情感分析、异动检测、政策面监控 |
|
| 新闻/公告分析 | `services/news_analyzer.py` | 公告采集+分类、LLM情感分析、异动检测、政策面监控 |
|
||||||
| 综合评分引擎 | `services/score_engine.py` | 汇总技术面+所有外部因素,输出最终评分 |
|
| 综合评分引擎 | `services/score_engine.py` | 汇总技术面+所有外部因素,输出最终评分 |
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@
|
|||||||
| 端点 | 方法 | 说明 |
|
| 端点 | 方法 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `/api/market_sentiment` | GET | 市场情绪指标 |
|
| `/api/market_sentiment` | GET | 市场情绪指标 |
|
||||||
| `/api/external_factors` | GET | 外部因素综合数据(北向/美股/商品/汇率) |
|
| `/api/external_factors` | GET | 外部因素综合数据(南向/美股/商品/汇率) |
|
||||||
| `/api/fund_flow_analysis/<code>` | GET | 个股资金流向分析 |
|
| `/api/fund_flow_analysis/<code>` | GET | 个股资金流向分析 |
|
||||||
| `/api/news_analysis/<code>` | GET | 个股消息面分析(公告+政策+异动) |
|
| `/api/news_analysis/<code>` | GET | 个股消息面分析(公告+政策+异动) |
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ deep_analyze 接口调用流程:
|
|||||||
|------|------|----------|------|
|
|------|------|----------|------|
|
||||||
| P0 资金面 | `fund_flow_analyzer` | ±20 | 主力在买还是在卖?有没有暗中吸筹/出货? |
|
| P0 资金面 | `fund_flow_analyzer` | ±20 | 主力在买还是在卖?有没有暗中吸筹/出货? |
|
||||||
| P1 市场情绪 | `market_sentiment` | ±10 | 今天涨停的股票多还是跌停的多?市场热不热? |
|
| P1 市场情绪 | `market_sentiment` | ±10 | 今天涨停的股票多还是跌停的多?市场热不热? |
|
||||||
| P2 北向资金 | `external_factors` | ±10 | 外资今天是买还是卖? |
|
| P2 南向资金 | `external_factors` | ±10 | 跨境资金今天是流入还是流出? |
|
||||||
| P3 美股外盘 | `external_factors` | ±10 | 昨晚美股涨了还是跌了? |
|
| P3 美股外盘 | `external_factors` | ±10 | 昨晚美股涨了还是跌了? |
|
||||||
| P4 大宗商品 | `external_factors` | ±5 | 原油/黄金/铜的价格变化对相关板块的影响 |
|
| P4 大宗商品 | `external_factors` | ±5 | 原油/黄金/铜的价格变化对相关板块的影响 |
|
||||||
| P5 公告/异动 | `news_analyzer` | ±15 | 有没有并购/业绩预告等重大消息?股价有没有异动? |
|
| P5 公告/异动 | `news_analyzer` | ±15 | 有没有并购/业绩预告等重大消息?股价有没有异动? |
|
||||||
@@ -681,7 +681,7 @@ deep_analyze 接口调用流程:
|
|||||||
5. 形态识别(发现了什么技术形态)
|
5. 形态识别(发现了什么技术形态)
|
||||||
6. 综合建议(根据评分给出操作建议)
|
6. 综合建议(根据评分给出操作建议)
|
||||||
7. 空间估算(风险收益比如何)
|
7. 空间估算(风险收益比如何)
|
||||||
8. 外部因素(资金面/市场情绪/北向资金/美股/消息面等综合影响)
|
8. 外部因素(资金面/市场情绪/南向资金/美股/消息面等综合影响)
|
||||||
|
|
||||||
还可以调用豆包 LLM 对规则文本进行润色,让表达更自然生动。
|
还可以调用豆包 LLM 对规则文本进行润色,让表达更自然生动。
|
||||||
|
|
||||||
|
|||||||
+16
-16
@@ -15,7 +15,7 @@
|
|||||||
信号检测层(signal_detector.py) ← 第三章
|
信号检测层(signal_detector.py) ← 第三章
|
||||||
↓ 基于7个信号检测买卖点
|
↓ 基于7个信号检测买卖点
|
||||||
外部因素模块(fund_flow/sentiment/external/news)← 第四章
|
外部因素模块(fund_flow/sentiment/external/news)← 第四章
|
||||||
↓ 资金面/情绪/北向/美股/商品/公告/政策/汇率 → 各因素评分
|
↓ 资金面/情绪/南向/美股/商品/公告/政策/汇率 → 各因素评分
|
||||||
算法决策层(stock_algorithms.py) ← 第五章
|
算法决策层(stock_algorithms.py) ← 第五章
|
||||||
↓ 统一推荐逻辑、牛股阶段识别、技术面深度分析(7维度)→ 技术面基础分
|
↓ 统一推荐逻辑、牛股阶段识别、技术面深度分析(7维度)→ 技术面基础分
|
||||||
综合评分引擎(score_engine.py) ← 第五章
|
综合评分引擎(score_engine.py) ← 第五章
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 技术指标层 | `services/technical_indicators.py` | 计算 MACD、SKDJ、KDJ、EMA、MA 等基础指标 | 二 |
|
| 技术指标层 | `services/technical_indicators.py` | 计算 MACD、SKDJ、KDJ、EMA、MA 等基础指标 | 二 |
|
||||||
| 信号检测层 | `services/signal_detector.py` | 基于7个信号检测买卖点 | 三 |
|
| 信号检测层 | `services/signal_detector.py` | 基于7个信号检测买卖点 | 三 |
|
||||||
| 外部因素模块 | `fund_flow_analyzer.py` / `market_sentiment.py` / `external_factors.py` / `news_analyzer.py` | 资金面、市场情绪、北向资金、美股、大宗商品、公告/政策、汇率 | 四 |
|
| 外部因素模块 | `fund_flow_analyzer.py` / `market_sentiment.py` / `external_factors.py` / `news_analyzer.py` | 资金面、市场情绪、南向资金、美股、大宗商品、公告/政策、汇率 | 四 |
|
||||||
| 算法决策层 | `services/stock_algorithms.py` | 统一推荐逻辑、牛股阶段识别、技术面深度分析 | 五 |
|
| 算法决策层 | `services/stock_algorithms.py` | 统一推荐逻辑、牛股阶段识别、技术面深度分析 | 五 |
|
||||||
| 综合评分引擎 | `services/score_engine.py` | 整合外部因素(P0-P7)与技术面评分,输出最终评分 | 五 |
|
| 综合评分引擎 | `services/score_engine.py` | 整合外部因素(P0-P7)与技术面评分,输出最终评分 | 五 |
|
||||||
|
|
||||||
@@ -259,21 +259,21 @@
|
|||||||
- **API端点**:`GET /api/market_sentiment`
|
- **API端点**:`GET /api/market_sentiment`
|
||||||
- **评分范围**:±10
|
- **评分范围**:±10
|
||||||
|
|
||||||
### 4.3 北向资金(P2,已实现)
|
### 4.3 南向资金(P2,已实现)
|
||||||
|
|
||||||
**白话解释**:北向资金是从香港流入A股的"外资",被市场视为"聪明钱"。北向大幅买入通常被视为利好信号。
|
**白话解释**:南向资金是内地通过港股通配置港股的资金,反映跨境资金情绪。南向大幅流入说明内地资金积极配置港股,大中华区risk-on,对A股偏正面。注:2024年8月起北向资金实时数据已停止公布,改用南向资金作为替代指标。
|
||||||
|
|
||||||
| 信号 | 含义 | 可靠度 | 评分 |
|
| 信号 | 含义 | 可靠度 | 评分 |
|
||||||
|------|------|--------|------|
|
|------|------|--------|------|
|
||||||
| 北向单日净流入>50亿 | 外资看好,市场偏多 | ★★★★ | +5 |
|
| 南向单日净流入>80亿 | 跨境资金情绪偏暖 | ★★★★ | +5 |
|
||||||
| 北向单日净流出>50亿 | 外资看空,注意风险 | ★★★★ | -5 |
|
| 南向单日净流出>80亿 | 跨境资金情绪偏冷 | ★★★★ | -5 |
|
||||||
| 北向连续3日净流入 | 外资持续看好,中期偏多 | ★★★★★ | +3 |
|
| 南向连续3日净流入 | 资金持续流入,中期偏多 | ★★★★★ | +3 |
|
||||||
| 北向连续3日净流出 | 外资持续撤离,中期偏空 | ★★★★ | -3 |
|
| 南向连续3日净流出 | 资金持续流出,中期偏空 | ★★★★ | -3 |
|
||||||
|
|
||||||
#### 实现模块
|
#### 实现模块
|
||||||
|
|
||||||
- **模块文件**:`services/external_factors.py` → `get_northbound_capital()`
|
- **模块文件**:`services/external_factors.py` → `get_southbound_capital()`
|
||||||
- **数据来源**:AKShare `stock_hsgt_north_net_flow_in_em`(北向资金净流入)
|
- **数据来源**:AKShare `stock_hsgt_fund_flow_summary_em`(今日汇总)+ `stock_hsgt_hist_em`(南向资金历史)
|
||||||
- **评分范围**:±10
|
- **评分范围**:±10
|
||||||
|
|
||||||
### 4.4 美股隔夜板块变化(P3,已实现)
|
### 4.4 美股隔夜板块变化(P3,已实现)
|
||||||
@@ -414,7 +414,7 @@
|
|||||||
资金流向数据 → 资金信号 ────────────────────────────────┤
|
资金流向数据 → 资金信号 ────────────────────────────────┤
|
||||||
美股隔夜数据 → 外盘情绪 ────────────────────────────────┤→ 综合评分引擎 → 最终评分 → 买卖建议/AI解说
|
美股隔夜数据 → 外盘情绪 ────────────────────────────────┤→ 综合评分引擎 → 最终评分 → 买卖建议/AI解说
|
||||||
公告/新闻 → LLM情感分析 ───────────────────────────────┤
|
公告/新闻 → LLM情感分析 ───────────────────────────────┤
|
||||||
北向资金 → 外资动向 ────────────────────────────────────┤
|
南向资金 → 跨境资金情绪 ────────────────────────────────┤
|
||||||
市场情绪指标 → 情绪评分 ────────────────────────────────┤
|
市场情绪指标 → 情绪评分 ────────────────────────────────┤
|
||||||
大宗商品 → 板块影响 ────────────────────────────────────┤
|
大宗商品 → 板块影响 ────────────────────────────────────┤
|
||||||
汇率 → 进出口影响 ──────────────────────────────────────┘
|
汇率 → 进出口影响 ──────────────────────────────────────┘
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
| 技术面基础分 | 0-100 | `compute_deep_analysis` 原始分 |
|
| 技术面基础分 | 0-100 | `compute_deep_analysis` 原始分 |
|
||||||
| P0 资金面 | ±20 | 连续流入+10,吸筹+8,大单突击+5 |
|
| P0 资金面 | ±20 | 连续流入+10,吸筹+8,大单突击+5 |
|
||||||
| P1 市场情绪 | ±10 | 涨跌停比+5/-5,连板+3,成交额+2/-2 |
|
| P1 市场情绪 | ±10 | 涨跌停比+5/-5,连板+3,成交额+2/-2 |
|
||||||
| P2 北向资金 | ±10 | 大幅流入+5,连续流入+3 |
|
| P2 南向资金 | ±10 | 大幅流入+5,连续流入+3 |
|
||||||
| P3 美股外盘 | ±10 | 美股大涨+5,大跌-5 |
|
| P3 美股外盘 | ±10 | 美股大涨+5,大跌-5 |
|
||||||
| P4 大宗商品 | ±5 | 单品种涨跌±1 |
|
| P4 大宗商品 | ±5 | 单品种涨跌±1 |
|
||||||
| P5 公告/异动 | ±15 | 并购+10,业绩预增+8,异动±5 |
|
| P5 公告/异动 | ±15 | 并购+10,业绩预增+8,异动±5 |
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 资金流向分析 | `services/fund_flow_analyzer.py` | 从DB读取资金流向,计算连续流入/流出、量价背离、大单突击 |
|
| 资金流向分析 | `services/fund_flow_analyzer.py` | 从DB读取资金流向,计算连续流入/流出、量价背离、大单突击 |
|
||||||
| 市场情绪指标 | `services/market_sentiment.py` | 从实时行情表计算涨停跌停比、连板高度、换手率中位数、两市成交额 |
|
| 市场情绪指标 | `services/market_sentiment.py` | 从实时行情表计算涨停跌停比、连板高度、换手率中位数、两市成交额 |
|
||||||
| 外部因素 | `services/external_factors.py` | 北向资金、美股隔夜板块、大宗商品、汇率变化 |
|
| 外部因素 | `services/external_factors.py` | 南向资金、美股隔夜板块、大宗商品、汇率变化 |
|
||||||
| 新闻/公告分析 | `services/news_analyzer.py` | 公告采集+分类、LLM情感分析、异动检测、政策面监控 |
|
| 新闻/公告分析 | `services/news_analyzer.py` | 公告采集+分类、LLM情感分析、异动检测、政策面监控 |
|
||||||
| 综合评分引擎 | `services/score_engine.py` | 汇总技术面+所有外部因素,输出最终评分 |
|
| 综合评分引擎 | `services/score_engine.py` | 汇总技术面+所有外部因素,输出最终评分 |
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@
|
|||||||
| 端点 | 方法 | 说明 |
|
| 端点 | 方法 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `/api/market_sentiment` | GET | 市场情绪指标 |
|
| `/api/market_sentiment` | GET | 市场情绪指标 |
|
||||||
| `/api/external_factors` | GET | 外部因素综合数据(北向/美股/商品/汇率) |
|
| `/api/external_factors` | GET | 外部因素综合数据(南向/美股/商品/汇率) |
|
||||||
| `/api/fund_flow_analysis/<code>` | GET | 个股资金流向分析 |
|
| `/api/fund_flow_analysis/<code>` | GET | 个股资金流向分析 |
|
||||||
| `/api/news_analysis/<code>` | GET | 个股消息面分析(公告+政策+异动) |
|
| `/api/news_analysis/<code>` | GET | 个股消息面分析(公告+政策+异动) |
|
||||||
|
|
||||||
@@ -670,7 +670,7 @@ deep_analyze 接口调用流程:
|
|||||||
|------|------|----------|------|
|
|------|------|----------|------|
|
||||||
| P0 资金面 | `fund_flow_analyzer` | ±20 | 主力在买还是在卖?有没有暗中吸筹/出货? |
|
| P0 资金面 | `fund_flow_analyzer` | ±20 | 主力在买还是在卖?有没有暗中吸筹/出货? |
|
||||||
| P1 市场情绪 | `market_sentiment` | ±10 | 今天涨停的股票多还是跌停的多?市场热不热? |
|
| P1 市场情绪 | `market_sentiment` | ±10 | 今天涨停的股票多还是跌停的多?市场热不热? |
|
||||||
| P2 北向资金 | `external_factors` | ±10 | 外资今天是买还是卖? |
|
| P2 南向资金 | `external_factors` | ±10 | 跨境资金今天是流入还是流出? |
|
||||||
| P3 美股外盘 | `external_factors` | ±10 | 昨晚美股涨了还是跌了? |
|
| P3 美股外盘 | `external_factors` | ±10 | 昨晚美股涨了还是跌了? |
|
||||||
| P4 大宗商品 | `external_factors` | ±5 | 原油/黄金/铜的价格变化对相关板块的影响 |
|
| P4 大宗商品 | `external_factors` | ±5 | 原油/黄金/铜的价格变化对相关板块的影响 |
|
||||||
| P5 公告/异动 | `news_analyzer` | ±15 | 有没有并购/业绩预告等重大消息?股价有没有异动? |
|
| P5 公告/异动 | `news_analyzer` | ±15 | 有没有并购/业绩预告等重大消息?股价有没有异动? |
|
||||||
@@ -772,7 +772,7 @@ deep_analyze 接口调用流程:
|
|||||||
5. 形态识别(发现了什么技术形态)
|
5. 形态识别(发现了什么技术形态)
|
||||||
6. 综合建议(根据**综合得分**给出操作建议)
|
6. 综合建议(根据**综合得分**给出操作建议)
|
||||||
7. 空间估算(风险收益比如何)
|
7. 空间估算(风险收益比如何)
|
||||||
8. 外部因素(资金面/市场情绪/北向资金/美股/消息面等综合影响)
|
8. 外部因素(资金面/市场情绪/南向资金/美股/消息面等综合影响)
|
||||||
9. **三项得分汇总**(技术得分、外部得分、综合得分)
|
9. **三项得分汇总**(技术得分、外部得分、综合得分)
|
||||||
|
|
||||||
还可以调用豆包 LLM 对规则文本进行润色,让表达更自然生动。
|
还可以调用豆包 LLM 对规则文本进行润色,让表达更自然生动。
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ def get_all_stock_codes(conn):
|
|||||||
SELECT code, name FROM stock_realtime_price
|
SELECT code, name FROM stock_realtime_price
|
||||||
WHERE volume > 0 AND price > 0
|
WHERE volume > 0 AND price > 0
|
||||||
AND name NOT LIKE '%%退%%'
|
AND name NOT LIKE '%%退%%'
|
||||||
|
AND name NOT LIKE '%%ST%%'
|
||||||
AND name NOT LIKE 'PT%%'
|
AND name NOT LIKE 'PT%%'
|
||||||
ORDER BY code
|
ORDER BY code
|
||||||
""")
|
""")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
flask>=2.0.0
|
flask>=2.0.0
|
||||||
flask-cors>=3.0.0
|
flask-cors>=3.0.0
|
||||||
|
requests>=2.25.0
|
||||||
akshare>=1.10.0
|
akshare>=1.10.0
|
||||||
pandas>=1.5.0
|
pandas>=1.5.0
|
||||||
numpy>=1.20.0
|
numpy>=1.20.0
|
||||||
|
|||||||
+23
-28
@@ -2,7 +2,7 @@
|
|||||||
管理后台 API 路由
|
管理后台 API 路由
|
||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, jsonify, session, render_template
|
from flask import Blueprint, request, jsonify, session, render_template
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
import functools
|
import functools
|
||||||
@@ -182,7 +182,7 @@ def admin_required(f):
|
|||||||
if not row or not row[0]:
|
if not row or not row[0]:
|
||||||
return jsonify({'success': False, 'error': '无管理员权限'}), 403
|
return jsonify({'success': False, 'error': '无管理员权限'}), 403
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ def admin_page():
|
|||||||
if not row or not row[0]:
|
if not row or not row[0]:
|
||||||
return '无权访问', 403
|
return '无权访问', 403
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
return render_template('admin.html')
|
return render_template('admin.html')
|
||||||
|
|
||||||
|
|
||||||
@@ -281,7 +281,7 @@ def dashboard():
|
|||||||
|
|
||||||
return jsonify({'success': True, 'data': stats})
|
return jsonify({'success': True, 'data': stats})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ========== 用户管理 ==========
|
# ========== 用户管理 ==========
|
||||||
@@ -339,7 +339,7 @@ def list_users():
|
|||||||
|
|
||||||
return jsonify({'success': True, 'data': users})
|
return jsonify({'success': True, 'data': users})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/admin/users/<int:user_id>', methods=['GET'])
|
@bp.route('/api/admin/users/<int:user_id>', methods=['GET'])
|
||||||
@@ -437,7 +437,7 @@ def get_user_detail(user_id):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/admin/users/<int:user_id>/reset_password', methods=['POST'])
|
@bp.route('/api/admin/users/<int:user_id>/reset_password', methods=['POST'])
|
||||||
@@ -461,7 +461,7 @@ def reset_user_password(user_id):
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/admin/users/<int:user_id>/toggle_admin', methods=['POST'])
|
@bp.route('/api/admin/users/<int:user_id>/toggle_admin', methods=['POST'])
|
||||||
@@ -483,7 +483,7 @@ def toggle_admin(user_id):
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/admin/users/<int:user_id>', methods=['DELETE'])
|
@bp.route('/api/admin/users/<int:user_id>', methods=['DELETE'])
|
||||||
@@ -509,7 +509,7 @@ def delete_user(user_id):
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ========== 用户数据管理 ==========
|
# ========== 用户数据管理 ==========
|
||||||
@@ -526,7 +526,7 @@ def delete_user_watchlist(user_id, code):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/admin/users/<int:user_id>/trades/<int:trade_id>', methods=['DELETE'])
|
@bp.route('/api/admin/users/<int:user_id>/trades/<int:trade_id>', methods=['DELETE'])
|
||||||
@@ -541,7 +541,7 @@ def delete_user_trade(user_id, trade_id):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ========== 系统数据 ==========
|
# ========== 系统数据 ==========
|
||||||
@@ -568,7 +568,7 @@ def scan_history():
|
|||||||
""")
|
""")
|
||||||
return jsonify({'success': True, 'data': [dict(r) for r in cur.fetchall()]})
|
return jsonify({'success': True, 'data': [dict(r) for r in cur.fetchall()]})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/api/admin/data_stats', methods=['GET'])
|
@bp.route('/api/admin/data_stats', methods=['GET'])
|
||||||
@@ -603,7 +603,7 @@ def data_stats():
|
|||||||
|
|
||||||
return jsonify({'success': True, 'data': {'tables': result, 'logs': logs}})
|
return jsonify({'success': True, 'data': {'tables': result, 'logs': logs}})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ========== 全景扫描管理 ==========
|
# ========== 全景扫描管理 ==========
|
||||||
@@ -753,13 +753,9 @@ def trigger_kline_sync():
|
|||||||
def kline_sync_status():
|
def kline_sync_status():
|
||||||
"""管理员查询K线同步状态"""
|
"""管理员查询K线同步状态"""
|
||||||
try:
|
try:
|
||||||
import psycopg2
|
conn = get_db()
|
||||||
from config import Config
|
if not conn:
|
||||||
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
|
||||||
)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# K线数据统计
|
# K线数据统计
|
||||||
@@ -786,7 +782,6 @@ def kline_sync_status():
|
|||||||
total_stocks = cur.fetchone()[0]
|
total_stocks = cur.fetchone()[0]
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
is_running = _is_kline_sync_running()
|
is_running = _is_kline_sync_running()
|
||||||
|
|
||||||
@@ -862,6 +857,8 @@ def kline_sync_status():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ========== 定时任务监控 ==========
|
# ========== 定时任务监控 ==========
|
||||||
@@ -1282,13 +1279,10 @@ def admin_scan_status():
|
|||||||
"""管理员查询扫描进度"""
|
"""管理员查询扫描进度"""
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import psycopg2
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
conn = get_db()
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
if not conn:
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
scan_date = datetime.now().strftime('%Y-%m-%d')
|
scan_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
@@ -1308,7 +1302,6 @@ def admin_scan_status():
|
|||||||
triggered = cur.fetchone()[0]
|
triggered = cur.fetchone()[0]
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
is_running = _is_scan_running()
|
is_running = _is_scan_running()
|
||||||
|
|
||||||
@@ -1357,3 +1350,5 @@ def admin_scan_status():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from services.stock_algorithms import (
|
|||||||
)
|
)
|
||||||
from db import (
|
from db import (
|
||||||
login_required, get_current_user_id,
|
login_required, get_current_user_id,
|
||||||
db_get_alerts_cache, db_save_alerts_cache
|
db_get_alerts_cache, db_save_alerts_cache,
|
||||||
|
get_db, put_db
|
||||||
)
|
)
|
||||||
|
|
||||||
bp = Blueprint('analysis', __name__, url_prefix='/api')
|
bp = Blueprint('analysis', __name__, url_prefix='/api')
|
||||||
@@ -61,7 +62,7 @@ def analyze():
|
|||||||
# 3. 获取实时价格补充到结果(优先腾讯API,兼容腾讯云)
|
# 3. 获取实时价格补充到结果(优先腾讯API,兼容腾讯云)
|
||||||
try:
|
try:
|
||||||
import requests as _req
|
import requests as _req
|
||||||
_tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code
|
_tcode = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code
|
||||||
_r = _req.get(f'http://qt.gtimg.cn/q={_tcode}', timeout=5,
|
_r = _req.get(f'http://qt.gtimg.cn/q={_tcode}', timeout=5,
|
||||||
headers={'Referer': 'https://finance.qq.com'})
|
headers={'Referer': 'https://finance.qq.com'})
|
||||||
if _r.status_code == 200 and '\"' in _r.text:
|
if _r.status_code == 200 and '\"' in _r.text:
|
||||||
@@ -98,7 +99,7 @@ def deep_analyze():
|
|||||||
if not stock_code:
|
if not stock_code:
|
||||||
return jsonify({'error': '股票代码不能为空'}), 400
|
return jsonify({'error': '股票代码不能为空'}), 400
|
||||||
|
|
||||||
df = algo_get_kline_data(stock_code, days=180)
|
df = algo_get_kline_data(stock_code, days=120)
|
||||||
if df is None or len(df) < 30:
|
if df is None or len(df) < 30:
|
||||||
return jsonify({'error': 'K线数据不足'}), 400
|
return jsonify({'error': 'K线数据不足'}), 400
|
||||||
|
|
||||||
@@ -313,7 +314,7 @@ def ai_analyze_stream(stock_code):
|
|||||||
from flask import Response
|
from flask import Response
|
||||||
from services.doubao_api import analyze_stock_stream, format_fund_flow, format_market_cap
|
from services.doubao_api import analyze_stock_stream, format_fund_flow, format_market_cap
|
||||||
from services.mairui_api import get_realtime_price as mairui_price, get_financial_indicators
|
from services.mairui_api import get_realtime_price as mairui_price, get_financial_indicators
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
# 获取股票数据
|
# 获取股票数据
|
||||||
@@ -339,6 +340,7 @@ def ai_analyze_stream(stock_code):
|
|||||||
stock_name = get_stock_name(stock_code) or stock_code
|
stock_name = get_stock_name(stock_code) or stock_code
|
||||||
|
|
||||||
# 获取资金流向和技术信号
|
# 获取资金流向和技术信号
|
||||||
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if conn:
|
if conn:
|
||||||
@@ -383,12 +385,14 @@ def ai_analyze_stream(stock_code):
|
|||||||
stock_data['indicators'] = scan_row['indicators'] or {}
|
stock_data['indicators'] = scan_row['indicators'] or {}
|
||||||
stock_data['triggered_count'] = scan_row['triggered_count'] or 0
|
stock_data['triggered_count'] = scan_row['triggered_count'] or 0
|
||||||
cur2.close()
|
cur2.close()
|
||||||
|
|
||||||
conn.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取数据失败: {e}")
|
print(f"获取数据失败: {e}")
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
# 记录AI调用日志
|
# 记录AI调用日志
|
||||||
|
_conn = None
|
||||||
try:
|
try:
|
||||||
from flask import session as _sess
|
from flask import session as _sess
|
||||||
_uid = _sess.get('user_id')
|
_uid = _sess.get('user_id')
|
||||||
@@ -399,9 +403,11 @@ def ai_analyze_stream(stock_code):
|
|||||||
_cur.execute("INSERT INTO ai_call_log (user_id, stock_code, stock_name) VALUES (%s, %s, %s)",
|
_cur.execute("INSERT INTO ai_call_log (user_id, stock_code, stock_name) VALUES (%s, %s, %s)",
|
||||||
(_uid, stock_code, stock_name))
|
(_uid, stock_code, stock_name))
|
||||||
_conn.commit()
|
_conn.commit()
|
||||||
_conn.close()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
if _conn:
|
||||||
|
put_db(_conn)
|
||||||
|
|
||||||
# 流式调用AI
|
# 流式调用AI
|
||||||
for chunk in analyze_stock_stream(stock_code, stock_name, stock_data):
|
for chunk in analyze_stock_stream(stock_code, stock_name, stock_data):
|
||||||
@@ -421,7 +427,7 @@ def ai_analyze(stock_code):
|
|||||||
try:
|
try:
|
||||||
from services.doubao_api import analyze_stock
|
from services.doubao_api import analyze_stock
|
||||||
from services.mairui_api import get_realtime_price as mairui_price, get_financial_indicators
|
from services.mairui_api import get_realtime_price as mairui_price, get_financial_indicators
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
|
|
||||||
# 获取股票数据
|
# 获取股票数据
|
||||||
stock_data = {}
|
stock_data = {}
|
||||||
@@ -446,6 +452,7 @@ def ai_analyze(stock_code):
|
|||||||
stock_name = get_stock_name(stock_code) or stock_code
|
stock_name = get_stock_name(stock_code) or stock_code
|
||||||
|
|
||||||
# 获取近三日资金流向
|
# 获取近三日资金流向
|
||||||
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if conn:
|
if conn:
|
||||||
@@ -467,9 +474,11 @@ def ai_analyze(stock_code):
|
|||||||
'super_pct': float(row[3]) if row[3] else 0,
|
'super_pct': float(row[3]) if row[3] else 0,
|
||||||
})
|
})
|
||||||
stock_data['fund_flow_3days'] = fund_flow
|
stock_data['fund_flow_3days'] = fund_flow
|
||||||
conn.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取资金流向失败: {e}")
|
print(f"获取资金流向失败: {e}")
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
# 调用AI分析
|
# 调用AI分析
|
||||||
result = analyze_stock(stock_code, stock_name, stock_data)
|
result = analyze_stock(stock_code, stock_name, stock_data)
|
||||||
@@ -544,10 +553,8 @@ def batch_technical_signals():
|
|||||||
"""批量检测技术交易信号 — 优先从 stock_signal_scan 读取(与提醒一致),无记录时实时计算"""
|
"""批量检测技术交易信号 — 优先从 stock_signal_scan 读取(与提醒一致),无记录时实时计算"""
|
||||||
try:
|
try:
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from services.signal_detector import detect_all_signals
|
from services.signal_detector import detect_all_signals
|
||||||
from config import Config
|
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
codes = data.get('codes', [])
|
codes = data.get('codes', [])
|
||||||
@@ -566,10 +573,9 @@ def batch_technical_signals():
|
|||||||
change_map = {}
|
change_map = {}
|
||||||
scan_map = {}
|
scan_map = {}
|
||||||
try:
|
try:
|
||||||
conn = psycopg2.connect(
|
conn = get_db()
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
if not conn:
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
raise Exception('数据库连接失败')
|
||||||
)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
placeholders = ','.join(['%s'] * len(codes))
|
placeholders = ','.join(['%s'] * len(codes))
|
||||||
|
|
||||||
@@ -603,9 +609,10 @@ def batch_technical_signals():
|
|||||||
scan_map[row['code']] = row
|
scan_map[row['code']] = row
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"批量扫描获取数据失败: {e}")
|
print(f"批量扫描获取数据失败: {e}")
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
errors = []
|
errors = []
|
||||||
@@ -675,9 +682,6 @@ def batch_technical_signals():
|
|||||||
def get_scan_results():
|
def get_scan_results():
|
||||||
"""查询全量扫描结果"""
|
"""查询全量扫描结果"""
|
||||||
try:
|
try:
|
||||||
import psycopg2
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
scan_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
scan_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
||||||
min_triggered = int(request.args.get('min_triggered', 0))
|
min_triggered = int(request.args.get('min_triggered', 0))
|
||||||
signal_type = request.args.get('signal_type', '')
|
signal_type = request.args.get('signal_type', '')
|
||||||
@@ -689,10 +693,9 @@ def get_scan_results():
|
|||||||
holding_set = set(c.strip() for c in holding_codes_str.split(',') if c.strip())
|
holding_set = set(c.strip() for c in holding_codes_str.split(',') if c.strip())
|
||||||
recommend_text = (request.args.get('recommend_text') or '').strip()
|
recommend_text = (request.args.get('recommend_text') or '').strip()
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
conn = get_db()
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
if not conn:
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# 检查请求日期是否有数据,如果没有则自动回退到最近可用的扫描日期
|
# 检查请求日期是否有数据,如果没有则自动回退到最近可用的扫描日期
|
||||||
@@ -714,7 +717,8 @@ def get_scan_results():
|
|||||||
)
|
)
|
||||||
total_scanned = cur.fetchone()[0]
|
total_scanned = cur.fetchone()[0]
|
||||||
|
|
||||||
cur.execute("SELECT count(*) FROM stock_realtime_price")
|
cur.execute("""SELECT count(*) FROM stock_realtime_price
|
||||||
|
WHERE name NOT LIKE '%%退%%' AND name NOT LIKE '%%ST%%' AND name NOT LIKE 'PT%%'""")
|
||||||
total_stocks = cur.fetchone()[0]
|
total_stocks = cur.fetchone()[0]
|
||||||
|
|
||||||
where_clauses = ["scan_date = %s"]
|
where_clauses = ["scan_date = %s"]
|
||||||
@@ -890,6 +894,7 @@ def get_scan_results():
|
|||||||
count(*) as cnt
|
count(*) as cnt
|
||||||
FROM stock_signal_scan, jsonb_array_elements(signal_status) s
|
FROM stock_signal_scan, jsonb_array_elements(signal_status) s
|
||||||
WHERE scan_date = %s AND (s.value->>'triggered')::boolean = true
|
WHERE scan_date = %s AND (s.value->>'triggered')::boolean = true
|
||||||
|
AND name NOT LIKE '%%退%%' AND name NOT LIKE '%%ST%%'
|
||||||
GROUP BY s.value->>'name', s.value->>'type'
|
GROUP BY s.value->>'name', s.value->>'type'
|
||||||
ORDER BY cnt DESC
|
ORDER BY cnt DESC
|
||||||
""", (scan_date,))
|
""", (scan_date,))
|
||||||
@@ -901,6 +906,7 @@ def get_scan_results():
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT count(*) FROM stock_signal_scan
|
SELECT count(*) FROM stock_signal_scan
|
||||||
WHERE scan_date = %s AND triggered_count > 0
|
WHERE scan_date = %s AND triggered_count > 0
|
||||||
|
AND name NOT LIKE '%%退%%' AND name NOT LIKE '%%ST%%'
|
||||||
""", (scan_date,))
|
""", (scan_date,))
|
||||||
triggered_stocks = cur.fetchone()[0]
|
triggered_stocks = cur.fetchone()[0]
|
||||||
|
|
||||||
@@ -916,6 +922,7 @@ def get_scan_results():
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT code, signal_status, indicators, triggered_count
|
SELECT code, signal_status, indicators, triggered_count
|
||||||
FROM stock_signal_scan WHERE scan_date = %s
|
FROM stock_signal_scan WHERE scan_date = %s
|
||||||
|
AND name NOT LIKE '%%退%%' AND name NOT LIKE '%%ST%%'
|
||||||
""", (scan_date,))
|
""", (scan_date,))
|
||||||
recommend_counts = {}
|
recommend_counts = {}
|
||||||
for r in cur.fetchall():
|
for r in cur.fetchall():
|
||||||
@@ -925,7 +932,6 @@ def get_scan_results():
|
|||||||
recommend_counts[disp] = recommend_counts.get(disp, 0) + 1
|
recommend_counts[disp] = recommend_counts.get(disp, 0) + 1
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -945,6 +951,8 @@ def get_scan_results():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
def _compute_recommend(signal_status, indicators, triggered_count, is_holding):
|
def _compute_recommend(signal_status, indicators, triggered_count, is_holding):
|
||||||
@@ -955,9 +963,7 @@ def _compute_recommend(signal_status, indicators, triggered_count, is_holding):
|
|||||||
@bp.route('/signal_alerts', methods=['POST'])
|
@bp.route('/signal_alerts', methods=['POST'])
|
||||||
def signal_alerts():
|
def signal_alerts():
|
||||||
"""基于信号扫描结果生成买入/卖出/观望提醒(与扫描结果共用 _compute_recommend)"""
|
"""基于信号扫描结果生成买入/卖出/观望提醒(与扫描结果共用 _compute_recommend)"""
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from config import Config
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
@@ -968,10 +974,9 @@ def signal_alerts():
|
|||||||
if not stock_codes:
|
if not stock_codes:
|
||||||
return jsonify({'success': True, 'results': []})
|
return jsonify({'success': True, 'results': []})
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
conn = get_db()
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
if not conn:
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
# 优先今天的扫描数据,无则回退到最近可用日期
|
# 优先今天的扫描数据,无则回退到最近可用日期
|
||||||
@@ -1003,7 +1008,6 @@ def signal_alerts():
|
|||||||
WHERE code IN ({placeholders})
|
WHERE code IN ({placeholders})
|
||||||
""", stock_codes)
|
""", stock_codes)
|
||||||
price_rows = cur.fetchall()
|
price_rows = cur.fetchall()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
price_map = {}
|
price_map = {}
|
||||||
change_map = {}
|
change_map = {}
|
||||||
@@ -1085,6 +1089,8 @@ def signal_alerts():
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
def _is_scan_running():
|
def _is_scan_running():
|
||||||
@@ -1127,13 +1133,9 @@ def get_stock_score_detail(code):
|
|||||||
def get_scan_status():
|
def get_scan_status():
|
||||||
"""查询扫描进度"""
|
"""查询扫描进度"""
|
||||||
try:
|
try:
|
||||||
import psycopg2
|
conn = get_db()
|
||||||
from config import Config
|
if not conn:
|
||||||
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
|
||||||
)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
scan_date = datetime.now().strftime('%Y-%m-%d')
|
scan_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
@@ -1143,7 +1145,8 @@ def get_scan_status():
|
|||||||
)
|
)
|
||||||
scanned = cur.fetchone()[0]
|
scanned = cur.fetchone()[0]
|
||||||
|
|
||||||
cur.execute("SELECT count(*) FROM stock_realtime_price")
|
cur.execute("""SELECT count(*) FROM stock_realtime_price
|
||||||
|
WHERE name NOT LIKE '%%退%%' AND name NOT LIKE '%%ST%%' AND name NOT LIKE 'PT%%'""")
|
||||||
total = cur.fetchone()[0]
|
total = cur.fetchone()[0]
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -1153,7 +1156,6 @@ def get_scan_status():
|
|||||||
triggered = cur.fetchone()[0]
|
triggered = cur.fetchone()[0]
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -1167,6 +1169,8 @@ def get_scan_status():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/start_full_scan', methods=['POST'])
|
@bp.route('/start_full_scan', methods=['POST'])
|
||||||
@@ -1210,18 +1214,15 @@ def start_full_scan():
|
|||||||
def get_scan_strategy():
|
def get_scan_strategy():
|
||||||
"""基于体系最强战法,给出分梯队买卖建议。与全景扫描推荐共用 _compute_recommend,算法一致。"""
|
"""基于体系最强战法,给出分梯队买卖建议。与全景扫描推荐共用 _compute_recommend,算法一致。"""
|
||||||
try:
|
try:
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from config import Config
|
|
||||||
|
|
||||||
scan_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
scan_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
||||||
holding_codes_str = request.args.get('holding_codes', '')
|
holding_codes_str = request.args.get('holding_codes', '')
|
||||||
holding_set = set(c.strip() for c in holding_codes_str.split(',') if c.strip())
|
holding_set = set(c.strip() for c in holding_codes_str.split(',') if c.strip())
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
conn = get_db()
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
if not conn:
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
# 如果前端未指定日期,且当天无数据,自动回退到最近扫描日期
|
# 如果前端未指定日期,且当天无数据,自动回退到最近扫描日期
|
||||||
@@ -1241,7 +1242,6 @@ def get_scan_strategy():
|
|||||||
""", (scan_date,))
|
""", (scan_date,))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
tier1, tier2, tier3, tier4 = [], [], [], []
|
tier1, tier2, tier3, tier4 = [], [], [], []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
@@ -1322,10 +1322,12 @@ def get_scan_strategy():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
def _get_kline_data(stock_code, days=120):
|
def _get_kline_data(stock_code, days=120):
|
||||||
"""获取K线数据 — 委托给 services.stock_algorithms.get_kline_data(实时分析不用本地DB缓存)"""
|
"""获取K线数据 — 实时检测使用外部API获取最新数据,与扫描结果(本地DB快照)形成互补"""
|
||||||
return algo_get_kline_data(stock_code, days=days, use_local_db=False)
|
return algo_get_kline_data(stock_code, days=days, use_local_db=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -1345,19 +1347,16 @@ def get_bull_stocks():
|
|||||||
stage: 可选,筛选特定阶段(1-5)
|
stage: 可选,筛选特定阶段(1-5)
|
||||||
holdingStocks: 可选,持仓代码逗号分隔
|
holdingStocks: 可选,持仓代码逗号分隔
|
||||||
"""
|
"""
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from config import Config
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stage_filter = request.args.get('stage', type=int, default=0)
|
stage_filter = request.args.get('stage', type=int, default=0)
|
||||||
holding_str = request.args.get('holdingStocks', '')
|
holding_str = request.args.get('holdingStocks', '')
|
||||||
holding_codes = set(holding_str.split(',')) if holding_str else set()
|
holding_codes = set(holding_str.split(',')) if holding_str else set()
|
||||||
|
|
||||||
conn = psycopg2.connect(
|
conn = get_db()
|
||||||
host=Config.DB_HOST, port=Config.DB_PORT,
|
if not conn:
|
||||||
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
|
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||||
)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
# 获取最近一次扫描数据(过滤退市/ST)
|
# 获取最近一次扫描数据(过滤退市/ST)
|
||||||
@@ -1376,8 +1375,6 @@ def get_bull_stocks():
|
|||||||
for p in cur.fetchall():
|
for p in cur.fetchall():
|
||||||
price_map[p['code']] = {'price': float(p['price']), 'change_pct': float(p.get('change_pct') or 0)}
|
price_map[p['code']] = {'price': float(p['price']), 'change_pct': float(p.get('change_pct') or 0)}
|
||||||
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# ---- 批量计算综合评分 ----
|
# ---- 批量计算综合评分 ----
|
||||||
scores_map = None
|
scores_map = None
|
||||||
try:
|
try:
|
||||||
@@ -1439,6 +1436,8 @@ def get_bull_stocks():
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
def _llm_polish_summary(stock_name, stock_code, ai_summary, score, verdict):
|
def _llm_polish_summary(stock_name, stock_code, ai_summary, score, verdict):
|
||||||
|
|||||||
+11
-10
@@ -6,7 +6,7 @@ import pandas as pd
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from services.stock_service import get_stock_fund_flow, load_cached_data
|
from services.stock_service import get_stock_fund_flow, load_cached_data
|
||||||
from services.stock_algorithms import get_kline_data as algo_get_kline_data
|
from services.stock_algorithms import get_kline_data as algo_get_kline_data
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
|
|
||||||
bp = Blueprint('market', __name__, url_prefix='/api')
|
bp = Blueprint('market', __name__, url_prefix='/api')
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ def db_realtime_price(stock_code):
|
|||||||
'data': dict(row)
|
'data': dict(row)
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/db/realtime_prices', methods=['POST'])
|
@bp.route('/db/realtime_prices', methods=['POST'])
|
||||||
@@ -71,7 +71,7 @@ def db_realtime_prices():
|
|||||||
'data': [dict(row) for row in rows]
|
'data': [dict(row) for row in rows]
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/db/fund_flow_today/<stock_code>', methods=['GET'])
|
@bp.route('/db/fund_flow_today/<stock_code>', methods=['GET'])
|
||||||
@@ -102,7 +102,7 @@ def db_fund_flow_today(stock_code):
|
|||||||
'data': dict(row)
|
'data': dict(row)
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/db/fund_flow_today_batch', methods=['POST'])
|
@bp.route('/db/fund_flow_today_batch', methods=['POST'])
|
||||||
@@ -135,7 +135,7 @@ def db_fund_flow_today_batch():
|
|||||||
'data': [dict(row) for row in rows]
|
'data': [dict(row) for row in rows]
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/db/data_status', methods=['GET'])
|
@bp.route('/db/data_status', methods=['GET'])
|
||||||
@@ -171,7 +171,7 @@ def db_data_status():
|
|||||||
'recent_logs': [dict(log) for log in logs]
|
'recent_logs': [dict(log) for log in logs]
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ============ 原有API(兼容) ============
|
# ============ 原有API(兼容) ============
|
||||||
@@ -305,7 +305,7 @@ def get_fund_flow_rank():
|
|||||||
return jsonify({'success': True, 'data': flow_data, 'total': len(flow_data),
|
return jsonify({'success': True, 'data': flow_data, 'total': len(flow_data),
|
||||||
'source': 'cache'})
|
'source': 'cache'})
|
||||||
finally:
|
finally:
|
||||||
_conn.close()
|
put_db(_conn)
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': [], 'total': 0})
|
return jsonify({'success': True, 'data': [], 'total': 0})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -324,6 +324,7 @@ def get_fundamental(stock_code):
|
|||||||
realtime_data = None
|
realtime_data = None
|
||||||
signal_data = None
|
signal_data = None
|
||||||
|
|
||||||
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if conn:
|
if conn:
|
||||||
@@ -388,10 +389,10 @@ def get_fundamental(stock_code):
|
|||||||
'indicators': ind,
|
'indicators': ind,
|
||||||
'triggered_count': sig_row[2] or 0,
|
'triggered_count': sig_row[2] or 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取数据失败: {e}")
|
print(f"获取数据失败: {e}")
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
# 优先从数据库获取当日缓存
|
# 优先从数据库获取当日缓存
|
||||||
cached = db_get_fundamental(stock_code)
|
cached = db_get_fundamental(stock_code)
|
||||||
@@ -484,7 +485,7 @@ def get_fundamental(stock_code):
|
|||||||
# 备用方案:先试腾讯API,再试akshare
|
# 备用方案:先试腾讯API,再试akshare
|
||||||
try:
|
try:
|
||||||
import requests as _rq
|
import requests as _rq
|
||||||
_tc = ('sh' if stock_code.startswith('6') else 'sz') + stock_code
|
_tc = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code
|
||||||
_rr = _rq.get(f'http://qt.gtimg.cn/q={_tc}', timeout=5,
|
_rr = _rq.get(f'http://qt.gtimg.cn/q={_tc}', timeout=5,
|
||||||
headers={'Referer': 'https://finance.qq.com'})
|
headers={'Referer': 'https://finance.qq.com'})
|
||||||
if _rr.status_code == 200 and '\"' in _rr.text:
|
if _rr.status_code == 200 and '\"' in _rr.text:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from datetime import datetime, date, time
|
from datetime import datetime, date, time
|
||||||
from db import get_db, login_required, get_current_user_id
|
from db import get_db, put_db, login_required, get_current_user_id
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
bp = Blueprint('sim_trade', __name__, url_prefix='/api/sim')
|
bp = Blueprint('sim_trade', __name__, url_prefix='/api/sim')
|
||||||
@@ -29,7 +29,7 @@ def init_user_config(user_id):
|
|||||||
cur.execute("SELECT * FROM sim_config WHERE user_id = %s", (user_id,))
|
cur.execute("SELECT * FROM sim_config WHERE user_id = %s", (user_id,))
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/config', methods=['GET'])
|
@bp.route('/config', methods=['GET'])
|
||||||
@@ -82,7 +82,7 @@ def update_config():
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/trades', methods=['GET'])
|
@bp.route('/trades', methods=['GET'])
|
||||||
@@ -114,7 +114,7 @@ def get_trades():
|
|||||||
trades = cur.fetchall()
|
trades = cur.fetchall()
|
||||||
return jsonify({'success': True, 'trades': trades})
|
return jsonify({'success': True, 'trades': trades})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/positions', methods=['GET'])
|
@bp.route('/positions', methods=['GET'])
|
||||||
@@ -140,7 +140,7 @@ def get_positions():
|
|||||||
positions = cur.fetchall()
|
positions = cur.fetchall()
|
||||||
return jsonify({'success': True, 'positions': positions})
|
return jsonify({'success': True, 'positions': positions})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/stats', methods=['GET'])
|
@bp.route('/stats', methods=['GET'])
|
||||||
@@ -261,7 +261,7 @@ def get_stats():
|
|||||||
'history': list(reversed(history)) if history else []
|
'history': list(reversed(history)) if history else []
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/execute', methods=['POST'])
|
@bp.route('/execute', methods=['POST'])
|
||||||
@@ -381,7 +381,7 @@ def execute_trade():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/auto_execute', methods=['POST'])
|
@bp.route('/auto_execute', methods=['POST'])
|
||||||
@@ -532,7 +532,7 @@ def auto_execute():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/update_prices', methods=['POST'])
|
@bp.route('/update_prices', methods=['POST'])
|
||||||
@@ -588,7 +588,7 @@ def update_prices():
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/reset', methods=['POST'])
|
@bp.route('/reset', methods=['POST'])
|
||||||
@@ -612,7 +612,7 @@ def reset_simulation():
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/trigger_trade', methods=['POST'])
|
@bp.route('/trigger_trade', methods=['POST'])
|
||||||
@@ -623,12 +623,12 @@ def trigger_trade():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 优先使用智能引擎
|
# 优先使用智能引擎
|
||||||
|
smart_conn = None
|
||||||
try:
|
try:
|
||||||
from services.smart_trade_engine import execute_smart_trade
|
from services.smart_trade_engine import execute_smart_trade
|
||||||
conn = get_db()
|
smart_conn = get_db()
|
||||||
if conn:
|
if smart_conn:
|
||||||
result = execute_smart_trade(conn, user_id, scan_date=None)
|
result = execute_smart_trade(smart_conn, user_id, scan_date=None)
|
||||||
conn.close()
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
results = result.get('results', [])
|
results = result.get('results', [])
|
||||||
buy_count = len([r for r in results if r['type'] == 'buy'])
|
buy_count = len([r for r in results if r['type'] == 'buy'])
|
||||||
@@ -641,19 +641,29 @@ def trigger_trade():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[trigger_trade] 智能引擎异常,降级: {e}")
|
print(f"[trigger_trade] 智能引擎异常,降级: {e}")
|
||||||
|
finally:
|
||||||
|
if smart_conn:
|
||||||
|
put_db(smart_conn)
|
||||||
|
|
||||||
# 降级: 使用旧引擎
|
# 降级: 使用旧引擎
|
||||||
from services.scheduler import execute_auto_trade_for_user
|
from services.scheduler import execute_auto_trade_for_user
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if conn:
|
if conn:
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
cur.execute("SELECT trade_quantity FROM sim_config WHERE user_id = %s", (user_id,))
|
cur.execute("SELECT trade_quantity FROM sim_config WHERE user_id = %s", (user_id,))
|
||||||
config = cur.fetchone()
|
config = cur.fetchone()
|
||||||
trade_quantity = config['trade_quantity'] if config else 1000
|
trade_quantity = config['trade_quantity'] if config else 1000
|
||||||
conn.close()
|
|
||||||
else:
|
else:
|
||||||
trade_quantity = 1000
|
trade_quantity = 1000
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[trigger_trade] 获取交易配置失败: {e}")
|
||||||
|
trade_quantity = 1000
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
result = execute_auto_trade_for_user(user_id, trade_quantity)
|
result = execute_auto_trade_for_user(user_id, trade_quantity)
|
||||||
|
|
||||||
@@ -730,4 +740,4 @@ def get_today_trades():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from db import get_db, login_required, get_current_user_id
|
from db import get_db, put_db, login_required, get_current_user_id
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
bp = Blueprint('smart_trade', __name__, url_prefix='/api/smart')
|
bp = Blueprint('smart_trade', __name__, url_prefix='/api/smart')
|
||||||
@@ -43,7 +43,7 @@ def get_algo_templates():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
@@ -78,7 +78,7 @@ def get_algo_config():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/config', methods=['POST'])
|
@bp.route('/config', methods=['POST'])
|
||||||
@@ -111,7 +111,7 @@ def save_algo_config():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/apply_template', methods=['POST'])
|
@bp.route('/apply_template', methods=['POST'])
|
||||||
@@ -139,7 +139,7 @@ def apply_template():
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
@@ -164,7 +164,7 @@ def get_status():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
@@ -205,7 +205,7 @@ def trigger_smart_trade():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
@@ -242,7 +242,7 @@ def get_signals():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
@@ -284,4 +284,4 @@ def get_position_meta():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ def update_trade(trade_id):
|
|||||||
""", (trade_id, user_id))
|
""", (trade_id, user_id))
|
||||||
old_trade = cur.fetchone()
|
old_trade = cur.fetchone()
|
||||||
if not old_trade:
|
if not old_trade:
|
||||||
put_db(conn)
|
|
||||||
return jsonify({'error': '交易记录不存在'}), 404
|
return jsonify({'error': '交易记录不存在'}), 404
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
@@ -152,7 +151,6 @@ def update_trade(trade_id):
|
|||||||
trade = cur.fetchone()
|
trade = cur.fetchone()
|
||||||
if not trade:
|
if not trade:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
put_db(conn)
|
|
||||||
return jsonify({'error': '交易记录不存在'}), 404
|
return jsonify({'error': '交易记录不存在'}), 404
|
||||||
|
|
||||||
old_delta = _calc_cash_delta(old_trade.get('trade_type'), old_trade.get('price'), old_trade.get('quantity'))
|
old_delta = _calc_cash_delta(old_trade.get('trade_type'), old_trade.get('price'), old_trade.get('quantity'))
|
||||||
@@ -194,7 +192,6 @@ def delete_trade(trade_id):
|
|||||||
""", (trade_id, user_id))
|
""", (trade_id, user_id))
|
||||||
old_trade = cur.fetchone()
|
old_trade = cur.fetchone()
|
||||||
if not old_trade:
|
if not old_trade:
|
||||||
put_db(conn)
|
|
||||||
return jsonify({'success': False, 'error': '交易记录不存在'}), 404
|
return jsonify({'success': False, 'error': '交易记录不存在'}), 404
|
||||||
|
|
||||||
cur.execute("DELETE FROM trades WHERE id = %s AND user_id = %s", (trade_id, user_id))
|
cur.execute("DELETE FROM trades WHERE id = %s AND user_id = %s", (trade_id, user_id))
|
||||||
@@ -306,7 +303,7 @@ def check_stoploss():
|
|||||||
current_price = 0
|
current_price = 0
|
||||||
try:
|
try:
|
||||||
import requests as _rq
|
import requests as _rq
|
||||||
_tc = ('sh' if code.startswith('6') else 'sz') + code
|
_tc = ('sh' if code.startswith('6') else 'bj' if code.startswith(('8', '9')) else 'sz') + code
|
||||||
_rr = _rq.get(f'http://qt.gtimg.cn/q={_tc}', timeout=5,
|
_rr = _rq.get(f'http://qt.gtimg.cn/q={_tc}', timeout=5,
|
||||||
headers={'Referer': 'https://finance.qq.com'})
|
headers={'Referer': 'https://finance.qq.com'})
|
||||||
if _rr.status_code == 200 and '\"' in _rr.text:
|
if _rr.status_code == 200 and '\"' in _rr.text:
|
||||||
|
|||||||
@@ -155,6 +155,39 @@ def _get_fund_flow_from_mairui(stock_code, days=10):
|
|||||||
'small_net_inflow_pct': small_net_pct,
|
'small_net_inflow_pct': small_net_pct,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 从本地DB补充 close_price 和 change_pct(避免全为0导致量价背离误判)
|
||||||
|
from db import get_db, put_db
|
||||||
|
conn = get_db()
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
date_list = [r['date'] for r in records if r['date']]
|
||||||
|
if date_list:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT k.trade_date::text, k.close,
|
||||||
|
CASE WHEN prev.close > 0
|
||||||
|
THEN ROUND((k.close - prev.close) / prev.close * 100, 2)
|
||||||
|
ELSE 0 END AS change_pct
|
||||||
|
FROM stock_kline_daily k
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT close FROM stock_kline_daily
|
||||||
|
WHERE code = k.code AND trade_date < k.trade_date
|
||||||
|
ORDER BY trade_date DESC LIMIT 1
|
||||||
|
) prev ON true
|
||||||
|
WHERE k.code = %s AND k.trade_date::text = ANY(%s)
|
||||||
|
""", (stock_code, date_list))
|
||||||
|
price_map = {r[0]: {'close': float(r[1] or 0), 'change_pct': float(r[2] or 0)}
|
||||||
|
for r in cur.fetchall()}
|
||||||
|
for r in records:
|
||||||
|
info = price_map.get(r['date'])
|
||||||
|
if info:
|
||||||
|
r['close_price'] = info['close']
|
||||||
|
r['change_pct'] = info['change_pct']
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"补充K线价格失败({stock_code}): {e}")
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
|
||||||
# 按日期升序排列
|
# 按日期升序排列
|
||||||
records.sort(key=lambda x: x['date'])
|
records.sort(key=lambda x: x['date'])
|
||||||
logger.info(f"麦蕊API获取{stock_code}资金流向{len(records)}条")
|
logger.info(f"麦蕊API获取{stock_code}资金流向{len(records)}条")
|
||||||
@@ -245,6 +278,9 @@ def analyze_fund_flow(stock_code, days=5):
|
|||||||
# 出货:主力净流出但价格不跌(跌幅<2%)
|
# 出货:主力净流出但价格不跌(跌幅<2%)
|
||||||
accumulation = False
|
accumulation = False
|
||||||
distribution = False
|
distribution = False
|
||||||
|
# 检查是否有有效的价格数据(close_price 全为0说明数据不完整,跳过背离检测)
|
||||||
|
has_valid_price = any(r.get('close_price', 0) > 0 for r in recent)
|
||||||
|
if has_valid_price:
|
||||||
if total_main_inflow > 0:
|
if total_main_inflow > 0:
|
||||||
price_changes = [r['change_pct'] for r in recent]
|
price_changes = [r['change_pct'] for r in recent]
|
||||||
avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0
|
avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0
|
||||||
|
|||||||
@@ -245,17 +245,21 @@ def get_fund_flow(stock_code, days=3):
|
|||||||
flow_data = []
|
flow_data = []
|
||||||
for item in data:
|
for item in data:
|
||||||
# 计算主力净流入 = 主买大单+主买特大单 - 主卖大单-主卖特大单
|
# 计算主力净流入 = 主买大单+主买特大单 - 主卖大单-主卖特大单
|
||||||
main_buy = float(item.get('zmbddcje', 0)) + float(item.get('zmbtdcje', 0))
|
main_buy = float(item.get('zmbddcje', 0) or 0) + float(item.get('zmbtdcje', 0) or 0)
|
||||||
main_sell = float(item.get('zmsddcje', 0)) + float(item.get('zmstdcje', 0))
|
main_sell = float(item.get('zmsddcje', 0) or 0) + float(item.get('zmstdcje', 0) or 0)
|
||||||
main_net = main_buy - main_sell
|
main_net = main_buy - main_sell
|
||||||
|
|
||||||
|
# 日期解析:与 fund_flow_analyzer.py 保持一致,用字符串截取
|
||||||
|
t_str = str(item.get('t', ''))
|
||||||
|
date_str = t_str[:10] if t_str else ''
|
||||||
|
|
||||||
flow_data.append({
|
flow_data.append({
|
||||||
'date': datetime.fromtimestamp(item.get('t', 0)).strftime('%Y-%m-%d') if item.get('t') else '',
|
'date': date_str,
|
||||||
'main_net_inflow': main_net,
|
'main_net_inflow': main_net,
|
||||||
'super_buy': float(item.get('zmbtdcje', 0)),
|
'super_buy': float(item.get('zmbtdcje', 0) or 0),
|
||||||
'super_sell': float(item.get('zmstdcje', 0)),
|
'super_sell': float(item.get('zmstdcje', 0) or 0),
|
||||||
'big_buy': float(item.get('zmbddcje', 0)),
|
'big_buy': float(item.get('zmbddcje', 0) or 0),
|
||||||
'big_sell': float(item.get('zmsddcje', 0)),
|
'big_sell': float(item.get('zmsddcje', 0) or 0),
|
||||||
})
|
})
|
||||||
return {'success': True, 'data': flow_data}
|
return {'success': True, 'data': flow_data}
|
||||||
|
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ def analyze_policy_impact(policy_news):
|
|||||||
affected[category] = impact
|
affected[category] = impact
|
||||||
|
|
||||||
# 尝试用LLM深度分析重大政策
|
# 尝试用LLM深度分析重大政策
|
||||||
|
llm_summary = None
|
||||||
major_policies = [n for n in policy_news if n['category'] in ['行业扶持', '行业监管', '货币政策']]
|
major_policies = [n for n in policy_news if n['category'] in ['行业扶持', '行业监管', '货币政策']]
|
||||||
if major_policies and len(major_policies) <= 5:
|
if major_policies and len(major_policies) <= 5:
|
||||||
try:
|
try:
|
||||||
@@ -463,14 +464,17 @@ def analyze_policy_impact(policy_news):
|
|||||||
if llm_result:
|
if llm_result:
|
||||||
score = llm_result.get('score', score)
|
score = llm_result.get('score', score)
|
||||||
reasons = llm_result.get('reasons', reasons)
|
reasons = llm_result.get('reasons', reasons)
|
||||||
|
llm_summary = llm_result.get('summary')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"LLM政策分析失败: {e}")
|
logger.warning(f"LLM政策分析失败: {e}")
|
||||||
|
|
||||||
score = max(-10, min(10, score))
|
score = max(-10, min(10, score))
|
||||||
|
|
||||||
if positive_count > negative_count:
|
if llm_summary:
|
||||||
|
summary = llm_summary
|
||||||
|
elif score > 0:
|
||||||
summary = f'近期{len(policy_news)}条政策消息,偏利好({positive_count}条利好/{negative_count}条利空)'
|
summary = f'近期{len(policy_news)}条政策消息,偏利好({positive_count}条利好/{negative_count}条利空)'
|
||||||
elif negative_count > positive_count:
|
elif score < 0:
|
||||||
summary = f'近期{len(policy_news)}条政策消息,偏利空({negative_count}条利空/{positive_count}条利好)'
|
summary = f'近期{len(policy_news)}条政策消息,偏利空({negative_count}条利空/{positive_count}条利好)'
|
||||||
else:
|
else:
|
||||||
summary = f'近期{len(policy_news)}条政策消息,影响中性'
|
summary = f'近期{len(policy_news)}条政策消息,影响中性'
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ def is_trading_time():
|
|||||||
|
|
||||||
def get_all_users():
|
def get_all_users():
|
||||||
"""获取所有启用自动交易的用户"""
|
"""获取所有启用自动交易的用户"""
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -121,7 +121,7 @@ def get_all_users():
|
|||||||
print(f"[定时任务] 获取用户列表失败: {e}")
|
print(f"[定时任务] 获取用户列表失败: {e}")
|
||||||
return []
|
return []
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
# 自定义股票列表(100只精选股票)
|
# 自定义股票列表(100只精选股票)
|
||||||
@@ -168,7 +168,7 @@ def execute_auto_trade_for_user(user_id, trade_quantity=1000, scan_date=None):
|
|||||||
|
|
||||||
scan_date: 使用哪天的扫描数据, None则自动选择最近可用的
|
scan_date: 使用哪天的扫描数据, None则自动选择最近可用的
|
||||||
"""
|
"""
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
print(f"[定时任务] 开始为用户{user_id}执行策略交易(统一推荐算法)...")
|
print(f"[定时任务] 开始为用户{user_id}执行策略交易(统一推荐算法)...")
|
||||||
@@ -407,7 +407,7 @@ def execute_auto_trade_for_user(user_id, trade_quantity=1000, scan_date=None):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {'error': str(e)}
|
return {'error': str(e)}
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
def _get_latest_price(stock_code):
|
def _get_latest_price(stock_code):
|
||||||
@@ -417,7 +417,7 @@ def _get_latest_price(stock_code):
|
|||||||
|
|
||||||
def update_positions_price_for_user(user_id):
|
def update_positions_price_for_user(user_id):
|
||||||
"""更新用户持仓的当前价格(收盘时调用)— 使用腾讯财经API"""
|
"""更新用户持仓的当前价格(收盘时调用)— 使用腾讯财经API"""
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -443,6 +443,8 @@ def update_positions_price_for_user(user_id):
|
|||||||
for c in codes:
|
for c in codes:
|
||||||
if c.startswith('6'):
|
if c.startswith('6'):
|
||||||
tencent_codes.append(f'sh{c}')
|
tencent_codes.append(f'sh{c}')
|
||||||
|
elif c.startswith('8') or c.startswith('9'):
|
||||||
|
tencent_codes.append(f'bj{c}')
|
||||||
else:
|
else:
|
||||||
tencent_codes.append(f'sz{c}')
|
tencent_codes.append(f'sz{c}')
|
||||||
_r = _req.get(f'http://qt.gtimg.cn/q={",".join(tencent_codes)}',
|
_r = _req.get(f'http://qt.gtimg.cn/q={",".join(tencent_codes)}',
|
||||||
@@ -492,7 +494,7 @@ def update_positions_price_for_user(user_id):
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
print(f"[定时任务] 更新用户{user_id}持仓价格失败: {e}")
|
print(f"[定时任务] 更新用户{user_id}持仓价格失败: {e}")
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
put_db(conn)
|
||||||
|
|
||||||
|
|
||||||
def job_morning_trade():
|
def job_morning_trade():
|
||||||
@@ -512,19 +514,22 @@ def job_morning_trade():
|
|||||||
for user in users:
|
for user in users:
|
||||||
user_id = user['user_id']
|
user_id = user['user_id']
|
||||||
# 尝试使用智能引擎
|
# 尝试使用智能引擎
|
||||||
|
smart_conn = None
|
||||||
try:
|
try:
|
||||||
from services.smart_trade_engine import execute_smart_trade
|
from services.smart_trade_engine import execute_smart_trade
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
conn = get_db()
|
smart_conn = get_db()
|
||||||
if conn:
|
if smart_conn:
|
||||||
result = execute_smart_trade(conn, user_id, scan_date=None)
|
result = execute_smart_trade(smart_conn, user_id, scan_date=None)
|
||||||
conn.close()
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
print(f"[定时任务] 用户{user_id} 智能引擎执行成功 "
|
print(f"[定时任务] 用户{user_id} 智能引擎执行成功 "
|
||||||
f"(算法:{result.get('algo','?')}, 信号:{result.get('signals',0)})")
|
f"(算法:{result.get('algo','?')}, 信号:{result.get('signals',0)})")
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[定时任务] 用户{user_id} 智能引擎异常,降级到旧引擎: {e}")
|
print(f"[定时任务] 用户{user_id} 智能引擎异常,降级到旧引擎: {e}")
|
||||||
|
finally:
|
||||||
|
if smart_conn:
|
||||||
|
put_db(smart_conn)
|
||||||
|
|
||||||
# 降级:使用旧引擎
|
# 降级:使用旧引擎
|
||||||
trade_quantity = user.get('trade_quantity') or 1000
|
trade_quantity = user.get('trade_quantity') or 1000
|
||||||
@@ -551,18 +556,21 @@ def job_afternoon_trade():
|
|||||||
for user in users:
|
for user in users:
|
||||||
user_id = user['user_id']
|
user_id = user['user_id']
|
||||||
# 尝试使用智能引擎
|
# 尝试使用智能引擎
|
||||||
|
smart_conn = None
|
||||||
try:
|
try:
|
||||||
from services.smart_trade_engine import execute_smart_trade
|
from services.smart_trade_engine import execute_smart_trade
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
conn = get_db()
|
smart_conn = get_db()
|
||||||
if conn:
|
if smart_conn:
|
||||||
result = execute_smart_trade(conn, user_id, scan_date=today)
|
result = execute_smart_trade(smart_conn, user_id, scan_date=today)
|
||||||
conn.close()
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
print(f"[定时任务] 用户{user_id} 午后智能引擎执行成功")
|
print(f"[定时任务] 用户{user_id} 午后智能引擎执行成功")
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[定时任务] 用户{user_id} 智能引擎异常,降级: {e}")
|
print(f"[定时任务] 用户{user_id} 智能引擎异常,降级: {e}")
|
||||||
|
finally:
|
||||||
|
if smart_conn:
|
||||||
|
put_db(smart_conn)
|
||||||
trade_quantity = user.get('trade_quantity') or 1000
|
trade_quantity = user.get('trade_quantity') or 1000
|
||||||
execute_auto_trade_for_user(user_id, trade_quantity, scan_date=today)
|
execute_auto_trade_for_user(user_id, trade_quantity, scan_date=today)
|
||||||
|
|
||||||
@@ -645,7 +653,7 @@ def trigger_afternoon_trade():
|
|||||||
|
|
||||||
def trigger_closing_update():
|
def trigger_closing_update():
|
||||||
"""手动触发收盘更新(仅更新持仓价格)"""
|
"""手动触发收盘更新(仅更新持仓价格)"""
|
||||||
from db import get_db
|
from db import get_db, put_db
|
||||||
if not is_trading_day():
|
if not is_trading_day():
|
||||||
print("[定时任务] 今天不是交易日,跳过")
|
print("[定时任务] 今天不是交易日,跳过")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -134,10 +134,16 @@ def compute_comprehensive_score(stock_code, stock_name, technical_score, df=None
|
|||||||
external_score = 0
|
external_score = 0
|
||||||
summaries = []
|
summaries = []
|
||||||
|
|
||||||
# ---- P0: 主力资金进出 ----
|
# ---- P0: 主力资金进出(带30分钟缓存,与批量模式一致)----
|
||||||
try:
|
try:
|
||||||
|
now = time.time()
|
||||||
|
cached_ff = _fund_flow_cache.get(stock_code)
|
||||||
|
if cached_ff and (now - cached_ff[1]) < _FUND_FLOW_TTL:
|
||||||
|
fund_result = cached_ff[0]
|
||||||
|
else:
|
||||||
from services.fund_flow_analyzer import analyze_fund_flow
|
from services.fund_flow_analyzer import analyze_fund_flow
|
||||||
fund_result = analyze_fund_flow(stock_code, days=5)
|
fund_result = analyze_fund_flow(stock_code, days=5)
|
||||||
|
_fund_flow_cache[stock_code] = (fund_result, now)
|
||||||
factors['fund_flow'] = fund_result
|
factors['fund_flow'] = fund_result
|
||||||
external_score += fund_result.get('score', 0)
|
external_score += fund_result.get('score', 0)
|
||||||
all_reasons.extend(fund_result.get('reasons', []))
|
all_reasons.extend(fund_result.get('reasons', []))
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ def detect_all_signals(df, lookback=5):
|
|||||||
if not np.issubdtype(df[col].dtype, np.floating):
|
if not np.issubdtype(df[col].dtype, np.floating):
|
||||||
df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0).astype(np.float64)
|
df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0).astype(np.float64)
|
||||||
|
|
||||||
|
# 若已有指标列则跳过重复计算(deep_analyze 等场景预计算过)
|
||||||
|
if 'dif' not in df.columns or 'ma5' not in df.columns:
|
||||||
df = calc_all_indicators(df)
|
df = calc_all_indicators(df)
|
||||||
|
|
||||||
all_signals = []
|
all_signals = []
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ def generate_sell_decisions(conn, user_id, config, current_prices, scan_map=None
|
|||||||
if current_shares <= 0:
|
if current_shares <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
profit_pct_now = (price - buy_price) / buy_price * 100
|
profit_pct_now = (price - buy_price) / buy_price * 100 if buy_price > 0 else 0
|
||||||
days_held = pos.get('days_held', 0) or 0
|
days_held = pos.get('days_held', 0) or 0
|
||||||
print(f" {code} 成本{buy_price:.2f} 现价{price:.2f} 盈亏{profit_pct_now:+.1f}% 持仓{days_held}天")
|
print(f" {code} 成本{buy_price:.2f} 现价{price:.2f} 盈亏{profit_pct_now:+.1f}% 持仓{days_held}天")
|
||||||
consec_up = pos.get('consecutive_up_days', 0) or 0
|
consec_up = pos.get('consecutive_up_days', 0) or 0
|
||||||
@@ -1003,7 +1003,7 @@ def execute_smart_trade(conn, user_id, scan_date=None):
|
|||||||
log_signal(conn, user_id, today, code, pos['stock_name'],
|
log_signal(conn, user_id, today, code, pos['stock_name'],
|
||||||
'partial_sell', dec['reason'], dec['rule'],
|
'partial_sell', dec['reason'], dec['rule'],
|
||||||
price, pos['avg_cost'],
|
price, pos['avg_cost'],
|
||||||
(price - pos['avg_cost']) / pos['avg_cost'] * 100,
|
(price - pos['avg_cost']) / pos['avg_cost'] * 100 if pos['avg_cost'] else 0,
|
||||||
True, price, shares)
|
True, price, shares)
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
@@ -1052,7 +1052,7 @@ def execute_smart_trade(conn, user_id, scan_date=None):
|
|||||||
log_signal(conn, user_id, today, code, pos['stock_name'],
|
log_signal(conn, user_id, today, code, pos['stock_name'],
|
||||||
'sell', dec['reason'], dec['rule'],
|
'sell', dec['reason'], dec['rule'],
|
||||||
price, pos['avg_cost'],
|
price, pos['avg_cost'],
|
||||||
(price - pos['avg_cost']) / pos['avg_cost'] * 100,
|
(price - pos['avg_cost']) / pos['avg_cost'] * 100 if pos['avg_cost'] else 0,
|
||||||
True, price, qty)
|
True, price, qty)
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
|
|||||||
@@ -165,28 +165,30 @@ def get_kline_data(stock_code, days=120, use_local_db=True):
|
|||||||
|
|
||||||
|
|
||||||
def _get_kline_from_local_db(stock_code, days=120):
|
def _get_kline_from_local_db(stock_code, days=120):
|
||||||
"""从本地数据库读取K线(最快,毫秒级)"""
|
"""从本地数据库读取K线(最快,毫秒级)
|
||||||
|
使用 LIMIT 限制交易日条数,与外部 API 的 limit=days 行为一致。
|
||||||
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
try:
|
try:
|
||||||
from db import get_db, put_db
|
from db import get_db, put_db
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if not conn:
|
if not conn:
|
||||||
return None
|
return None
|
||||||
conn.autocommit = True
|
|
||||||
start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT trade_date, open, high, low, close, volume
|
SELECT trade_date, open, high, low, close, volume
|
||||||
FROM stock_kline_daily
|
FROM stock_kline_daily
|
||||||
WHERE code = %s AND trade_date >= %s
|
WHERE code = %s
|
||||||
ORDER BY trade_date
|
ORDER BY trade_date DESC
|
||||||
""", (stock_code, start_date))
|
LIMIT %s
|
||||||
|
""", (stock_code, min(days, 300)))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
finally:
|
finally:
|
||||||
put_db(conn)
|
put_db(conn)
|
||||||
|
|
||||||
if rows and len(rows) >= 30:
|
if rows and len(rows) >= 30:
|
||||||
|
rows = list(reversed(rows))
|
||||||
df = pd.DataFrame(rows, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
df = pd.DataFrame(rows, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
||||||
df['date'] = df['date'].astype(str)
|
df['date'] = df['date'].astype(str)
|
||||||
for col in ('open', 'high', 'low', 'close', 'volume'):
|
for col in ('open', 'high', 'low', 'close', 'volume'):
|
||||||
@@ -202,7 +204,9 @@ _thread_local = threading.local()
|
|||||||
|
|
||||||
|
|
||||||
def get_kline_from_local_db_threaded(stock_code, days=120):
|
def get_kline_from_local_db_threaded(stock_code, days=120):
|
||||||
"""多线程扫描专用:使用线程本地连接从本地DB读取K线"""
|
"""多线程扫描专用:使用线程本地连接从本地DB读取K线
|
||||||
|
使用 LIMIT 限制交易日条数,与外部 API 的 limit=days 行为一致。
|
||||||
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
try:
|
try:
|
||||||
conn = getattr(_thread_local, 'kline_conn', None)
|
conn = getattr(_thread_local, 'kline_conn', None)
|
||||||
@@ -215,17 +219,18 @@ def get_kline_from_local_db_threaded(stock_code, days=120):
|
|||||||
conn.autocommit = True
|
conn.autocommit = True
|
||||||
_thread_local.kline_conn = conn
|
_thread_local.kline_conn = conn
|
||||||
|
|
||||||
start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT trade_date, open, high, low, close, volume
|
SELECT trade_date, open, high, low, close, volume
|
||||||
FROM stock_kline_daily
|
FROM stock_kline_daily
|
||||||
WHERE code = %s AND trade_date >= %s
|
WHERE code = %s
|
||||||
ORDER BY trade_date
|
ORDER BY trade_date DESC
|
||||||
""", (stock_code, start_date))
|
LIMIT %s
|
||||||
|
""", (stock_code, min(days, 300)))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
if rows and len(rows) >= 30:
|
if rows and len(rows) >= 30:
|
||||||
|
rows = list(reversed(rows))
|
||||||
df = pd.DataFrame(rows, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
df = pd.DataFrame(rows, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
||||||
df['date'] = df['date'].astype(str)
|
df['date'] = df['date'].astype(str)
|
||||||
for col in ('open', 'high', 'low', 'close', 'volume'):
|
for col in ('open', 'high', 'low', 'close', 'volume'):
|
||||||
@@ -962,8 +967,8 @@ def _generate_plain_summary(price, change_pct, ma_trend, position, supports,
|
|||||||
parts.append(trend_desc + '。')
|
parts.append(trend_desc + '。')
|
||||||
|
|
||||||
# 2. 价格位置(用大白话)
|
# 2. 价格位置(用大白话)
|
||||||
pos_20 = position.get('20d', {})
|
pos_20 = position.get('d20', {})
|
||||||
pct_20 = pos_20.get('pct', 50)
|
pct_20 = pos_20.get('range_pct', 50)
|
||||||
if pct_20 > 80:
|
if pct_20 > 80:
|
||||||
parts.append(f'当前股价处于近20天的高位区间({pct_20:.0f}%位置),已经涨了不少,追高要小心。')
|
parts.append(f'当前股价处于近20天的高位区间({pct_20:.0f}%位置),已经涨了不少,追高要小心。')
|
||||||
elif pct_20 > 50:
|
elif pct_20 > 50:
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ from datetime import datetime, timedelta
|
|||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
# ========== 股票名称缓存 ==========
|
# ========== 股票名称缓存 ==========
|
||||||
_stock_name_cache = {}
|
_stock_name_cache = {}
|
||||||
|
_name_cache_dirty = False
|
||||||
|
_name_cache_last_save = 0.0
|
||||||
|
_name_cache_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def _load_stock_name_cache():
|
def _load_stock_name_cache():
|
||||||
@@ -26,32 +31,61 @@ def _load_stock_name_cache():
|
|||||||
print(f"加载股票名称缓存失败: {e}")
|
print(f"加载股票名称缓存失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _save_stock_name_cache():
|
def _save_stock_name_cache(force=False):
|
||||||
"""保存股票名称缓存到本地"""
|
"""保存股票名称缓存到本地(延迟批量保存,60秒去抖)"""
|
||||||
|
global _name_cache_dirty, _name_cache_last_save
|
||||||
|
if not force and not _name_cache_dirty:
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
if not force and (now - _name_cache_last_save) < 60:
|
||||||
|
return
|
||||||
|
with _name_cache_lock:
|
||||||
try:
|
try:
|
||||||
with open(Config.STOCK_NAME_CACHE_FILE, 'w', encoding='utf-8') as f:
|
with open(Config.STOCK_NAME_CACHE_FILE, 'w', encoding='utf-8') as f:
|
||||||
json.dump(_stock_name_cache, f, ensure_ascii=False, indent=2)
|
json.dump(_stock_name_cache, f, ensure_ascii=False, indent=2)
|
||||||
|
_name_cache_dirty = False
|
||||||
|
_name_cache_last_save = now
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"保存股票名称缓存失败: {e}")
|
print(f"保存股票名称缓存失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
def get_stock_name(stock_code):
|
def get_stock_name(stock_code):
|
||||||
"""获取股票名称 — 使用腾讯财经API"""
|
"""获取股票名称 — 优先内存缓存 → DB stock_realtime_price → 腾讯财经API"""
|
||||||
global _stock_name_cache
|
global _stock_name_cache, _name_cache_dirty
|
||||||
|
|
||||||
if stock_code in _stock_name_cache:
|
if stock_code in _stock_name_cache:
|
||||||
return _stock_name_cache[stock_code]
|
return _stock_name_cache[stock_code]
|
||||||
|
|
||||||
# 腾讯财经API获取股票名称
|
# 优先从数据库 stock_realtime_price 表查(毫秒级)
|
||||||
|
try:
|
||||||
|
from db import get_db, put_db
|
||||||
|
conn = get_db()
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT name FROM stock_realtime_price WHERE code = %s", (stock_code,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
_stock_name_cache[stock_code] = row[0]
|
||||||
|
_name_cache_dirty = True
|
||||||
|
_save_stock_name_cache()
|
||||||
|
return row[0]
|
||||||
|
finally:
|
||||||
|
put_db(conn)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 回退到腾讯财经API
|
||||||
try:
|
try:
|
||||||
import requests as _req
|
import requests as _req
|
||||||
tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code
|
tcode = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code
|
||||||
_r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5,
|
_r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5,
|
||||||
headers={'Referer': 'https://finance.qq.com'})
|
headers={'Referer': 'https://finance.qq.com'})
|
||||||
if _r.status_code == 200 and '\"' in _r.text:
|
if _r.status_code == 200 and '\"' in _r.text:
|
||||||
_fields = _r.text.split('\"')[1].split('~')
|
_fields = _r.text.split('\"')[1].split('~')
|
||||||
if len(_fields) > 2 and _fields[1]:
|
if len(_fields) > 2 and _fields[1]:
|
||||||
_stock_name_cache[stock_code] = _fields[1]
|
_stock_name_cache[stock_code] = _fields[1]
|
||||||
|
_name_cache_dirty = True
|
||||||
_save_stock_name_cache()
|
_save_stock_name_cache()
|
||||||
return _fields[1]
|
return _fields[1]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -119,6 +153,8 @@ def get_stock_fund_flow(stock_code, start_date, end_date, force_refresh=False):
|
|||||||
market = 'sh'
|
market = 'sh'
|
||||||
elif stock_code.startswith('0') or stock_code.startswith('3'):
|
elif stock_code.startswith('0') or stock_code.startswith('3'):
|
||||||
market = 'sz'
|
market = 'sz'
|
||||||
|
elif stock_code.startswith('8') or stock_code.startswith('9'):
|
||||||
|
market = 'bj'
|
||||||
else:
|
else:
|
||||||
return None, None, "无法识别股票代码所属市场"
|
return None, None, "无法识别股票代码所属市场"
|
||||||
|
|
||||||
@@ -313,7 +349,7 @@ def get_realtime_price(stock_code):
|
|||||||
# 备用方案2:使用腾讯财经API(腾讯云可用)
|
# 备用方案2:使用腾讯财经API(腾讯云可用)
|
||||||
try:
|
try:
|
||||||
import requests as _req
|
import requests as _req
|
||||||
tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code
|
tcode = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code
|
||||||
_r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5,
|
_r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5,
|
||||||
headers={'Referer': 'https://finance.qq.com'})
|
headers={'Referer': 'https://finance.qq.com'})
|
||||||
if _r.status_code == 200 and '\"' in _r.text:
|
if _r.status_code == 200 and '\"' in _r.text:
|
||||||
|
|||||||
@@ -655,6 +655,32 @@
|
|||||||
.scan-pagination button:disabled { opacity: 0.3; cursor: not-allowed; }
|
.scan-pagination button:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||||
.scan-pagination span { font-size: 13px; color: var(--text-secondary); }
|
.scan-pagination span { font-size: 13px; color: var(--text-secondary); }
|
||||||
|
|
||||||
|
/* 结果列表可收起 header */
|
||||||
|
.full-scan-list-section {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.scan-list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.scan-list-header:hover { background: rgba(255,255,255,0.06); }
|
||||||
|
.scan-list-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.scan-list-title small {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-link-btn {
|
.admin-link-btn {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -86,10 +86,12 @@
|
|||||||
fullScanLoading: false,
|
fullScanLoading: false,
|
||||||
fullScanPage: 1,
|
fullScanPage: 1,
|
||||||
fullScanTotalPages: 1,
|
fullScanTotalPages: 1,
|
||||||
|
fullScanPerPage: 5, // 每页显示5只股票
|
||||||
fullScanFilter: 'all',
|
fullScanFilter: 'all',
|
||||||
fullScanFilterTypes: [],
|
fullScanFilterTypes: [],
|
||||||
fullScanFilterRecommend: '买入', // 扫描结果打开时缺省显示买入列表;all|买入|加仓|卖出|持有|关注|观察|观望
|
fullScanFilterRecommend: '买入', // 扫描结果打开时缺省显示买入列表;all|买入|加仓|卖出|持有|关注|观察|观望
|
||||||
scanSummaryCollapsed: false,
|
scanSummaryCollapsed: false,
|
||||||
|
scanResultsCollapsed: false, // 结果列表可收起
|
||||||
fullScanSignalDist: [],
|
fullScanSignalDist: [],
|
||||||
|
|
||||||
// 找牛股(保留兼容)
|
// 找牛股(保留兼容)
|
||||||
@@ -2253,7 +2255,7 @@
|
|||||||
const resp = await axios.get('/api/scan_results', {
|
const resp = await axios.get('/api/scan_results', {
|
||||||
params: {
|
params: {
|
||||||
page: this.fullScanPage,
|
page: this.fullScanPage,
|
||||||
per_page: 50,
|
per_page: this.fullScanPerPage,
|
||||||
min_triggered: minTriggered,
|
min_triggered: minTriggered,
|
||||||
signal_type: signalTypes,
|
signal_type: signalTypes,
|
||||||
holding_codes: (this.holdingStocks || []).join(','),
|
holding_codes: (this.holdingStocks || []).join(','),
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ def get_all_stock_codes(conn):
|
|||||||
SELECT code, name FROM stock_realtime_price
|
SELECT code, name FROM stock_realtime_price
|
||||||
WHERE volume > 0 AND price > 0
|
WHERE volume > 0 AND price > 0
|
||||||
AND name NOT LIKE '%%退%%'
|
AND name NOT LIKE '%%退%%'
|
||||||
|
AND name NOT LIKE '%%ST%%'
|
||||||
AND name NOT LIKE 'PT%%'
|
AND name NOT LIKE 'PT%%'
|
||||||
ORDER BY code
|
ORDER BY code
|
||||||
""")
|
""")
|
||||||
|
|||||||
@@ -243,8 +243,9 @@ def fetch_5min_kline_sina(code):
|
|||||||
l = float(row.get('low', 0))
|
l = float(row.get('low', 0))
|
||||||
c = float(row.get('close', 0))
|
c = float(row.get('close', 0))
|
||||||
v = int(float(row.get('volume', 0)))
|
v = int(float(row.get('volume', 0)))
|
||||||
|
amt = float(row.get('amount', 0))
|
||||||
|
|
||||||
rows.append((code, dt, o, h, l, c, v, 0.0, 0.0))
|
rows.append((code, dt, o, h, l, c, v, amt, 0.0))
|
||||||
return rows if rows else None
|
return rows if rows else None
|
||||||
|
|
||||||
except (IndexError, KeyError, ValueError):
|
except (IndexError, KeyError, ValueError):
|
||||||
@@ -366,7 +367,8 @@ def fetch_5min_kline_tencent(code):
|
|||||||
dt = datetime(today.year, today.month, today.day, h, m, 0)
|
dt = datetime(today.year, today.month, today.day, h, m, 0)
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
continue
|
continue
|
||||||
min_data.append((dt, price, vol))
|
amt = float(parts[3]) if len(parts) > 3 else 0.0
|
||||||
|
min_data.append((dt, price, vol, amt))
|
||||||
|
|
||||||
if not min_data:
|
if not min_data:
|
||||||
return None
|
return None
|
||||||
@@ -375,26 +377,30 @@ def fetch_5min_kline_tencent(code):
|
|||||||
# 5分钟窗口: 09:30-09:35, 09:35-09:40, ...
|
# 5分钟窗口: 09:30-09:35, 09:35-09:40, ...
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
bars = defaultdict(list)
|
bars = defaultdict(list)
|
||||||
for dt, price, vol in min_data:
|
for dt, price, vol, amt in min_data:
|
||||||
# 5分钟窗口起始时间
|
# 5分钟窗口起始时间
|
||||||
minute = dt.minute
|
minute = dt.minute
|
||||||
bar_min = (minute // 5) * 5
|
bar_min = (minute // 5) * 5
|
||||||
bar_dt = dt.replace(minute=bar_min, second=0)
|
bar_dt = dt.replace(minute=bar_min, second=0)
|
||||||
bars[bar_dt].append((price, vol))
|
bars[bar_dt].append((price, vol, amt))
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
prev_vol = 0
|
prev_vol = 0
|
||||||
|
prev_amt = 0
|
||||||
for bar_dt in sorted(bars.keys()):
|
for bar_dt in sorted(bars.keys()):
|
||||||
ticks = bars[bar_dt]
|
ticks = bars[bar_dt]
|
||||||
o = ticks[0][0] # 第一个价格
|
o = ticks[0][0] # 第一个价格
|
||||||
c = ticks[-1][0] # 最后一个价格
|
c = ticks[-1][0] # 最后一个价格
|
||||||
h = max(p for p, _ in ticks)
|
h = max(p for p, _, _ in ticks)
|
||||||
l = min(p for p, _ in ticks)
|
l = min(p for p, _, _ in ticks)
|
||||||
# 腾讯的volume是累积值,取窗口最后的 - 窗口最前的之前
|
# 腾讯的volume和amount是累积值,取窗口最后的 - 前一窗口最后的
|
||||||
last_vol = ticks[-1][1]
|
last_vol = ticks[-1][1]
|
||||||
bar_vol = last_vol - prev_vol if prev_vol > 0 else ticks[-1][1]
|
bar_vol = last_vol - prev_vol if prev_vol > 0 else ticks[-1][1]
|
||||||
prev_vol = last_vol
|
prev_vol = last_vol
|
||||||
rows.append((code, bar_dt, o, h, l, c, max(0, bar_vol), 0.0, 0.0))
|
last_amt = ticks[-1][2]
|
||||||
|
bar_amt = last_amt - prev_amt if prev_amt > 0 else ticks[-1][2]
|
||||||
|
prev_amt = last_amt
|
||||||
|
rows.append((code, bar_dt, o, h, l, c, max(0, bar_vol), max(0, bar_amt), 0.0))
|
||||||
|
|
||||||
return rows if rows else None
|
return rows if rows else None
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/components.css?v=20260718v1">
|
<link rel="stylesheet" href="/static/css/components.css?v=20260718v1">
|
||||||
<link rel="stylesheet" href="/static/css/auth.css?v=20260718v1">
|
<link rel="stylesheet" href="/static/css/auth.css?v=20260718v1">
|
||||||
<link rel="stylesheet" href="/static/css/pages.css?v=20260719v2">
|
<link rel="stylesheet" href="/static/css/pages.css?v=20260719v2">
|
||||||
<link rel="stylesheet" href="/static/css/scan.css?v=20260719v5">
|
<link rel="stylesheet" href="/static/css/scan.css?v=20260722v1">
|
||||||
<link rel="stylesheet" href="/static/css/responsive.css?v=20260719v1">
|
<link rel="stylesheet" href="/static/css/responsive.css?v=20260719v1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -883,7 +883,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="full-scan-list">
|
<div class="full-scan-list-section">
|
||||||
|
<div class="scan-list-header" role="button" tabindex="0" :aria-expanded="!scanResultsCollapsed" @click="scanResultsCollapsed = !scanResultsCollapsed" @keyup.enter="scanResultsCollapsed = !scanResultsCollapsed">
|
||||||
|
<span class="scan-list-title">推荐列表 <small v-if="fullScanTotalPages > 1">{{ fullScanPage }}/{{ fullScanTotalPages }}页</small></span>
|
||||||
|
<span class="scan-summary-toggle">{{ scanResultsCollapsed ? '▶' : '▼' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="!scanResultsCollapsed" class="full-scan-list">
|
||||||
<div v-if="fullScanResultsFiltered.length === 0" class="scan-empty-hint">暂无符合条件的股票</div>
|
<div v-if="fullScanResultsFiltered.length === 0" class="scan-empty-hint">暂无符合条件的股票</div>
|
||||||
<div v-for="item in fullScanResultsFiltered" :key="item.code" class="scan-result-card"
|
<div v-for="item in fullScanResultsFiltered" :key="item.code" class="scan-result-card"
|
||||||
:class="{ 'has-signal': item.triggered_count > 0, 'is-watched': searchHistory.some(h => h.code === item.code) }">
|
:class="{ 'has-signal': item.triggered_count > 0, 'is-watched': searchHistory.some(h => h.code === item.code) }">
|
||||||
@@ -915,11 +920,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scan-holding-note" v-if="item.holding_note">{{ item.holding_note }}</div>
|
<div class="scan-holding-note" v-if="item.holding_note">{{ item.holding_note }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div v-if="fullScanTotalPages > 1" class="scan-pagination">
|
<div v-if="fullScanTotalPages > 1" class="scan-pagination">
|
||||||
<button type="button" @click="fetchFullScanResults(fullScanPage - 1)" :disabled="fullScanPage <= 1">上一页</button>
|
<button type="button" @click.stop="fetchFullScanResults(fullScanPage - 1)" :disabled="fullScanPage <= 1">上一页</button>
|
||||||
<span>{{ fullScanPage }} / {{ fullScanTotalPages }}</span>
|
<span>{{ fullScanPage }} / {{ fullScanTotalPages }}</span>
|
||||||
<button type="button" @click="fetchFullScanResults(fullScanPage + 1)" :disabled="fullScanPage >= fullScanTotalPages">下一页</button>
|
<button type="button" @click.stop="fetchFullScanResults(fullScanPage + 1)" :disabled="fullScanPage >= fullScanTotalPages">下一页</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -2299,6 +2304,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
<script src="/static/js/app.js?v=20260718v2"></script>
|
<script src="/static/js/app.js?v=20260722v1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user