Files
stock/stock-html/services/market_sentiment.py
T
selfrelease 2b5a32ca1e 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整合外部因素修正推荐
2026-07-17 23:50:42 +08:00

198 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
市场情绪指标模块(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': [],
}