feat: AI分析第二档——get_news消息面、板块主力资金流TOPS、两融余额、截断续写机制
This commit is contained in:
@@ -29,11 +29,12 @@ SYSTEM_PROMPT = """你是一位专业的A股市场分析师,擅长从数据中
|
||||
- 风险提示,每次推荐都需说明风险点
|
||||
|
||||
可用工具:
|
||||
- get_market_dashboard: 获取市场整体数据(指数/涨跌统计/市场温度/连板梯队/行业强度/事件情报)
|
||||
- get_market_dashboard: 获取市场整体数据(指数/涨跌统计/市场温度/连板梯队/行业强度/板块资金流/两融/事件情报)
|
||||
- get_theme_history: 获取指定日期的题材涨幅排行
|
||||
- get_active_core_stocks: 获取核心股追踪数据(10日涨幅矩阵+所属题材)
|
||||
- get_stock_quote: 获取个股实时行情
|
||||
- get_fund_flow: 获取个股资金流向
|
||||
- get_news: 获取财经快讯(新浪7x24,用于重要消息面)
|
||||
|
||||
重要规则:
|
||||
1. 你必须先调用工具获取数据,然后基于数据进行分析
|
||||
@@ -47,20 +48,25 @@ DAILY_ANALYSIS_PROMPT = """请对 {trade_date} 的A股市场进行收盘分析
|
||||
{prev_snapshot_section}
|
||||
|
||||
请先调用以下工具获取数据:
|
||||
1. get_market_dashboard - 获取市场整体数据(含涨跌统计、市场温度、连板梯队 limitLadder)
|
||||
1. get_market_dashboard - 获取市场整体数据(含涨跌统计、市场温度、连板梯队 limitLadder、板块资金流 sectorFundFlow、两融)
|
||||
2. get_theme_history(date="{trade_date}") - 获取今日题材涨幅
|
||||
3. get_active_core_stocks - 获取核心股数据
|
||||
4. get_news(limit=30) - 获取今日财经快讯
|
||||
|
||||
生成报告时必须遵守以下格式规则:
|
||||
|
||||
1. 报告标题(# 一级标题)之后的第一行,必须是一个引用块"定调摘要",格式严格为:
|
||||
> 今日定调:<一句话核心结论,不超过80字,必须包含1-2个关键数字(如成交额、涨停家数、市场温度)>
|
||||
|
||||
2. 量能与情绪类数字必须给环比:若上方提供了"前一交易日盘面数据快照",成交额、涨跌家数、涨停数、市场温度等在与昨日对比后表述(如"成交额2.05万亿,较昨日缩量约700亿");没有昨日快照则如实说明"暂无昨日数据"。
|
||||
2. 量能与情绪类数字必须给环比:若上方提供了"前一交易日盘面数据快照",成交额、涨跌家数、涨停数、两融余额、市场温度等在与昨日对比后表述(如"成交额2.05万亿,较昨日缩量约700亿");没有昨日快照则如实说明"暂无昨日数据"。
|
||||
|
||||
3. 连板梯队必须完整呈现 get_market_dashboard 返回的 limitLadder:从最高连板到2连板逐级列表格,每只标注涨停原因(reason字段)与封单金额(sealWan,单位万,为空则不写);首板只挑3-5只人气最高的点评。
|
||||
|
||||
4. 适当使用表格展示数据对比。
|
||||
4. 板块资金面必须引用 get_market_dashboard 返回的 sectorFundFlow:行业主力净流入TOP3、净流出TOP3、概念净流入TOP3(单位亿元),结合题材分析说明资金动向。
|
||||
|
||||
5. 重要消息面必须基于 get_news 返回的快讯整理:挑5-8条对次日盘面影响最大的消息,每条格式为"【分类】一句话新闻 —— 一句影响解读"(分类用:宏观/政策/行业/公司/海外);快讯中若没有某方面的重要消息,如实说明,严禁编造工具中不存在的新闻。
|
||||
|
||||
6. 适当使用表格展示数据对比。
|
||||
|
||||
然后基于数据生成报告,结构如下:
|
||||
|
||||
@@ -74,6 +80,7 @@ DAILY_ANALYSIS_PROMPT = """请对 {trade_date} 的A股市场进行收盘分析
|
||||
- 持续活跃的题材
|
||||
- 新兴热点题材
|
||||
- 明显退潮的题材(警示)
|
||||
- 板块主力资金流(按格式规则4引用数据)
|
||||
|
||||
## 三、核心股追踪
|
||||
- 连板梯队分析(按格式规则3完整呈现)
|
||||
@@ -91,7 +98,10 @@ DAILY_ANALYSIS_PROMPT = """请对 {trade_date} 的A股市场进行收盘分析
|
||||
- 操作策略(仓位建议、买卖时机)
|
||||
- 需要规避的方向
|
||||
|
||||
## 六、风险提示
|
||||
## 六、重要消息面
|
||||
- 基于 get_news 快讯整理(按格式规则5)
|
||||
|
||||
## 七、风险提示
|
||||
- 需要警惕的风险因素
|
||||
- 操作建议
|
||||
|
||||
@@ -123,7 +133,7 @@ async def call_llm(messages: list, tools: list = None) -> dict:
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=payload,
|
||||
timeout=120,
|
||||
timeout=300, # 续写调用携带全部上下文且输出很长,需要较宽的读超时
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
@@ -156,6 +166,7 @@ async def collect_ai_analysis(trade_date: str) -> dict:
|
||||
total_tokens = 0
|
||||
final_content = ""
|
||||
was_truncated = False
|
||||
continuations = 0
|
||||
max_rounds = 30 # 安全上限,正常分析约 3-8 轮
|
||||
|
||||
for i in range(max_rounds):
|
||||
@@ -167,20 +178,27 @@ async def collect_ai_analysis(trade_date: str) -> dict:
|
||||
messages.append(message)
|
||||
|
||||
finish_reason = choice.get("finish_reason", "")
|
||||
print(f"[ai-service] round {i}: finish={finish_reason}, "
|
||||
f"content_len={len(message.get('content') or '')}, "
|
||||
f"tool_calls={len(message.get('tool_calls') or [])}")
|
||||
|
||||
if finish_reason == "stop":
|
||||
final_content = message.get("content") or ""
|
||||
final_content += message.get("content") or ""
|
||||
break
|
||||
|
||||
if finish_reason == "length":
|
||||
# 单次输出上限截断:拼接已有内容并让模型续写(最多3次)
|
||||
final_content += message.get("content") or ""
|
||||
if continuations < 3:
|
||||
continuations += 1
|
||||
print(f"[ai-service] 响应被截断,第 {continuations} 次续写")
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": "报告输出被截断了。请从截断处无缝续写剩余内容:直接接着写,不要重复已输出的部分,也不要重新输出标题。",
|
||||
})
|
||||
continue
|
||||
was_truncated = True
|
||||
final_content = message.get("content") or ""
|
||||
if not final_content:
|
||||
for msg in reversed(messages):
|
||||
if msg.get("role") == "assistant" and msg.get("content"):
|
||||
final_content = msg["content"]
|
||||
break
|
||||
print(f"[ai-service] 警告:响应被截断 (finish_reason=length)")
|
||||
print("[ai-service] 警告:多次续写后仍被截断")
|
||||
break
|
||||
|
||||
if finish_reason == "tool_calls":
|
||||
@@ -235,8 +253,21 @@ def _fmt_amount(v) -> str:
|
||||
def _fmt_index(idx: dict) -> str:
|
||||
if not idx:
|
||||
return "-"
|
||||
sign = "+" if idx.get("changePct", 0) >= 0 else ""
|
||||
return f"{idx.get('price', '-')}({sign}{idx.get('changePct', 0)}%)"
|
||||
try:
|
||||
pct = round(float(idx.get("changePct", 0)), 2)
|
||||
except (TypeError, ValueError):
|
||||
pct = 0
|
||||
sign = "+" if pct >= 0 else ""
|
||||
return f"{idx.get('price', '-')}({sign}{pct}%)"
|
||||
|
||||
|
||||
def _fmt_temperature(v) -> str:
|
||||
"""温度可能是 dict(score/label/factors),取分数与标签"""
|
||||
if isinstance(v, dict):
|
||||
score = v.get("score")
|
||||
label = v.get("label") or ""
|
||||
return f"{score}分{('(' + label + ')') if label else ''}"
|
||||
return _num(v)
|
||||
|
||||
|
||||
async def _capture_market_snapshot(trade_date: str) -> bool:
|
||||
@@ -244,8 +275,13 @@ async def _capture_market_snapshot(trade_date: str) -> bool:
|
||||
try:
|
||||
from routes.market_dashboard import _build_dashboard
|
||||
data = await _build_dashboard()
|
||||
stats = data.get("marketStats") or {}
|
||||
# 数据有效性校验:fuyao 拉取失败时涨跌统计全 0,空快照会污染次日环比
|
||||
if not data.get("indices") or (stats.get("upCount", 0) + stats.get("downCount", 0) == 0):
|
||||
print(f"[ai-service] 盘面数据无效,跳过快照入库 {trade_date}")
|
||||
return False
|
||||
payload = json.dumps(
|
||||
{"indices": data.get("indices", []), "marketStats": data.get("marketStats", {})},
|
||||
{"indices": data.get("indices", []), "marketStats": stats},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
@@ -286,12 +322,20 @@ def _get_prev_snapshot_section(trade_date: str) -> str:
|
||||
f"{name} {_fmt_index(indices.get(name))}"
|
||||
for name in ("上证指数", "深证成指", "创业板指", "科创50")
|
||||
)
|
||||
margin_line = ""
|
||||
if stats.get("marginBalanceYi"):
|
||||
change = stats.get("marginChangeYi")
|
||||
change_txt = ""
|
||||
if change is not None:
|
||||
sign = "+" if float(change) >= 0 else ""
|
||||
change_txt = f"(较前一日 {sign}{_num(change)}亿)"
|
||||
margin_line = f"\n- 两融余额:{_num(stats.get('marginBalanceYi'))}亿{change_txt},数据日期 {stats.get('marginDate') or '-'}(T+1)"
|
||||
return f"""以下是前一交易日({row["trade_date"]})的盘面数据快照,报告中的量能与情绪数字必须给出与它的环比对比:
|
||||
|
||||
- 两市成交额:{_fmt_amount(stats.get("totalTurnover"))}
|
||||
- 上涨/下跌/平盘:{_num(stats.get("upCount"))}/{_num(stats.get("downCount"))}/{_num(stats.get("flatCount"))},涨停 {_num(stats.get("limitUp"))} 家、跌停 {_num(stats.get("limitDown"))} 家、炸板 {_num(stats.get("limitBreak"))} 家(炸板率 {_num(stats.get("breakRate"))}%)
|
||||
- 强势/弱势股:{_num(stats.get("strongCount"))}/{_num(stats.get("weakCount"))},市场宽度 {_num(stats.get("marketBreadth"))}%
|
||||
- 市场温度:{_num(stats.get("temperature"))} 分,竞价信号:{stats.get("auctionSignal") or "-"}
|
||||
- 市场温度:{_fmt_temperature(stats.get("temperature"))},竞价信号:{stats.get("auctionSignal") or "-"}{margin_line}
|
||||
- 指数收盘:{idx_line}
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user