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:
selfrelease
2026-07-17 23:50:42 +08:00
parent 04f8b9f951
commit 2b5a32ca1e
23 changed files with 4191 additions and 268 deletions
+208
View File
@@ -11,6 +11,7 @@ from services.stock_service import (
from services.stock_algorithms import (
compute_recommend, get_kline_data as algo_get_kline_data,
compute_bull_stage, find_bull_stocks, BULL_STAGES,
compute_deep_analysis,
)
from db import (
login_required, get_current_user_id,
@@ -88,6 +89,143 @@ def analyze():
return jsonify({'error': str(e)}), 500
@bp.route('/deep_analyze', methods=['POST'])
def deep_analyze():
"""单股深度分析(价格位置、压力支撑、量价、空间、综合评分)"""
try:
data = request.get_json()
stock_code = data.get('stock_code', '').strip()
if not stock_code:
return jsonify({'error': '股票代码不能为空'}), 400
df = algo_get_kline_data(stock_code, days=180)
if df is None or len(df) < 30:
return jsonify({'error': 'K线数据不足'}), 400
from services.technical_indicators import calc_all_indicators
from services.signal_detector import detect_all_signals
df = calc_all_indicators(df)
signal_result = detect_all_signals(df, lookback=5)
from db import get_db, put_db
from psycopg2.extras import RealDictCursor
realtime_info = None
conn = get_db()
if conn:
try:
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT code, name, price, change_pct, volume, amount,
high, low, open, prev_close, pe, pb, total_market_cap
FROM stock_realtime_price WHERE code = %s
""", (stock_code,))
realtime_info = cur.fetchone()
finally:
put_db(conn)
report = compute_deep_analysis(df, signal_result, realtime_info)
stock_name = get_stock_name(stock_code) or (realtime_info or {}).get('name', '')
sig_status = signal_result.get('signal_status', [])
indicators = signal_result.get('indicators', {})
sig_count = signal_result.get('signal_summary', {}).get('total_signals', 0)
rec = compute_recommend(sig_status, indicators, sig_count, False)
report['stock_code'] = stock_code
report['stock_name'] = stock_name
report['recommend'] = {
'signal_type': rec[0],
'display': rec[1],
'reason': rec[2],
'rate': rec[3],
}
report['signals'] = signal_result.get('signals', [])
report['signal_status'] = sig_status
# ---- 综合评分引擎:整合外部因素(P0-P7)----
try:
from services.score_engine import compute_comprehensive_score
tech_score = report.get('deep_score', 50)
comprehensive = compute_comprehensive_score(
stock_code, stock_name, tech_score, df
)
report['comprehensive'] = comprehensive
# 用综合评分更新最终评分和评级
report['deep_score'] = comprehensive['final_score']
report['verdict'] = comprehensive['verdict']
report['score_reasons'].extend(comprehensive.get('all_reasons', []))
# ---- 根据综合评级修正买卖建议 ----
# 技术面推荐(compute_recommend)不含外部因素,
# 当综合评级与技术面推荐矛盾时,以综合评级为准调整推荐
final_score = comprehensive['final_score']
final_verdict = comprehensive['verdict']
orig_display = report['recommend'].get('display', '')
orig_reason = report['recommend'].get('reason', '')
orig_rate = report['recommend'].get('rate', 0)
# 综合评级偏空但技术面建议买入/加仓 → 降级为关注
if final_score < 50 and orig_display in ('买入', '加仓'):
report['recommend'] = {
'signal_type': 'watch',
'display': '关注',
'reason': f"技术面信号偏多,但综合评级「{final_verdict}」(外部因素拖累),建议观望",
'rate': final_score,
}
# 综合评级强烈看多但技术面建议观望/关注 → 升级为买入
elif final_score >= 80 and orig_display in ('观望', '关注', '观察'):
report['recommend'] = {
'signal_type': 'buy',
'display': '买入',
'reason': f"技术面{orig_display},但综合评级「{final_verdict}」(外部因素共振看好),建议买入",
'rate': final_score,
}
# 综合评级看空但技术面建议持有 → 降级为卖出
elif final_score < 35 and orig_display in ('持有', '观望'):
report['recommend'] = {
'signal_type': 'sell',
'display': '卖出',
'reason': f"技术面{orig_display},但综合评级「{final_verdict}」(外部因素重大利空),建议卖出",
'rate': final_score,
}
# 其他情况保持技术面推荐,但更新评分为综合评分
else:
report['recommend']['rate'] = final_score
except Exception as e:
print(f"综合评分引擎计算失败,使用技术面评分: {e}")
if realtime_info:
report['realtime'] = {
'price': float(realtime_info.get('price') or 0),
'change_pct': float(realtime_info.get('change_pct') or 0),
'pe': float(realtime_info.get('pe') or 0),
'pb': float(realtime_info.get('pb') or 0),
'total_market_cap': float(realtime_info.get('total_market_cap') or 0),
'volume': int(realtime_info.get('volume') or 0),
}
skip_llm = data.get('skip_llm', False)
if report.get('ai_summary') and not skip_llm:
try:
polished = _llm_polish_summary(
stock_name, stock_code, report['ai_summary'],
report.get('deep_score', 0), report.get('verdict', '')
)
if polished:
report['ai_summary']['text'] = polished['text']
report['ai_summary']['action_tip'] = polished['action_tip']
except Exception as e:
print(f"LLM润色失败,使用规则文本: {e}")
return jsonify({'success': True, 'report': report})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@bp.route('/realtime_price/<stock_code>', methods=['GET'])
def realtime_price(stock_code):
"""获取实时价格(直接调用实时API,不使用数据库缓存)"""
@@ -1178,3 +1316,73 @@ def get_bull_stocks():
import traceback
traceback.print_exc()
return jsonify({'success': False, 'error': str(e)}), 500
def _llm_polish_summary(stock_name, stock_code, ai_summary, score, verdict):
"""调用豆包LLM将规则模板生成的分析文本润色成更自然流畅的表达"""
import requests as _req
from config import Config
api_key = Config.DOUBAO_API_KEY
if not api_key:
return None
draft_text = ai_summary.get('text', '')
draft_action = ai_summary.get('action_tip', '')
prompt = f"""你是一位资深股票分析师,擅长用通俗易懂的语言给普通投资者解读技术分析。
以下是对{stock_name}({stock_code})的技术分析草稿,综合评分{score}分({verdict}):
【分析草稿】
{draft_text}
【操作建议草稿】
{draft_action}
请你将上面的草稿改写成更自然、更生动的表达。要求:
1. 用口语化表达,像老朋友聊天一样,避免专业术语堆砌
2. 保留所有关键数据和结论,不要遗漏
3. 适当加入比喻或生活化的表达,让小白也能听懂
4. 操作建议要明确、具体,有可操作性
5. 总字数控制在200字以内
6. 不要用markdown格式,纯文本即可
请严格按以下JSON格式输出,不要输出其他内容:
{{"text": "润色后的分析文本", "action_tip": "润色后的操作建议"}}"""
try:
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
payload = {
"model": "doubao-seed-1-6-251015",
"max_completion_tokens": 2048,
"stream": False,
"messages": [{"role": "user", "content": prompt}]
}
resp = _req.post(
"https://ark.cn-beijing.volces.com/api/v3/chat/completions",
headers=headers, json=payload, timeout=45
)
if resp.status_code != 200:
return None
data = resp.json()
content = data.get('choices', [{}])[0].get('message', {}).get('content', '')
if not content:
return None
content = content.strip()
if content.startswith('```'):
content = content.split('\n', 1)[-1].rsplit('```', 1)[0].strip()
import json as _json
result = _json.loads(content)
if result.get('text') and result.get('action_tip'):
return result
return None
except Exception as e:
print(f"LLM polish error: {e}")
return None