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,257 @@
|
||||
"""
|
||||
主力资金流向分析模块(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,
|
||||
}
|
||||
Reference in New Issue
Block a user