Files
stock/stock-html/services/market_sentiment.py
T
selfrelease 79e869eeda fix: 修复外部因素数据源 - 北向资金改为南向资金,修复P0-P7全部外部因素
- P0资金面:DB数据过期时用麦蕊API获取资金流向
- P1市场情绪:去掉turnover字段依赖(DB无此字段)
- P2南向资金:北向实时数据已停公布,改用南向资金(港股通)替代
- P3美股:用腾讯财经API替代失效的AKShare接口
- P4大宗商品:用腾讯财经API替代,修复var_name解析
- P7汇率:用新浪财经API替代失效的AKShare接口
- 修复评分详情API技术得分硬编码问题
- 前端传入tech_score参数确保明细与列表分数一致
2026-07-18 16:45:58 +08:00

197 lines
6.1 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
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 = 0 # DB无turnover字段,不再使用
# 涨跌停比
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': [],
}