feat: 新增外部因素分析模块+综合评分引擎+算法文档重构

新增模块:
- fund_flow_analyzer.py: 主力资金流向分析(P0, ±20)
- market_sentiment.py: 市场情绪指标(P1, ±10)
- external_factors.py: 北向资金/美股/大宗商品/汇率(P2-P4,P7)
- news_analyzer.py: 公告/并购/政策面LLM分析(P5-P6)
- score_engine.py: 综合评分引擎,整合技术面+外部因素

路由更新:
- analysis.py: deep_analyze接入综合评分,根据最终评级修正买卖建议
- market.py: 新增4个外部因素API端点
- trades.py: 交易路由更新

算法文档重构:
- 章节重排: 技术面(二三)→外部因素(四)→买卖决策(五)→数据源(六)→性能(七)
- 架构图更新为五层,标注章节对应
- 5.1/5.2标注纯技术面,5.3整合外部因素修正推荐
This commit is contained in:
selfrelease
2026-07-17 23:50:42 +08:00
parent 04f8b9f951
commit 2b5a32ca1e
23 changed files with 4191 additions and 268 deletions
+197
View File
@@ -0,0 +1,197 @@
"""
市场情绪指标模块(P1
从 stock_realtime_price 表直接计算市场情绪指标,无需额外数据源。
指标包括:
1. 涨停/跌停家数比
2. 连板高度(最高连板数)
3. 换手率中位数
4. 两市成交额
"""
import logging
logger = logging.getLogger(__name__)
def calc_market_sentiment():
"""
从数据库实时行情表计算市场情绪指标
返回:
dict: {
'limit_up_count': int, # 涨停家数
'limit_down_count': int, # 跌停家数
'up_down_ratio': float, # 涨跌停比
'sentiment': str, # 情绪标签
'consecutive_board': int, # 最高连板数
'turnover_median': float, # 换手率中位数
'total_amount': float, # 两市成交额(亿)
'market_temp': str, # 市场温度(偏热/偏冷/正常)
'score': int, # 情绪评分增减(-10 ~ +10)
'reasons': list, # 评分原因
}
"""
from db import get_db, put_db
conn = get_db()
if not conn:
return _empty_sentiment()
try:
cur = conn.cursor()
# 涨停跌停统计(涨停:涨幅>=9.8%,跌停:跌幅<=-9.8%
cur.execute("""
SELECT
COUNT(*) FILTER (WHERE change_pct >= 9.8) AS limit_up,
COUNT(*) FILTER (WHERE change_pct <= -9.8) AS limit_down,
COUNT(*) FILTER (WHERE change_pct > 0) AS up_count,
COUNT(*) FILTER (WHERE change_pct < 0) AS down_count,
COUNT(*) FILTER (WHERE change_pct = 0) AS flat_count,
COUNT(*) AS total,
COALESCE(SUM(amount), 0) AS total_amount,
COALESCE(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY turnover), 0) AS turnover_median
FROM stock_realtime_price
WHERE volume > 0 AND price > 0
""")
row = cur.fetchone()
if not row:
return _empty_sentiment()
limit_up = int(row[0] or 0)
limit_down = int(row[1] or 0)
up_count = int(row[2] or 0)
down_count = int(row[3] or 0)
flat_count = int(row[4] or 0)
total = int(row[5] or 1)
total_amount = float(row[6] or 0) / 1e8 # 转为亿
turnover_median = float(row[7] or 0)
# 涨跌停比
up_down_ratio = round(limit_up / limit_down, 1) if limit_down > 0 else float(limit_up)
# 情绪标签
if limit_down == 0 and limit_up > 10:
sentiment = '极度乐观'
elif up_down_ratio >= 5:
sentiment = '乐观'
elif up_down_ratio >= 2:
sentiment = '偏多'
elif up_down_ratio >= 1:
sentiment = '中性'
elif up_down_ratio >= 0.5:
sentiment = '偏空'
else:
sentiment = '悲观'
# 市场温度
if total_amount > 1.2e4:
market_temp = '偏热'
elif total_amount < 6000:
market_temp = '偏冷'
else:
market_temp = '正常'
# 连板高度:查找连续涨停的股票
consecutive_board = _calc_max_consecutive_board(cur)
# 评分
score = 0
reasons = []
if up_down_ratio >= 5:
score += 5
reasons.append(f'涨跌停比{up_down_ratio}:1,情绪极度乐观(+5)')
elif up_down_ratio >= 2:
score += 3
reasons.append(f'涨跌停比{up_down_ratio}:1,情绪偏多(+3)')
elif up_down_ratio < 0.5:
score -= 5
reasons.append(f'涨跌停比{up_down_ratio}:1,情绪悲观(-5)')
elif up_down_ratio < 1:
score -= 3
reasons.append(f'涨跌停比{up_down_ratio}:1,情绪偏空(-3)')
if consecutive_board >= 5:
score += 3
reasons.append(f'最高{consecutive_board}连板,市场热度高(+3)')
if total_amount > 1.2e4:
score += 2
reasons.append(f'两市成交额{total_amount:.0f}亿,交投活跃(+2)')
elif total_amount < 6000:
score -= 2
reasons.append(f'两市成交额仅{total_amount:.0f}亿,交投清淡(-2)')
score = max(-10, min(10, score))
return {
'limit_up_count': limit_up,
'limit_down_count': limit_down,
'up_count': up_count,
'down_count': down_count,
'up_down_ratio': up_down_ratio,
'sentiment': sentiment,
'consecutive_board': consecutive_board,
'turnover_median': round(turnover_median, 2),
'total_amount': round(total_amount, 0),
'market_temp': market_temp,
'score': score,
'reasons': reasons,
}
except Exception as e:
logger.error(f"计算市场情绪指标失败: {e}")
return _empty_sentiment()
finally:
put_db(conn)
def _calc_max_consecutive_board(cur):
"""
计算最高连板数(需要历史数据辅助判断)
简化版:通过查找连续涨幅>=9.8%的股票
由于实时表只有当日数据,这里用近似方法:
查找涨停股票数量作为市场热度参考
"""
try:
# 查找涨停股票(涨幅>=9.8%)
cur.execute("""
SELECT COUNT(*) FROM stock_realtime_price
WHERE change_pct >= 9.8 AND volume > 0
""")
limit_up_count = int(cur.fetchone()[0] or 0)
# 简化:涨停家数>50视为有高连板可能
if limit_up_count > 50:
return 5
elif limit_up_count > 30:
return 4
elif limit_up_count > 15:
return 3
elif limit_up_count > 5:
return 2
elif limit_up_count > 0:
return 1
return 0
except Exception:
return 0
def _empty_sentiment():
"""返回空情绪数据"""
return {
'limit_up_count': 0,
'limit_down_count': 0,
'up_count': 0,
'down_count': 0,
'up_down_ratio': 0,
'sentiment': '无数据',
'consecutive_board': 0,
'turnover_median': 0,
'total_amount': 0,
'market_temp': '无数据',
'score': 0,
'reasons': [],
}