""" 主力资金流向分析模块(P0) 功能: 1. 从数据库读取近N日资金流向数据 2. 计算主力连续净流入/流出天数、累计净流入额 3. 检测量价背离(资金流入+价格不涨 → 吸筹;资金流出+价格不跌 → 出货) 4. 返回资金面评分和信号列表 数据来源:stock_fund_flow_history 表(由 sync_fund_flow.py 每日同步) """ import logging from datetime import datetime, timedelta logger = logging.getLogger(__name__) def get_fund_flow_history(stock_code, days=10): """ 从数据库读取近N日资金流向历史数据 参数: stock_code: 股票代码 days: 获取天数 返回: list[dict]: 每日资金流向记录,按日期升序排列 """ from db import get_db, put_db conn = get_db() if not conn: return [] try: cur = conn.cursor() start_date = (datetime.now() - timedelta(days=days + 5)).strftime('%Y-%m-%d') cur.execute(""" SELECT trade_date, close_price, change_pct, main_net_inflow, main_net_inflow_pct, super_net_inflow, super_net_inflow_pct, big_net_inflow, big_net_inflow_pct, mid_net_inflow, mid_net_inflow_pct, small_net_inflow, small_net_inflow_pct FROM stock_fund_flow_history WHERE code = %s AND trade_date >= %s ORDER BY trade_date ASC """, (stock_code, start_date)) rows = cur.fetchall() records = [] for row in rows: records.append({ 'date': row[0].strftime('%Y-%m-%d') if row[0] else '', 'close_price': float(row[1] or 0), 'change_pct': float(row[2] or 0), 'main_net_inflow': float(row[3] or 0), 'main_net_inflow_pct': float(row[4] or 0), 'super_net_inflow': float(row[5] or 0), 'super_net_inflow_pct': float(row[6] or 0), 'big_net_inflow': float(row[7] or 0), 'big_net_inflow_pct': float(row[8] or 0), 'mid_net_inflow': float(row[9] or 0), 'mid_net_inflow_pct': float(row[10] or 0), 'small_net_inflow': float(row[11] or 0), 'small_net_inflow_pct': float(row[12] or 0), }) return records except Exception as e: logger.error(f"获取资金流向历史失败({stock_code}): {e}") return [] finally: put_db(conn) def _get_fund_flow_from_mairui(stock_code, days=10): """从麦蕊智数API获取资金流向数据,转换为与DB记录相同的格式。 API: https://api.mairuiapi.com/hsstock/history/transaction/{code}/{licence}?lt={n} 字段: zmbtdcje=主买特大单, zmbddcje=主买大单, zmbzdcje=主买中单, zmbxdcje=主买小单 zmstdcje=主卖特大单, zmsddcje=主卖大单, zmszdcje=主卖中单, zmsxdcje=主卖小单 """ try: import requests from config import Config LICENCE = Config.MAIRUI_LICENCE or "5352ED2F-94E5-4E96-8B7F-B57BA75284E3" url = f"https://api.mairuiapi.com/hsstock/history/transaction/{stock_code}/{LICENCE}?lt={days}" resp = requests.get(url, timeout=10) if resp.status_code != 200: logger.warning(f"麦蕊资金流向API返回{resp.status_code}") return [] data = resp.json() if not data or not isinstance(data, list): return [] records = [] for item in data: # 主买总额 = 特大单+大单+中单+小单 buy_total = ( float(item.get('zmbtdcje', 0) or 0) + float(item.get('zmbddcje', 0) or 0) + float(item.get('zmbzdcje', 0) or 0) + float(item.get('zmbxdcje', 0) or 0) ) # 主卖总额 sell_total = ( float(item.get('zmstdcje', 0) or 0) + float(item.get('zmsddcje', 0) or 0) + float(item.get('zmszdcje', 0) or 0) + float(item.get('zmsxdcje', 0) or 0) ) # 主力净流入 = (特大单+大单)买 - (特大单+大单)卖 main_buy = float(item.get('zmbtdcje', 0) or 0) + float(item.get('zmbddcje', 0) or 0) main_sell = float(item.get('zmstdcje', 0) or 0) + float(item.get('zmsddcje', 0) or 0) main_net = main_buy - main_sell # 超大单净流入 super_net = float(item.get('zmbtdcje', 0) or 0) - float(item.get('zmstdcje', 0) or 0) # 总成交额 total_amount = buy_total + sell_total main_net_pct = round(main_net / total_amount * 100, 2) if total_amount > 0 else 0 super_net_pct = round(super_net / total_amount * 100, 2) if total_amount > 0 else 0 # 大单净流入 big_net = float(item.get('zmbddcje', 0) or 0) - float(item.get('zmsddcje', 0) or 0) big_net_pct = round(big_net / total_amount * 100, 2) if total_amount > 0 else 0 # 中单净流入 mid_net = float(item.get('zmbzdcje', 0) or 0) - float(item.get('zmszdcje', 0) or 0) mid_net_pct = round(mid_net / total_amount * 100, 2) if total_amount > 0 else 0 # 小单净流入 small_net = float(item.get('zmbxdcje', 0) or 0) - float(item.get('zmsxdcje', 0) or 0) small_net_pct = round(small_net / total_amount * 100, 2) if total_amount > 0 else 0 # 日期解析 t_str = str(item.get('t', '')) date_str = t_str[:10] if t_str else '' records.append({ 'date': date_str, 'close_price': 0, 'change_pct': 0, 'main_net_inflow': round(main_net, 2), 'main_net_inflow_pct': main_net_pct, 'super_net_inflow': round(super_net, 2), 'super_net_inflow_pct': super_net_pct, 'big_net_inflow': round(big_net, 2), 'big_net_inflow_pct': big_net_pct, 'mid_net_inflow': round(mid_net, 2), 'mid_net_inflow_pct': mid_net_pct, 'small_net_inflow': round(small_net, 2), 'small_net_inflow_pct': small_net_pct, }) # 按日期升序排列 records.sort(key=lambda x: x['date']) logger.info(f"麦蕊API获取{stock_code}资金流向{len(records)}条") return records except Exception as e: logger.warning(f"麦蕊资金流向API失败({stock_code}): {e}") return [] def analyze_fund_flow(stock_code, days=5): """ 分析主力资金流向,返回资金面评分和信号 数据源优先级: 1. DB stock_fund_flow_history 表(有最新数据时) 2. 麦蕊智数API hsstock/history/transaction(DB数据过期时补充) 参数: stock_code: 股票代码 days: 分析最近几天的资金流向 返回: dict: { 'score': int, # 资金面评分增减(-20 ~ +20) 'signals': list, # 资金信号列表 'summary': str, # 白话总结 'details': dict, # 详细数据 'reasons': list, # 评分原因列表 } """ records = get_fund_flow_history(stock_code, days=days + 5) # 检查DB数据是否足够新(最近3天内有数据) use_mairui = False if len(records) < 2: use_mairui = True else: from datetime import date latest_date = records[-1].get('date', '') if latest_date: try: latest = datetime.strptime(latest_date, '%Y-%m-%d').date() if (date.today() - latest).days > 5: use_mairui = True except ValueError: use_mairui = True if use_mairui: # 用麦蕊API获取资金流向数据 mairui_records = _get_fund_flow_from_mairui(stock_code, days + 5) if mairui_records: records = mairui_records if len(records) < 2: return { 'score': 0, 'signals': [], 'summary': '暂无资金流向数据', 'details': {}, 'reasons': [], } recent = records[-days:] if len(records) >= days else records # 计算连续净流入/流出天数 consecutive_inflow = 0 consecutive_outflow = 0 for r in reversed(recent): if r['main_net_inflow'] > 0: if consecutive_outflow > 0: break consecutive_inflow += 1 elif r['main_net_inflow'] < 0: if consecutive_inflow > 0: break consecutive_outflow += 1 # 累计净流入 total_main_inflow = sum(r['main_net_inflow'] for r in recent) avg_main_pct = sum(r['main_net_inflow_pct'] for r in recent) / len(recent) if recent else 0 # 超大单累计 total_super_inflow = sum(r['super_net_inflow'] for r in recent) avg_super_pct = sum(r['super_net_inflow_pct'] for r in recent) / len(recent) if recent else 0 # 量价背离检测 # 吸筹:主力净流入但价格不涨(涨幅<2%) # 出货:主力净流出但价格不跌(跌幅<2%) accumulation = False distribution = False if total_main_inflow > 0: price_changes = [r['change_pct'] for r in recent] avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0 if avg_price_change < 2: accumulation = True if total_main_inflow < 0: price_changes = [r['change_pct'] for r in recent] avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0 if avg_price_change > -2: distribution = True # 单日超大单突击 big_surge = False big_surge_day = None for r in recent: if r['super_net_inflow_pct'] > 15: big_surge = True big_surge_day = r['date'] break # 评分计算 score = 0 reasons = [] signals = [] if consecutive_inflow >= 3: score += 10 reasons.append(f'主力连续{consecutive_inflow}日净流入(+10)') signals.append({ 'type': 'fund_continuous_inflow', 'name': '主力持续流入', 'direction': 'buy', 'strength': 80, 'description': f'主力资金连续{consecutive_inflow}日净流入,累计{total_main_inflow/10000:.0f}万元', }) if consecutive_outflow >= 3: score -= 10 reasons.append(f'主力连续{consecutive_outflow}日净流出(-10)') signals.append({ 'type': 'fund_continuous_outflow', 'name': '主力持续流出', 'direction': 'sell', 'strength': 75, 'description': f'主力资金连续{consecutive_outflow}日净流出,累计{total_main_inflow/10000:.0f}万元', }) if accumulation: score += 8 reasons.append('主力暗中吸筹(+8)') signals.append({ 'type': 'fund_accumulation', 'name': '主力吸筹', 'direction': 'buy', 'strength': 85, 'description': f'主力净流入但价格未涨,暗中吸筹,可能即将拉升', }) if distribution: score -= 8 reasons.append('主力暗中出货(-8)') signals.append({ 'type': 'fund_distribution', 'name': '主力出货', 'direction': 'sell', 'strength': 80, 'description': f'主力净流出但价格未跌,暗中出货,需警惕', }) if big_surge: score += 5 reasons.append(f'超大单突击流入({big_surge_day})(+5)') signals.append({ 'type': 'fund_big_surge', 'name': '大单突击', 'direction': 'buy', 'strength': 70, 'description': f'{big_surge_day}超大单净流入占比>15%,大机构突击入场', }) # 主力净流入占比评分 if avg_main_pct > 10: score += 5 reasons.append(f'主力净流入占比{avg_main_pct:.1f}%(+5)') elif avg_main_pct < -10: score -= 5 reasons.append(f'主力净流出占比{abs(avg_main_pct):.1f}%(-5)') score = max(-20, min(20, score)) # 白话总结 summary_parts = [] if consecutive_inflow >= 3: summary_parts.append(f'近{consecutive_inflow}天主力持续买入,累计流入{total_main_inflow/10000:.0f}万元') elif consecutive_outflow >= 3: summary_parts.append(f'近{consecutive_outflow}天主力持续卖出,累计流出{abs(total_main_inflow)/10000:.0f}万元') elif total_main_inflow > 0: summary_parts.append(f'近期主力总体净流入{total_main_inflow/10000:.0f}万元') elif total_main_inflow < 0: summary_parts.append(f'近期主力总体净流出{abs(total_main_inflow)/10000:.0f}万元') if accumulation: summary_parts.append('但价格没怎么涨,像是在暗中吸筹') if distribution: summary_parts.append('但价格没怎么跌,像是在暗中出货,要小心') summary = ','.join(summary_parts) if summary_parts else '资金面无明显方向' return { 'score': score, 'signals': signals, 'summary': summary, 'details': { 'consecutive_inflow': consecutive_inflow, 'consecutive_outflow': consecutive_outflow, 'total_main_inflow': round(total_main_inflow, 2), 'avg_main_pct': round(avg_main_pct, 2), 'total_super_inflow': round(total_super_inflow, 2), 'avg_super_pct': round(avg_super_pct, 2), 'accumulation': accumulation, 'distribution': distribution, 'recent_days': len(recent), 'daily_data': recent, }, 'reasons': reasons, }