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