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:
@@ -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': [],
|
||||
}
|
||||
Reference in New Issue
Block a user