fix(assistant): 提示词数据口径改正换手率与指数涨跌幅为百分数, 不再引导模型放大 100 倍

「数据口径」写的是「工具返回的涨跌幅/换手率/振幅均为小数 (0.0366 = 3.66%)」。
CONTRIBUTING §3.1: enriched 的 turnover_rate 是百分数值, 指数实时展示缓存
(_build_index_quotes, 以及总览的 DB 兜底) 的 change_pct / amplitude 也是百分数。
模型照提示词换算会把换手率 5.2 说成 520%, 把上证指数 +1.23 说成上涨 123%。

拆成三条: change_pct / amplitude 为小数; turnover_rate 已是百分数; get_indices
与 get_market_overview 的 indices 里的 change_pct / amplitude 已是百分数。
工具返回的数据不变。

Closes #353

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kevin9327
2026-09-19 07:12:33 +09:00
co-authored by Claude Opus 5
parent 5eafc0ad0e
commit a967015d9d
2 changed files with 81 additions and 1 deletions
+4 -1
View File
@@ -30,7 +30,10 @@ get_factor_values 查因子排名; 需要验证假设时 run_backtest。
不重复查询同类信息。
数据口径:
- 工具返回的涨跌幅/换手率/振幅均为小数 (0.0366 = 3.66%), 表述时转为百分比。
- 涨跌幅 change_pct 与振幅 amplitude 为小数 (0.0366 = 3.66%), 表述时转为百分比。
- 换手率 turnover_rate 已是百分数 (5.2 = 5.2%), 直接加 % 表述, 不要再乘 100。
- 指数实时行情 (get_indices 与 get_market_overview 的 indices) 的 change_pct / amplitude \
已是百分数 (1.23 = 1.23%), 直接加 % 表述。
- 大额金额换算为「亿/万亿」表述 (如 2.09e12 元 → 2.09 万亿), 价格保留两位小数。
- 交代数据所处口径: 盘中是动态快照, 收盘口径以本地落库为准; \
今天是非交易日或数据未更新时, 明确提示数据停留在最近哪个交易日。
@@ -0,0 +1,77 @@
"""AI 助手 system prompt 的「数据口径」必须与工具实际返回的单位一致。
CONTRIBUTING §3.1: enriched 的 turnover_rate 是百分数 (5 = 5%), 指数实时展示缓存
(get_index_quotes) 的 change_pct / amplitude 也是百分数 (1.23 = 1.23%); 只有
change_pct / amplitude 在 enriched 里是小数。提示词却写「涨跌幅/换手率/振幅均为小数
(0.0366 = 3.66%), 表述时转为百分比」—— 模型照做会把换手率 5.2 说成 520%,
把上证指数 +1.23 说成「上涨 123%」(与 #285 同一类单位错位)。
"""
from __future__ import annotations
import polars as pl
from app.custom.assistant import tools as assistant_tools
from app.custom.assistant.prompt import build_system_prompt
from app.services.quote_service import QuoteService
def _unit_lines() -> list[str]:
"""提示词「数据口径」小节的条目行 (续行已由模板的反斜杠拼接)。"""
lines = build_system_prompt(None).splitlines()
start = lines.index("数据口径:") + 1
out: list[str] = []
for line in lines[start:]:
if not line.startswith("- "):
break
out.append(line)
return out
class _IndexQuoteService:
def __init__(self, df: pl.DataFrame) -> None:
self._df = df
def get_index_quotes(self, symbols=None) -> pl.DataFrame:
return self._df
async def test_index_quote_tool_returns_percent_units() -> None:
"""实际口径: get_indices 透传实时指数缓存, 涨跌幅已是百分数。"""
cache = QuoteService._build_index_quotes([{
"symbol": "000001.SH", "name": "上证指数", "last_price": 3850.0, "prev_close": 3803.23,
"change_pct": 0.0123, "amplitude": 0.0156,
}])
ctx = assistant_tools.ToolContext.build(quote_service=_IndexQuoteService(cache))
payload = await assistant_tools.execute_assistant_tool("get_indices", {}, ctx)
row = payload["result"]["rows"][0]
assert row["change_pct"] == 1.23
assert row["amplitude"] == 1.56
def test_enriched_turnover_is_percent_units() -> None:
"""实际口径: 实时入口的小数换手率在进入 enriched 前就乘了 100。"""
extra = QuoteService._build_quote_extra([{"symbol": "600519.SH", "turnover_rate": 0.052}])
assert extra["turnover_rate"][0] == 5.2
def test_prompt_does_not_declare_turnover_or_index_pct_as_decimal() -> None:
decimal_lines = [line for line in _unit_lines() if "0.0366" in line]
assert decimal_lines, "提示词应保留小数口径示例"
for line in decimal_lines:
assert "换手率" not in line
assert "turnover_rate" not in line
assert "指数" not in line
def test_prompt_declares_turnover_rate_as_percent() -> None:
lines = [line for line in _unit_lines() if "turnover_rate" in line]
assert lines
assert all("百分数" in line for line in lines)
def test_prompt_declares_index_quotes_as_percent() -> None:
lines = [line for line in _unit_lines() if "get_indices" in line]
assert lines
assert all("百分数" in line and "change_pct" in line for line in lines)
assert any("get_market_overview" in line for line in lines)