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
|
||||
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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -145,4 +145,4 @@ async def system_status(_admin: dict = Depends(get_current_admin)):
|
||||
"last_errors": last_errors,
|
||||
"tables_counts": tables_counts,
|
||||
"db_size_mb": db_size_mb,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
Binary file not shown.
@ -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),
|
||||
|
||||
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())
|
||||
|
||||
|
||||
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:00(Tushare 日线数据通常在 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
|
||||
|
||||
@ -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)/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"
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user