This commit is contained in:
aaron 2026-04-22 22:44:48 +08:00
parent eeddc58327
commit ea9fa6e5ff
14 changed files with 23 additions and 544 deletions

View File

@ -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
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
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
# 最新日期
@ -239,7 +238,6 @@ async def get_data_stats(admin: dict = Depends(get_current_admin)):
"tracking": track_count,
"sector_heat": sector_count,
"market_temperature": temp_count,
"daily_reviews": review_count,
"low_score_count": low_score,
"latest_date": str(latest_rec),
"earliest_date": str(earliest_rec),
@ -251,7 +249,7 @@ async def data_reset(req: DataResetRequest, admin: dict = Depends(get_current_ad
"""数据重置(管理员)
mode:
- "all": 清除所有业务数据推荐跟踪板块热度市场温度复盘
- "all": 清除所有业务数据推荐跟踪板块热度市场温度诊断
- "recommendations": 清除推荐记录和跟踪数据保留板块和市场温度
- "date_range": 清除指定日期之前的数据
- "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
result = await db.execute(text("DELETE FROM market_temperature"))
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"))
deleted["stock_diagnoses"] = result.rowcount or 0

View File

@ -109,7 +109,7 @@ async def system_status(_admin: dict = Depends(get_current_admin)):
# 各表数据量
tables_counts = {}
for t in ["recommendations", "sector_heat", "market_temperature",
"recommendation_tracking", "daily_reviews", "stock_diagnoses",
"recommendation_tracking", "stock_diagnoses",
"error_logs", "users"]:
result = await db.execute(text(f"SELECT COUNT(*) FROM {t}"))
tables_counts[t] = result.scalar() or 0

View File

@ -57,26 +57,6 @@ async def get_overview():
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")
async def get_strategy_board():
"""获取今日市场作战面板(只读,不触发 LLM"""
@ -123,13 +103,6 @@ async def get_ops_status():
"ORDER BY REPLACE(trade_date, '-', '') DESC, id DESC LIMIT 1"
)
)).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):
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_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_review_created_at": _fmt_dt(board_row._mapping["created_at"]) if board_row else "",
"status": "fresh" if latest_market_date else "empty",
"message": (
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
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():
"""盘中:腾讯实时指数行情"""
index_data = await tencent_client.get_index_realtime()

View File

@ -113,14 +113,6 @@ users_table = Table(
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", metadata,
Column("id", Integer, primary_key=True, autoincrement=True),

View File

@ -39,22 +39,6 @@ async def _run_scan(session_name: str):
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():
"""收盘后自动分析所有用户自选股。"""
logger.info("=== 开始自选股定时分析 ===")
@ -76,39 +60,26 @@ def setup_scheduler():
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(
_run_scan, CronTrigger(hour=9, minute=m, day_of_week="mon-fri"),
args=["morning_open"], id=f"morning_{m}", replace_existing=True
)
scheduler.add_job(
_run_scan, CronTrigger(hour=10, minute=0, day_of_week="mon-fri"),
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
_run_scan,
CronTrigger(hour=hour, minute=minute, day_of_week="mon-fri"),
args=[session_name],
id=job_id,
replace_existing=True,
)
# 收盘总结 16:00Tushare 日线数据通常在 15:30 后更新完成)
@ -117,12 +88,6 @@ def setup_scheduler():
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(
_run_watchlist_analysis, CronTrigger(hour=16, minute=20, day_of_week="mon-fri"),
id="watchlist_analysis", replace_existing=True

View File

@ -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}")

View File

@ -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}
## 明日关注
优先跟踪重点关注标的能否满足触发条件同时观察主线板块是否延续若市场温度回落或板块资金退潮应降低仓位并把未确认标的转回观察池"""

View File

@ -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)

View File

@ -1,7 +1,7 @@
{
"/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js",
"/(auth)/dashboard/page": "app/(auth)/dashboard/page.js",
"/(auth)/chat/page": "app/(auth)/chat/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"
}

View File

@ -608,7 +608,6 @@ export interface DataStats {
tracking: number;
sector_heat: number;
market_temperature: number;
daily_reviews: number;
low_score_count: number;
latest_date: string;
earliest_date: string;