Files
stock/stock-html/services/fund_flow_analyzer.py
T
selfrelease 2b5a32ca1e 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整合外部因素修正推荐
2026-07-17 23:50:42 +08:00

258 lines
8.9 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.
"""
主力资金流向分析模块(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 analyze_fund_flow(stock_code, days=5):
"""
分析主力资金流向,返回资金面评分和信号
参数:
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)
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,
}