2b5a32ca1e
新增模块: - 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整合外部因素修正推荐
198 lines
6.2 KiB
Python
198 lines
6.2 KiB
Python
"""
|
||
市场情绪指标模块(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': [],
|
||
}
|