update
This commit is contained in:
parent
eeddc58327
commit
ea9fa6e5ff
Binary file not shown.
Binary file not shown.
@ -227,7 +227,6 @@ async def get_data_stats(admin: dict = Depends(get_current_admin)):
|
|||||||
track_count = (await db.execute(text("SELECT COUNT(*) FROM recommendation_tracking"))).scalar() or 0
|
track_count = (await db.execute(text("SELECT COUNT(*) FROM recommendation_tracking"))).scalar() or 0
|
||||||
sector_count = (await db.execute(text("SELECT COUNT(*) FROM sector_heat"))).scalar() or 0
|
sector_count = (await db.execute(text("SELECT COUNT(*) FROM sector_heat"))).scalar() or 0
|
||||||
temp_count = (await db.execute(text("SELECT COUNT(*) FROM market_temperature"))).scalar() or 0
|
temp_count = (await db.execute(text("SELECT COUNT(*) FROM market_temperature"))).scalar() or 0
|
||||||
review_count = (await db.execute(text("SELECT COUNT(*) FROM daily_reviews"))).scalar() or 0
|
|
||||||
low_score = (await db.execute(text("SELECT COUNT(*) FROM recommendations WHERE score < 60"))).scalar() or 0
|
low_score = (await db.execute(text("SELECT COUNT(*) FROM recommendations WHERE score < 60"))).scalar() or 0
|
||||||
|
|
||||||
# 最新日期
|
# 最新日期
|
||||||
@ -239,7 +238,6 @@ async def get_data_stats(admin: dict = Depends(get_current_admin)):
|
|||||||
"tracking": track_count,
|
"tracking": track_count,
|
||||||
"sector_heat": sector_count,
|
"sector_heat": sector_count,
|
||||||
"market_temperature": temp_count,
|
"market_temperature": temp_count,
|
||||||
"daily_reviews": review_count,
|
|
||||||
"low_score_count": low_score,
|
"low_score_count": low_score,
|
||||||
"latest_date": str(latest_rec),
|
"latest_date": str(latest_rec),
|
||||||
"earliest_date": str(earliest_rec),
|
"earliest_date": str(earliest_rec),
|
||||||
@ -251,7 +249,7 @@ async def data_reset(req: DataResetRequest, admin: dict = Depends(get_current_ad
|
|||||||
"""数据重置(管理员)
|
"""数据重置(管理员)
|
||||||
|
|
||||||
mode:
|
mode:
|
||||||
- "all": 清除所有业务数据(推荐、跟踪、板块热度、市场温度、复盘)
|
- "all": 清除所有业务数据(推荐、跟踪、板块热度、市场温度、诊断)
|
||||||
- "recommendations": 清除推荐记录和跟踪数据,保留板块和市场温度
|
- "recommendations": 清除推荐记录和跟踪数据,保留板块和市场温度
|
||||||
- "date_range": 清除指定日期之前的数据
|
- "date_range": 清除指定日期之前的数据
|
||||||
- "low_score": 清除低分推荐(score < min_score)和过期跟踪数据
|
- "low_score": 清除低分推荐(score < min_score)和过期跟踪数据
|
||||||
@ -269,8 +267,6 @@ async def data_reset(req: DataResetRequest, admin: dict = Depends(get_current_ad
|
|||||||
deleted["sector_heat"] = result.rowcount or 0
|
deleted["sector_heat"] = result.rowcount or 0
|
||||||
result = await db.execute(text("DELETE FROM market_temperature"))
|
result = await db.execute(text("DELETE FROM market_temperature"))
|
||||||
deleted["market_temperature"] = result.rowcount or 0
|
deleted["market_temperature"] = result.rowcount or 0
|
||||||
result = await db.execute(text("DELETE FROM daily_reviews"))
|
|
||||||
deleted["daily_reviews"] = result.rowcount or 0
|
|
||||||
result = await db.execute(text("DELETE FROM stock_diagnoses"))
|
result = await db.execute(text("DELETE FROM stock_diagnoses"))
|
||||||
deleted["stock_diagnoses"] = result.rowcount or 0
|
deleted["stock_diagnoses"] = result.rowcount or 0
|
||||||
|
|
||||||
|
|||||||
@ -109,7 +109,7 @@ async def system_status(_admin: dict = Depends(get_current_admin)):
|
|||||||
# 各表数据量
|
# 各表数据量
|
||||||
tables_counts = {}
|
tables_counts = {}
|
||||||
for t in ["recommendations", "sector_heat", "market_temperature",
|
for t in ["recommendations", "sector_heat", "market_temperature",
|
||||||
"recommendation_tracking", "daily_reviews", "stock_diagnoses",
|
"recommendation_tracking", "stock_diagnoses",
|
||||||
"error_logs", "users"]:
|
"error_logs", "users"]:
|
||||||
result = await db.execute(text(f"SELECT COUNT(*) FROM {t}"))
|
result = await db.execute(text(f"SELECT COUNT(*) FROM {t}"))
|
||||||
tables_counts[t] = result.scalar() or 0
|
tables_counts[t] = result.scalar() or 0
|
||||||
|
|||||||
@ -57,26 +57,6 @@ async def get_overview():
|
|||||||
return _overview_daily()
|
return _overview_daily()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/daily-review")
|
|
||||||
async def get_daily_review():
|
|
||||||
"""获取每日复盘报告"""
|
|
||||||
from sqlalchemy import text
|
|
||||||
from app.db.database import get_db
|
|
||||||
async with get_db() as db:
|
|
||||||
result = await db.execute(
|
|
||||||
text("SELECT * FROM daily_reviews ORDER BY trade_date DESC LIMIT 5")
|
|
||||||
)
|
|
||||||
reviews = []
|
|
||||||
for row in result.fetchall():
|
|
||||||
r = row._mapping
|
|
||||||
reviews.append({
|
|
||||||
"trade_date": r["trade_date"],
|
|
||||||
"content": r["content"] or "",
|
|
||||||
"created_at": str(r["created_at"]) if r["created_at"] else "",
|
|
||||||
})
|
|
||||||
return {"reviews": reviews}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/strategy-board")
|
@router.get("/strategy-board")
|
||||||
async def get_strategy_board():
|
async def get_strategy_board():
|
||||||
"""获取今日市场作战面板(只读,不触发 LLM)"""
|
"""获取今日市场作战面板(只读,不触发 LLM)"""
|
||||||
@ -123,13 +103,6 @@ async def get_ops_status():
|
|||||||
"ORDER BY REPLACE(trade_date, '-', '') DESC, id DESC LIMIT 1"
|
"ORDER BY REPLACE(trade_date, '-', '') DESC, id DESC LIMIT 1"
|
||||||
)
|
)
|
||||||
)).fetchone()
|
)).fetchone()
|
||||||
board_row = (await db.execute(
|
|
||||||
text(
|
|
||||||
"SELECT created_at FROM daily_reviews "
|
|
||||||
"ORDER BY trade_date DESC LIMIT 1"
|
|
||||||
)
|
|
||||||
)).fetchone()
|
|
||||||
|
|
||||||
def _fmt_dt(value):
|
def _fmt_dt(value):
|
||||||
return str(value or "")
|
return str(value or "")
|
||||||
|
|
||||||
@ -149,7 +122,6 @@ async def get_ops_status():
|
|||||||
"last_tracking_created_at": _fmt_dt(tracking_row._mapping["created_at"]) if tracking_row else "",
|
"last_tracking_created_at": _fmt_dt(tracking_row._mapping["created_at"]) if tracking_row else "",
|
||||||
"last_market_created_at": _fmt_dt(market_row._mapping["created_at"]) if market_row else "",
|
"last_market_created_at": _fmt_dt(market_row._mapping["created_at"]) if market_row else "",
|
||||||
"last_sector_created_at": _fmt_dt(sector_row._mapping["created_at"]) if sector_row else "",
|
"last_sector_created_at": _fmt_dt(sector_row._mapping["created_at"]) if sector_row else "",
|
||||||
"last_review_created_at": _fmt_dt(board_row._mapping["created_at"]) if board_row else "",
|
|
||||||
"status": "fresh" if latest_market_date else "empty",
|
"status": "fresh" if latest_market_date else "empty",
|
||||||
"message": (
|
"message": (
|
||||||
f"最新市场日期 {latest_market_date},最近跟踪 {latest_tracking_date or '暂无'}"
|
f"最新市场日期 {latest_market_date},最近跟踪 {latest_tracking_date or '暂无'}"
|
||||||
@ -180,15 +152,6 @@ async def generate_strategy_iteration(limit: int = 50, _admin: dict = Depends(ge
|
|||||||
from app.llm.strategy_iteration import build_strategy_iteration_report
|
from app.llm.strategy_iteration import build_strategy_iteration_report
|
||||||
return await build_strategy_iteration_report(limit=limit, include_llm=True)
|
return await build_strategy_iteration_report(limit=limit, include_llm=True)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate-review")
|
|
||||||
async def generate_daily_review(_admin: dict = Depends(get_current_admin)):
|
|
||||||
"""手动触发生成每日复盘"""
|
|
||||||
from app.llm.daily_review import generate_review
|
|
||||||
result = await generate_review()
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
async def _overview_realtime():
|
async def _overview_realtime():
|
||||||
"""盘中:腾讯实时指数行情"""
|
"""盘中:腾讯实时指数行情"""
|
||||||
index_data = await tencent_client.get_index_realtime()
|
index_data = await tencent_client.get_index_realtime()
|
||||||
|
|||||||
Binary file not shown.
@ -113,14 +113,6 @@ users_table = Table(
|
|||||||
Column("updated_at", DateTime, server_default=func.now()),
|
Column("updated_at", DateTime, server_default=func.now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
daily_reviews_table = Table(
|
|
||||||
"daily_reviews", metadata,
|
|
||||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
|
||||||
Column("trade_date", Text, nullable=False, unique=True),
|
|
||||||
Column("content", Text, default=""),
|
|
||||||
Column("created_at", DateTime, server_default=func.now()),
|
|
||||||
)
|
|
||||||
|
|
||||||
stock_diagnoses_table = Table(
|
stock_diagnoses_table = Table(
|
||||||
"stock_diagnoses", metadata,
|
"stock_diagnoses", metadata,
|
||||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||||
|
|||||||
Binary file not shown.
@ -39,22 +39,6 @@ async def _run_scan(session_name: str):
|
|||||||
await log_error("scheduler", f"定时扫描失败 ({session_name}): {e}", detail=traceback.format_exc())
|
await log_error("scheduler", f"定时扫描失败 ({session_name}): {e}", detail=traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
async def _generate_daily_review():
|
|
||||||
"""16:10 自动生成每日复盘报告"""
|
|
||||||
logger.info("=== 开始生成每日复盘报告 ===")
|
|
||||||
try:
|
|
||||||
from app.llm.daily_review import generate_review
|
|
||||||
result = await generate_review()
|
|
||||||
if result.get("status") == "ok":
|
|
||||||
logger.info(f"复盘报告生成成功: {result.get('trade_date')}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"复盘报告生成失败: {result.get('message')}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"复盘报告生成异常: {e}")
|
|
||||||
from app.db.error_logger import log_error
|
|
||||||
await log_error("scheduler", f"复盘报告生成异常: {e}", detail=traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
async def _run_watchlist_analysis():
|
async def _run_watchlist_analysis():
|
||||||
"""收盘后自动分析所有用户自选股。"""
|
"""收盘后自动分析所有用户自选股。"""
|
||||||
logger.info("=== 开始自选股定时分析 ===")
|
logger.info("=== 开始自选股定时分析 ===")
|
||||||
@ -76,39 +60,26 @@ def setup_scheduler():
|
|||||||
args=["pre_market"], id="pre_market", replace_existing=True
|
args=["pre_market"], id="pre_market", replace_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# 早盘扫描 09:35-09:55 每5分钟 + 10:00
|
# 盘中扫描:按交易节奏执行,避免高频重复计算
|
||||||
for m in range(35, 60, 5):
|
scan_schedule = [
|
||||||
|
("morning_open_0935", 9, 35, "morning_open"),
|
||||||
|
("morning_open_0950", 9, 50, "morning_open"),
|
||||||
|
("morning_mid_1020", 10, 20, "morning_mid"),
|
||||||
|
("morning_mid_1050", 10, 50, "morning_mid"),
|
||||||
|
("morning_mid_1120", 11, 20, "morning_mid"),
|
||||||
|
("afternoon_1310", 13, 10, "afternoon"),
|
||||||
|
("afternoon_1340", 13, 40, "afternoon"),
|
||||||
|
("late_1410", 14, 10, "late_session"),
|
||||||
|
("late_1440", 14, 40, "late_session"),
|
||||||
|
("close_1500", 15, 0, "late_session"),
|
||||||
|
]
|
||||||
|
for job_id, hour, minute, session_name in scan_schedule:
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
_run_scan, CronTrigger(hour=9, minute=m, day_of_week="mon-fri"),
|
_run_scan,
|
||||||
args=["morning_open"], id=f"morning_{m}", replace_existing=True
|
CronTrigger(hour=hour, minute=minute, day_of_week="mon-fri"),
|
||||||
)
|
args=[session_name],
|
||||||
scheduler.add_job(
|
id=job_id,
|
||||||
_run_scan, CronTrigger(hour=10, minute=0, day_of_week="mon-fri"),
|
replace_existing=True,
|
||||||
args=["morning_open"], id="morning_1000", replace_existing=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 上午盘中 10:10-11:30 - 每10分钟
|
|
||||||
for h in [10, 11]:
|
|
||||||
start_m = 10 if h == 10 else 0
|
|
||||||
end_m = 60 if h == 10 else 31
|
|
||||||
for m in range(start_m, end_m, 10):
|
|
||||||
scheduler.add_job(
|
|
||||||
_run_scan, CronTrigger(hour=h, minute=m, day_of_week="mon-fri"),
|
|
||||||
args=["morning_mid"], id=f"morning_mid_{h}_{m}", replace_existing=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 午后扫描 13:00-14:00 - 每10分钟
|
|
||||||
for m in range(0, 60, 10):
|
|
||||||
scheduler.add_job(
|
|
||||||
_run_scan, CronTrigger(hour=13, minute=m, day_of_week="mon-fri"),
|
|
||||||
args=["afternoon"], id=f"afternoon_{m}", replace_existing=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 尾盘扫描 14:00-14:50 - 每10分钟
|
|
||||||
for m in range(0, 51, 10):
|
|
||||||
scheduler.add_job(
|
|
||||||
_run_scan, CronTrigger(hour=14, minute=m, day_of_week="mon-fri"),
|
|
||||||
args=["late_session"], id=f"late_{m}", replace_existing=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 收盘总结 16:00(Tushare 日线数据通常在 15:30 后更新完成)
|
# 收盘总结 16:00(Tushare 日线数据通常在 15:30 后更新完成)
|
||||||
@ -117,12 +88,6 @@ def setup_scheduler():
|
|||||||
args=["post_market"], id="post_market", replace_existing=True
|
args=["post_market"], id="post_market", replace_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# 每日复盘报告 16:10(扫描完成后生成)
|
|
||||||
scheduler.add_job(
|
|
||||||
_generate_daily_review, CronTrigger(hour=16, minute=10, day_of_week="mon-fri"),
|
|
||||||
id="daily_review", replace_existing=True
|
|
||||||
)
|
|
||||||
|
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
_run_watchlist_analysis, CronTrigger(hour=16, minute=20, day_of_week="mon-fri"),
|
_run_watchlist_analysis, CronTrigger(hour=16, minute=20, day_of_week="mon-fri"),
|
||||||
id="watchlist_analysis", replace_existing=True
|
id="watchlist_analysis", replace_existing=True
|
||||||
|
|||||||
@ -1,252 +0,0 @@
|
|||||||
"""AI 深度分析
|
|
||||||
|
|
||||||
预先获取 K 线、资金流、技术信号等数据,一次性传入 LLM 生成结构化分析报告。
|
|
||||||
不依赖 tool calling,避免 DeepSeek DSML 标签问题。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from app.llm.client import chat_completion
|
|
||||||
from app.llm.prompts import TREND_BREAKOUT_ANALYSIS_PROMPT
|
|
||||||
from app.config import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def analyze_recommendations(result: dict) -> None:
|
|
||||||
"""对所有推荐股票执行 AI 深度分析"""
|
|
||||||
recommendations = result.get("recommendations", [])
|
|
||||||
if not recommendations or not settings.deepseek_api_key:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await _do_analyze(result, recommendations)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"AI 分析任务异常: {e}")
|
|
||||||
from app.db.error_logger import log_error
|
|
||||||
await log_error("analysis_agent", f"AI 分析任务异常: {e}", detail=traceback.format_exc())
|
|
||||||
for rec in recommendations:
|
|
||||||
if not rec.llm_analysis:
|
|
||||||
rec.llm_analysis = "AI 分析暂时不可用"
|
|
||||||
await _broadcast_llm_ready(recommendations)
|
|
||||||
|
|
||||||
|
|
||||||
async def _do_analyze(result: dict, recommendations: list) -> None:
|
|
||||||
"""分析核心逻辑"""
|
|
||||||
market_temp = result.get("market_temp")
|
|
||||||
hot_sectors = result.get("hot_sectors", [])
|
|
||||||
|
|
||||||
# 构建板块文本
|
|
||||||
sectors_text = "\n".join(
|
|
||||||
f"- {s.sector_name}: 涨幅{s.pct_change}%, 资金流入{s.capital_inflow}万, "
|
|
||||||
f"涨停{s.limit_up_count}家, 热度{s.heat_score}分, 阶段={s.stage}"
|
|
||||||
for s in hot_sectors[:5]
|
|
||||||
) if hot_sectors else "暂无板块数据"
|
|
||||||
|
|
||||||
# 温度等级
|
|
||||||
temp_val = market_temp.temperature if market_temp else 0
|
|
||||||
if temp_val >= 60:
|
|
||||||
temp_level = "积极"
|
|
||||||
elif temp_val >= 30:
|
|
||||||
temp_level = "谨慎"
|
|
||||||
else:
|
|
||||||
temp_level = "低迷"
|
|
||||||
|
|
||||||
enhanced_count = 0
|
|
||||||
for rec in recommendations:
|
|
||||||
try:
|
|
||||||
# 预先获取该股票的详细数据
|
|
||||||
stock_data = await _fetch_stock_data(rec.ts_code, rec.sector)
|
|
||||||
|
|
||||||
strategy_label = "趋势突破"
|
|
||||||
signal_map = {"breakout": "突破型", "pullback": "回踩型", "launch": "启动型", "none": "无信号"}
|
|
||||||
entry_label = signal_map.get(rec.entry_signal_type, "无信号")
|
|
||||||
system_prompt = TREND_BREAKOUT_ANALYSIS_PROMPT
|
|
||||||
|
|
||||||
user_msg = _build_user_message(
|
|
||||||
rec=rec,
|
|
||||||
strategy_label=strategy_label,
|
|
||||||
entry_label=entry_label,
|
|
||||||
market_temp=market_temp,
|
|
||||||
temp_level=temp_level,
|
|
||||||
sectors_text=sectors_text,
|
|
||||||
stock_data=stock_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{"role": "system", "content": system_prompt},
|
|
||||||
{"role": "user", "content": user_msg},
|
|
||||||
]
|
|
||||||
|
|
||||||
resp = await chat_completion(messages)
|
|
||||||
if resp and resp.content:
|
|
||||||
analysis = resp.content.strip()
|
|
||||||
rec.llm_analysis = analysis
|
|
||||||
rec.llm_score = _extract_score(analysis)
|
|
||||||
enhanced_count += 1
|
|
||||||
else:
|
|
||||||
rec.llm_analysis = "AI 分析暂时不可用"
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logger.warning(f"AI 分析 {rec.ts_code} 被取消")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"AI 分析 {rec.ts_code} 失败: {e}")
|
|
||||||
rec.llm_analysis = "AI 分析暂时不可用"
|
|
||||||
|
|
||||||
# 无论成功失败都保存并广播
|
|
||||||
await _save_llm_analysis_to_db(recommendations)
|
|
||||||
await _broadcast_llm_ready(recommendations)
|
|
||||||
|
|
||||||
logger.info(f"AI 深度分析完成: {enhanced_count}/{len(recommendations)} 条")
|
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_stock_data(ts_code: str, sector: str) -> str:
|
|
||||||
"""预先获取个股详细数据,拼接为文本供 LLM 分析"""
|
|
||||||
from app.llm.tool_executor import (
|
|
||||||
_get_stock_kline,
|
|
||||||
_get_stock_capital_flow,
|
|
||||||
_get_stock_technical_signal,
|
|
||||||
_get_sector_performance,
|
|
||||||
)
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
# K 线(最近 30 天摘要)
|
|
||||||
try:
|
|
||||||
kline_text = await _get_stock_kline(ts_code, 60)
|
|
||||||
kline_data = json.loads(kline_text)
|
|
||||||
if isinstance(kline_data, list) and kline_data:
|
|
||||||
# 只取最近 10 条以控制 token
|
|
||||||
recent = kline_data[-10:]
|
|
||||||
kline_summary = "\n".join(
|
|
||||||
f" {d.get('trade_date', '')}: 收{d.get('close', '')} "
|
|
||||||
f"涨跌{d.get('pct_chg', '')}% 量{d.get('vol', '')} "
|
|
||||||
f"MA5={d.get('ma5', '')} MA10={d.get('ma10', '')} MA20={d.get('ma20', '')} "
|
|
||||||
f"DIF={d.get('dif', '')} DEA={d.get('dea', '')} RSI={d.get('rsi14', '')}"
|
|
||||||
for d in recent
|
|
||||||
)
|
|
||||||
parts.append(f"## K线数据(近10日)\n{kline_summary}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"获取K线数据失败 {ts_code}: {e}")
|
|
||||||
|
|
||||||
# 资金流向
|
|
||||||
try:
|
|
||||||
flow_text = await _get_stock_capital_flow(ts_code, 5)
|
|
||||||
flow_data = json.loads(flow_text)
|
|
||||||
if isinstance(flow_data, list) and flow_data:
|
|
||||||
flow_summary = "\n".join(
|
|
||||||
f" {d.get('trade_date', '')}: 主力净流入{d.get('main_net_inflow', 0)}万"
|
|
||||||
for d in flow_data[-5:]
|
|
||||||
)
|
|
||||||
parts.append(f"## 资金流向(近5日)\n{flow_summary}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"获取资金流向失败 {ts_code}: {e}")
|
|
||||||
|
|
||||||
# 技术信号
|
|
||||||
try:
|
|
||||||
signal_text = await _get_stock_technical_signal(ts_code)
|
|
||||||
parts.append(f"## 技术信号\n{signal_text}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"获取技术信号失败 {ts_code}: {e}")
|
|
||||||
|
|
||||||
# 板块表现
|
|
||||||
if sector:
|
|
||||||
try:
|
|
||||||
sector_text = await _get_sector_performance(sector)
|
|
||||||
parts.append(f"## 板块数据\n{sector_text}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"获取板块数据失败 {sector}: {e}")
|
|
||||||
|
|
||||||
return "\n\n".join(parts) if parts else "暂无额外数据"
|
|
||||||
|
|
||||||
|
|
||||||
def _build_user_message(
|
|
||||||
rec,
|
|
||||||
strategy_label: str,
|
|
||||||
entry_label: str,
|
|
||||||
market_temp,
|
|
||||||
temp_level: str,
|
|
||||||
sectors_text: str,
|
|
||||||
stock_data: str,
|
|
||||||
) -> str:
|
|
||||||
"""构建完整的用户消息(含预获取的数据)"""
|
|
||||||
return f"""## 量化系统数据
|
|
||||||
- 股票: {rec.name}({rec.ts_code})
|
|
||||||
- 所属板块: {rec.sector}
|
|
||||||
- 策略类型: {strategy_label}
|
|
||||||
- 入场信号: {entry_label}
|
|
||||||
- 综合评分: {rec.score}分({rec.level})
|
|
||||||
- 各维度: 市场{rec.market_temp_score} | 板块{rec.sector_score} | 资金{rec.capital_score} | 技术{rec.technical_score} | 位置{rec.position_score} | 估值{rec.valuation_score}
|
|
||||||
- 信号: {rec.signal}
|
|
||||||
- 参考价: 入场{rec.entry_price or 'N/A'} / 目标{rec.target_price or 'N/A'} / 止损{rec.stop_loss or 'N/A'}
|
|
||||||
- 量化理由: {";".join(rec.reasons) if rec.reasons else "无"}
|
|
||||||
|
|
||||||
## 市场环境
|
|
||||||
- 市场温度: {market_temp.temperature if market_temp else 'N/A'}/100({temp_level})
|
|
||||||
- 涨跌比: {market_temp.up_count if market_temp else 0}涨 / {market_temp.down_count if market_temp else 0}跌
|
|
||||||
- 涨停: {market_temp.limit_up_count if market_temp else 0}家
|
|
||||||
|
|
||||||
## 热门板块
|
|
||||||
{sectors_text}
|
|
||||||
|
|
||||||
## 个股详细数据
|
|
||||||
{stock_data}
|
|
||||||
|
|
||||||
请根据以上所有数据,按照指定格式输出深度分析报告。"""
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_score(text: str) -> float | None:
|
|
||||||
"""从 AI 分析报告中提取评分(1-10)"""
|
|
||||||
match = re.search(r"###\s*AI\s*评分[^\d]*(\d+(?:\.\d+)?)", text)
|
|
||||||
if match:
|
|
||||||
score = float(match.group(1))
|
|
||||||
return min(max(score, 1), 10)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def _save_llm_analysis_to_db(recommendations: list) -> None:
|
|
||||||
"""将 AI 分析结果更新到数据库"""
|
|
||||||
try:
|
|
||||||
from app.db.database import get_db
|
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
async with get_db() as db:
|
|
||||||
for rec in recommendations:
|
|
||||||
if not rec.llm_analysis:
|
|
||||||
continue
|
|
||||||
await db.execute(
|
|
||||||
text(
|
|
||||||
"UPDATE recommendations SET llm_analysis = :analysis, "
|
|
||||||
"llm_score = :score "
|
|
||||||
"WHERE ts_code = :code AND date(created_at) = date('now', 'localtime') "
|
|
||||||
"AND scan_session = :session"
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"analysis": rec.llm_analysis,
|
|
||||||
"score": rec.llm_score,
|
|
||||||
"code": rec.ts_code,
|
|
||||||
"session": rec.scan_session,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"保存 AI 分析到数据库失败: {e}")
|
|
||||||
from app.db.error_logger import log_error
|
|
||||||
await log_error("analysis_agent", f"保存 AI 分析到数据库失败: {e}", detail=traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
async def _broadcast_llm_ready(recommendations: list) -> None:
|
|
||||||
"""通过 WebSocket 广播 AI 分析完成事件"""
|
|
||||||
try:
|
|
||||||
from app.api.websocket import broadcast_update
|
|
||||||
await broadcast_update({
|
|
||||||
"type": "llm_analysis_ready",
|
|
||||||
"count": len([r for r in recommendations if r.llm_analysis]),
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"广播 AI 分析完成失败: {e}")
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
"""每日复盘报告生成"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from app.config import settings, today_trade_date
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def generate_review() -> dict:
|
|
||||||
"""生成每日复盘报告"""
|
|
||||||
from app.data.tushare_client import tushare_client
|
|
||||||
from app.data import tencent_client
|
|
||||||
from app.engine.recommender import get_latest_recommendations, get_latest_sectors
|
|
||||||
|
|
||||||
latest_trade_date = tushare_client.get_latest_trade_date()
|
|
||||||
trade_date = today_trade_date()
|
|
||||||
|
|
||||||
# 收集市场数据
|
|
||||||
result = await get_latest_recommendations()
|
|
||||||
mt = result.get("market_temp")
|
|
||||||
sectors = await get_latest_sectors()
|
|
||||||
recs = result.get("recommendations", [])
|
|
||||||
|
|
||||||
# 实时指数
|
|
||||||
try:
|
|
||||||
index_data = await tencent_client.get_index_realtime()
|
|
||||||
except Exception:
|
|
||||||
index_data = {}
|
|
||||||
|
|
||||||
# 构建数据摘要
|
|
||||||
market_summary = ""
|
|
||||||
if mt:
|
|
||||||
market_summary = (
|
|
||||||
f"市场温度: {mt.temperature}/100, "
|
|
||||||
f"上涨{mt.up_count}家/下跌{mt.down_count}家, "
|
|
||||||
f"涨停{mt.limit_up_count}家/跌停{mt.limit_down_count}家, "
|
|
||||||
f"连板最高{mt.max_streak}板, 炸板率{mt.broken_rate}%"
|
|
||||||
)
|
|
||||||
|
|
||||||
index_summary = ""
|
|
||||||
name_map = {"000001.SH": "上证", "399001.SZ": "深证", "399006.SZ": "创业板"}
|
|
||||||
for code in ["000001.SH", "399001.SZ", "399006.SZ"]:
|
|
||||||
d = index_data.get(code, {})
|
|
||||||
if d:
|
|
||||||
index_summary += f"{name_map[code]}: {d.get('price', 0):.2f} ({d.get('pct_chg', 0):+.2f}%), "
|
|
||||||
|
|
||||||
sector_summary = "热门板块: " + "、".join(
|
|
||||||
f"{s.sector_name}({(getattr(s, 'realtime_pct_change', None) or s.pct_change):+.1f}%)" for s in sectors[:5]
|
|
||||||
)
|
|
||||||
|
|
||||||
rec_summary = ""
|
|
||||||
for r in recs[:5]:
|
|
||||||
signal_map = {"breakout": "突破", "breakout_confirm": "确认", "pullback": "回踩",
|
|
||||||
"launch": "启动", "reversal": "反转"}
|
|
||||||
st = signal_map.get(r.entry_signal_type, r.entry_signal_type)
|
|
||||||
rec_summary += f"\n- {r.name}({r.ts_code}): {st}型, 评分{r.score}, {r.signal}"
|
|
||||||
|
|
||||||
user_msg = f"""请根据以下数据生成今日A股市场复盘报告(中文):
|
|
||||||
|
|
||||||
日期: {trade_date}
|
|
||||||
Tushare 最新交易日: {latest_trade_date}
|
|
||||||
{market_summary}
|
|
||||||
{index_summary}
|
|
||||||
{sector_summary}
|
|
||||||
|
|
||||||
今日推荐股票:
|
|
||||||
{rec_summary}
|
|
||||||
|
|
||||||
请按以下格式输出(Markdown格式,总字数300-500字):
|
|
||||||
## 市场概况
|
|
||||||
(指数走势、量能变化)
|
|
||||||
|
|
||||||
## 板块热点
|
|
||||||
(哪些板块领涨、资金流向)
|
|
||||||
|
|
||||||
## 交易机会
|
|
||||||
(今日推荐个股简要点评)
|
|
||||||
|
|
||||||
## 明日关注
|
|
||||||
(关注方向和操作建议)"""
|
|
||||||
|
|
||||||
if settings.deepseek_api_key:
|
|
||||||
try:
|
|
||||||
from app.llm.client import get_client
|
|
||||||
|
|
||||||
client = get_client()
|
|
||||||
response = await client.chat.completions.create(
|
|
||||||
model=settings.deepseek_model,
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": "你是一位专业的A股市场分析师,擅长市场复盘和策略分析。回复使用Markdown格式,简洁专业。"},
|
|
||||||
{"role": "user", "content": user_msg},
|
|
||||||
],
|
|
||||||
max_tokens=1500,
|
|
||||||
temperature=0.5,
|
|
||||||
)
|
|
||||||
content = response.choices[0].message.content.strip()
|
|
||||||
generated_by = "llm"
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"生成复盘报告失败,使用规则兜底: {e}")
|
|
||||||
content = _build_fallback_review(
|
|
||||||
trade_date=trade_date,
|
|
||||||
market_summary=market_summary,
|
|
||||||
index_summary=index_summary,
|
|
||||||
sector_summary=sector_summary,
|
|
||||||
recommendations=recs,
|
|
||||||
)
|
|
||||||
generated_by = "rules"
|
|
||||||
else:
|
|
||||||
content = _build_fallback_review(
|
|
||||||
trade_date=trade_date,
|
|
||||||
market_summary=market_summary,
|
|
||||||
index_summary=index_summary,
|
|
||||||
sector_summary=sector_summary,
|
|
||||||
recommendations=recs,
|
|
||||||
)
|
|
||||||
generated_by = "rules"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 保存到数据库
|
|
||||||
from sqlalchemy import text
|
|
||||||
from app.db.database import get_db
|
|
||||||
async with get_db() as db:
|
|
||||||
await db.execute(
|
|
||||||
text(
|
|
||||||
"INSERT OR REPLACE INTO daily_reviews (trade_date, content) "
|
|
||||||
"VALUES (:td, :content)"
|
|
||||||
),
|
|
||||||
{"td": trade_date, "content": content},
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
logger.info(f"已生成 {trade_date} 复盘报告 ({generated_by})")
|
|
||||||
return {"status": "ok", "trade_date": trade_date, "content": content, "generated_by": generated_by}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"保存复盘报告失败: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
def _build_fallback_review(
|
|
||||||
trade_date: str,
|
|
||||||
market_summary: str,
|
|
||||||
index_summary: str,
|
|
||||||
sector_summary: str,
|
|
||||||
recommendations: list,
|
|
||||||
) -> str:
|
|
||||||
"""LLM 不可用时生成结构化规则复盘,避免页面空白。"""
|
|
||||||
actionable = [r for r in recommendations if getattr(r, "action_plan", "") == "可操作"]
|
|
||||||
watch = [r for r in recommendations if getattr(r, "action_plan", "") == "重点关注"]
|
|
||||||
top_recs = recommendations[:5]
|
|
||||||
rec_lines = "\n".join(
|
|
||||||
f"- {r.name}({r.ts_code}):{getattr(r, 'action_plan', '观察')},"
|
|
||||||
f"{getattr(r, 'entry_signal_type', 'none')} 信号,评分 {getattr(r, 'score', 0)}。"
|
|
||||||
for r in top_recs
|
|
||||||
) or "- 暂无推荐标的。"
|
|
||||||
|
|
||||||
return f"""## 市场概况
|
|
||||||
{trade_date} 市场温度处于中性偏谨慎区间。{market_summary or "暂无市场温度数据。"} {index_summary or ""}
|
|
||||||
|
|
||||||
## 板块热点
|
|
||||||
{sector_summary or "暂无板块热度数据。"} 当前板块证据主要用于确认推荐方向是否有资金和赚钱效应支撑。
|
|
||||||
|
|
||||||
## 交易机会
|
|
||||||
今日推荐池共 {len(recommendations)} 只,其中可操作 {len(actionable)} 只、重点关注 {len(watch)} 只。当前更适合按触发条件等待确认,不宜把观察标的直接当作买入标的。
|
|
||||||
{rec_lines}
|
|
||||||
|
|
||||||
## 明日关注
|
|
||||||
优先跟踪重点关注标的能否满足触发条件,同时观察主线板块是否延续。若市场温度回落或板块资金退潮,应降低仓位并把未确认标的转回观察池。"""
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
"""推荐结果 LLM 增强
|
|
||||||
|
|
||||||
现在统一使用 analysis_agent 模块进行深度分析。
|
|
||||||
此文件保留为兼容入口,内部直接调用 analysis_agent。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def enhance_recommendations(result: dict) -> None:
|
|
||||||
"""对推荐结果进行 LLM 增强分析(兼容入口,委托给 analysis_agent)"""
|
|
||||||
from app.llm.analysis_agent import analyze_recommendations
|
|
||||||
await analyze_recommendations(result)
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js",
|
|
||||||
"/(auth)/dashboard/page": "app/(auth)/dashboard/page.js",
|
"/(auth)/dashboard/page": "app/(auth)/dashboard/page.js",
|
||||||
"/(auth)/chat/page": "app/(auth)/chat/page.js",
|
"/(auth)/chat/page": "app/(auth)/chat/page.js",
|
||||||
"/(public)/login/page": "app/(public)/login/page.js",
|
"/(public)/login/page": "app/(public)/login/page.js",
|
||||||
|
"/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js",
|
||||||
"/(public)/page": "app/(public)/page.js"
|
"/(public)/page": "app/(public)/page.js"
|
||||||
}
|
}
|
||||||
@ -608,7 +608,6 @@ export interface DataStats {
|
|||||||
tracking: number;
|
tracking: number;
|
||||||
sector_heat: number;
|
sector_heat: number;
|
||||||
market_temperature: number;
|
market_temperature: number;
|
||||||
daily_reviews: number;
|
|
||||||
low_score_count: number;
|
low_score_count: number;
|
||||||
latest_date: string;
|
latest_date: string;
|
||||||
earliest_date: string;
|
earliest_date: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user