""" 市场情绪指标模块(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': [], }