mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
feat: add analysis context data quality scoring (#1539)
This commit is contained in:
@@ -182,6 +182,7 @@ class AnalysisContextPackOverviewBlock(BaseModel):
|
||||
"stale",
|
||||
"estimated",
|
||||
"partial",
|
||||
"fetch_failed",
|
||||
] = Field(..., description="数据块质量状态")
|
||||
source: Optional[str] = Field(None, description="数据来源")
|
||||
warnings: List[str] = Field(default_factory=list, description="数据块告警码")
|
||||
@@ -198,6 +199,7 @@ class AnalysisContextPackOverviewCounts(BaseModel):
|
||||
stale: int = 0
|
||||
estimated: int = 0
|
||||
partial: int = 0
|
||||
fetch_failed: int = 0
|
||||
|
||||
|
||||
class AnalysisContextPackOverviewMetadata(BaseModel):
|
||||
@@ -207,6 +209,18 @@ class AnalysisContextPackOverviewMetadata(BaseModel):
|
||||
news_result_count: Optional[int] = Field(None, description="新闻结果数量")
|
||||
|
||||
|
||||
class AnalysisContextPackOverviewDataQuality(BaseModel):
|
||||
"""AnalysisContextPack 可见摘要数据质量评分"""
|
||||
|
||||
overall_score: Optional[int] = Field(None, ge=0, le=100, description="输入数据质量总分")
|
||||
level: Optional[Literal["good", "usable", "limited", "poor"]] = Field(
|
||||
None,
|
||||
description="输入数据质量等级",
|
||||
)
|
||||
block_scores: Dict[str, int] = Field(default_factory=dict, description="固定数据块质量分")
|
||||
limitations: List[str] = Field(default_factory=list, description="低敏数据限制说明")
|
||||
|
||||
|
||||
class AnalysisContextPackOverview(BaseModel):
|
||||
"""历史/API 可见的低敏 AnalysisContextPack 摘要"""
|
||||
|
||||
@@ -215,6 +229,10 @@ class AnalysisContextPackOverview(BaseModel):
|
||||
subject: AnalysisContextPackOverviewSubject
|
||||
blocks: List[AnalysisContextPackOverviewBlock] = Field(default_factory=list)
|
||||
counts: AnalysisContextPackOverviewCounts
|
||||
data_quality: Optional[AnalysisContextPackOverviewDataQuality] = Field(
|
||||
None,
|
||||
description="本次分析输入数据质量低敏摘要",
|
||||
)
|
||||
warnings: List[str] = Field(default_factory=list, description="顶层数据质量提醒")
|
||||
metadata: AnalysisContextPackOverviewMetadata = Field(default_factory=AnalysisContextPackOverviewMetadata)
|
||||
|
||||
|
||||
@@ -25,6 +25,33 @@ const STATUS_STYLE: Record<AnalysisContextPackBlockStatus, { variant: BadgeVaria
|
||||
stale: { variant: 'warning', tone: 'warning' },
|
||||
estimated: { variant: 'info', tone: 'info' },
|
||||
partial: { variant: 'warning', tone: 'warning' },
|
||||
fetch_failed: { variant: 'danger', tone: 'danger' },
|
||||
};
|
||||
|
||||
const QUALITY_STYLE = {
|
||||
good: { variant: 'success', tone: 'success' },
|
||||
usable: { variant: 'info', tone: 'info' },
|
||||
limited: { variant: 'warning', tone: 'warning' },
|
||||
poor: { variant: 'danger', tone: 'danger' },
|
||||
} as const satisfies Record<string, { variant: BadgeVariant; tone: StatusTone }>;
|
||||
|
||||
const BLOCK_LABELS: Record<ReportLanguage, Record<string, string>> = {
|
||||
zh: {
|
||||
quote: '行情',
|
||||
daily_bars: '日线',
|
||||
technical: '技术',
|
||||
news: '新闻',
|
||||
fundamentals: '基本面',
|
||||
chip: '筹码',
|
||||
},
|
||||
en: {
|
||||
quote: 'quote',
|
||||
daily_bars: 'daily bars',
|
||||
technical: 'technical',
|
||||
news: 'news',
|
||||
fundamentals: 'fundamentals',
|
||||
chip: 'chip',
|
||||
},
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
@@ -35,8 +62,16 @@ const TEXT = {
|
||||
source: '来源',
|
||||
warnings: '告警',
|
||||
missingReasons: '缺失原因',
|
||||
qualityScore: '质量分',
|
||||
limitations: '数据限制',
|
||||
newsResultCount: '新闻结果数',
|
||||
triggerSource: '触发来源',
|
||||
qualityLevel: {
|
||||
good: '良好',
|
||||
usable: '可用',
|
||||
limited: '受限',
|
||||
poor: '较差',
|
||||
},
|
||||
status: {
|
||||
available: '可用',
|
||||
missing: '缺失',
|
||||
@@ -45,6 +80,7 @@ const TEXT = {
|
||||
stale: '过期',
|
||||
estimated: '估算',
|
||||
partial: '部分可用',
|
||||
fetch_failed: '抓取失败',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
@@ -54,8 +90,16 @@ const TEXT = {
|
||||
source: 'Source',
|
||||
warnings: 'Warnings',
|
||||
missingReasons: 'Missing Reasons',
|
||||
qualityScore: 'Quality',
|
||||
limitations: 'Data Limitations',
|
||||
newsResultCount: 'News Results',
|
||||
triggerSource: 'Trigger',
|
||||
qualityLevel: {
|
||||
good: 'Good',
|
||||
usable: 'Usable',
|
||||
limited: 'Limited',
|
||||
poor: 'Poor',
|
||||
},
|
||||
status: {
|
||||
available: 'Available',
|
||||
missing: 'Missing',
|
||||
@@ -64,6 +108,7 @@ const TEXT = {
|
||||
stale: 'Stale',
|
||||
estimated: 'Estimated',
|
||||
partial: 'Partial',
|
||||
fetch_failed: 'Fetch failed',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -71,6 +116,7 @@ const TEXT = {
|
||||
const STATUS_ORDER: AnalysisContextPackBlockStatus[] = [
|
||||
'available',
|
||||
'missing',
|
||||
'fetch_failed',
|
||||
'not_supported',
|
||||
'fallback',
|
||||
'stale',
|
||||
@@ -85,9 +131,33 @@ const getCount = (
|
||||
if (status === 'not_supported') {
|
||||
return overview.counts.notSupported || 0;
|
||||
}
|
||||
if (status === 'fetch_failed') {
|
||||
return overview.counts.fetchFailed || 0;
|
||||
}
|
||||
return overview.counts[status] || 0;
|
||||
};
|
||||
|
||||
const formatLimitation = (
|
||||
value: string,
|
||||
language: ReportLanguage,
|
||||
text: typeof TEXT.zh | typeof TEXT.en,
|
||||
): string => {
|
||||
const [rawKey, ...statusParts] = value.split(':');
|
||||
if (!rawKey || statusParts.length === 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const key = rawKey.trim();
|
||||
const status = statusParts.join(':').trim();
|
||||
if (!key || !status) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const label = BLOCK_LABELS[language][key] || key;
|
||||
const statusLabel = (text.status as Record<string, string>)[status] || status;
|
||||
return language === 'zh' ? `${label}:${statusLabel}` : `${label}: ${statusLabel}`;
|
||||
};
|
||||
|
||||
export const AnalysisContextSummary: React.FC<AnalysisContextSummaryProps> = ({
|
||||
overview,
|
||||
language = 'zh',
|
||||
@@ -111,6 +181,11 @@ export const AnalysisContextSummary: React.FC<AnalysisContextSummaryProps> = ({
|
||||
: null,
|
||||
].filter((item): item is string => Boolean(item));
|
||||
const triggerSource = overview.metadata?.triggerSource?.trim();
|
||||
const quality = overview.dataQuality;
|
||||
const qualityLevel = quality?.level || undefined;
|
||||
const qualityStyle = qualityLevel ? QUALITY_STYLE[qualityLevel] : undefined;
|
||||
const qualityLabel = qualityLevel ? text.qualityLevel[qualityLevel] : undefined;
|
||||
const limitations = quality?.limitations?.map((item) => formatLimitation(item, reportLanguage, text)) || [];
|
||||
|
||||
return (
|
||||
<Card variant="bordered" padding="none" className="home-panel-card">
|
||||
@@ -128,6 +203,12 @@ export const AnalysisContextSummary: React.FC<AnalysisContextSummaryProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex min-w-0 flex-wrap items-center justify-end gap-2">
|
||||
{typeof quality?.overallScore === 'number' ? (
|
||||
<Badge variant={qualityStyle?.variant || 'default'} className="gap-1.5 shadow-none">
|
||||
{qualityStyle ? <StatusDot tone={qualityStyle.tone} className="h-1.5 w-1.5" /> : null}
|
||||
{text.qualityScore} {quality.overallScore}/100{qualityLabel ? ` ${qualityLabel}` : ''}
|
||||
</Badge>
|
||||
) : null}
|
||||
{summaryCounts.map(({ status, value }) => {
|
||||
const style = STATUS_STYLE[status];
|
||||
return (
|
||||
@@ -155,8 +236,13 @@ export const AnalysisContextSummary: React.FC<AnalysisContextSummaryProps> = ({
|
||||
<Database className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
actions={metadataItems.length > 0 ? (
|
||||
actions={metadataItems.length > 0 || typeof quality?.overallScore === 'number' ? (
|
||||
<div className="hidden flex-wrap justify-end gap-2 text-xs text-muted-text md:flex">
|
||||
{typeof quality?.overallScore === 'number' ? (
|
||||
<span className="home-accent-chip px-2 py-0.5">
|
||||
{text.qualityScore}: {quality.overallScore}/100{qualityLabel ? ` ${qualityLabel}` : ''}
|
||||
</span>
|
||||
) : null}
|
||||
{metadataItems.map((item) => (
|
||||
<span key={item} className="home-accent-chip px-2 py-0.5">
|
||||
{item}
|
||||
@@ -181,6 +267,13 @@ export const AnalysisContextSummary: React.FC<AnalysisContextSummaryProps> = ({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{limitations.length ? (
|
||||
<div className="mb-3 home-subpanel p-3 text-xs leading-5 text-muted-text">
|
||||
<span className="font-medium text-foreground">{text.limitations}: </span>
|
||||
{limitations.join(', ')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{overview.warnings?.length ? (
|
||||
<div className="mb-3 home-subpanel p-3 text-xs leading-5 text-warning">
|
||||
<span className="font-medium">{text.warnings}: </span>
|
||||
@@ -223,8 +316,13 @@ export const AnalysisContextSummary: React.FC<AnalysisContextSummaryProps> = ({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{metadataItems.length > 0 ? (
|
||||
{metadataItems.length > 0 || typeof quality?.overallScore === 'number' ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-muted-text md:hidden">
|
||||
{typeof quality?.overallScore === 'number' ? (
|
||||
<span className="home-accent-chip px-2 py-0.5">
|
||||
{text.qualityScore}: {quality.overallScore}/100{qualityLabel ? ` ${qualityLabel}` : ''}
|
||||
</span>
|
||||
) : null}
|
||||
{metadataItems.map((item) => (
|
||||
<span key={item} className="home-accent-chip px-2 py-0.5">
|
||||
{item}
|
||||
|
||||
@@ -37,6 +37,14 @@ const overview: AnalysisContextPackOverview = {
|
||||
warnings: ['news_provider_timeout'],
|
||||
missingReasons: ['news_context_missing'],
|
||||
},
|
||||
{
|
||||
key: 'fundamentals',
|
||||
label: '基本面',
|
||||
status: 'fetch_failed',
|
||||
source: 'fundamental_pipeline',
|
||||
warnings: [],
|
||||
missingReasons: ['fundamental_pipeline_failed'],
|
||||
},
|
||||
],
|
||||
counts: {
|
||||
available: 1,
|
||||
@@ -46,6 +54,20 @@ const overview: AnalysisContextPackOverview = {
|
||||
stale: 0,
|
||||
estimated: 0,
|
||||
partial: 0,
|
||||
fetchFailed: 1,
|
||||
},
|
||||
dataQuality: {
|
||||
overallScore: 82,
|
||||
level: 'usable',
|
||||
blockScores: {
|
||||
quote: 100,
|
||||
daily_bars: 100,
|
||||
technical: 100,
|
||||
news: 35,
|
||||
fundamentals: 25,
|
||||
chip: 100,
|
||||
},
|
||||
limitations: ['fundamentals: fetch_failed'],
|
||||
},
|
||||
warnings: ['intraday_realtime_overlay'],
|
||||
metadata: {
|
||||
@@ -67,6 +89,8 @@ describe('AnalysisContextSummary', () => {
|
||||
expect(within(panel).getAllByText('输入数据块')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('可用 1')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('缺失 1')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('抓取失败 1')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('质量分 82/100 可用')[0]).toBeVisible();
|
||||
expect(screen.getByText('触发来源: api')).toBeVisible();
|
||||
expect(screen.getByText('来源: mock_quote')).not.toBeVisible();
|
||||
|
||||
@@ -77,8 +101,11 @@ describe('AnalysisContextSummary', () => {
|
||||
expect(screen.getByText('来源: mock_quote')).toBeVisible();
|
||||
expect(screen.getByText('告警:')).toBeInTheDocument();
|
||||
expect(screen.getByText(/intraday_realtime_overlay/)).toBeInTheDocument();
|
||||
expect(screen.getByText('数据限制:')).toBeInTheDocument();
|
||||
expect(screen.getByText(/基本面:抓取失败/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/news_provider_timeout/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/news_context_missing/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/fundamental_pipeline_failed/)).toBeInTheDocument();
|
||||
expect(screen.getAllByText('新闻结果数: 3').some((item) => item.textContent === '新闻结果数: 3')).toBe(true);
|
||||
});
|
||||
|
||||
@@ -90,7 +117,14 @@ describe('AnalysisContextSummary', () => {
|
||||
expect(screen.getAllByText('Input Blocks')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('Available 1')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('Missing 1')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('Fetch failed 1')[0]).toBeVisible();
|
||||
expect(screen.getAllByText('Quality 82/100 Usable')[0]).toBeVisible();
|
||||
expect(screen.getByText('Trigger: api')).toBeVisible();
|
||||
|
||||
fireEvent.click(within(panel).getAllByText('Input Blocks')[0]);
|
||||
|
||||
expect(screen.getByText('Data Limitations:')).toBeInTheDocument();
|
||||
expect(screen.getByText(/fundamentals: Fetch failed/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces degraded non-zero states in the collapsed summary', () => {
|
||||
@@ -122,6 +156,7 @@ describe('AnalysisContextSummary', () => {
|
||||
stale: 1,
|
||||
estimated: 0,
|
||||
partial: 0,
|
||||
fetchFailed: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -130,7 +130,8 @@ export type AnalysisContextPackBlockStatus =
|
||||
| 'fallback'
|
||||
| 'stale'
|
||||
| 'estimated'
|
||||
| 'partial';
|
||||
| 'partial'
|
||||
| 'fetch_failed';
|
||||
|
||||
export interface AnalysisContextPackOverviewSubject {
|
||||
code: string;
|
||||
@@ -155,6 +156,7 @@ export interface AnalysisContextPackOverviewCounts {
|
||||
stale: number;
|
||||
estimated: number;
|
||||
partial: number;
|
||||
fetchFailed: number;
|
||||
}
|
||||
|
||||
export interface AnalysisContextPackOverviewMetadata {
|
||||
@@ -162,12 +164,22 @@ export interface AnalysisContextPackOverviewMetadata {
|
||||
newsResultCount?: number | null;
|
||||
}
|
||||
|
||||
export type AnalysisContextPackDataQualityLevel = 'good' | 'usable' | 'limited' | 'poor';
|
||||
|
||||
export interface AnalysisContextPackOverviewDataQuality {
|
||||
overallScore?: number | null;
|
||||
level?: AnalysisContextPackDataQualityLevel | null;
|
||||
blockScores: Record<string, number>;
|
||||
limitations: string[];
|
||||
}
|
||||
|
||||
export interface AnalysisContextPackOverview {
|
||||
packVersion: string;
|
||||
createdAt?: string | null;
|
||||
subject: AnalysisContextPackOverviewSubject;
|
||||
blocks: AnalysisContextPackOverviewBlock[];
|
||||
counts: AnalysisContextPackOverviewCounts;
|
||||
dataQuality?: AnalysisContextPackOverviewDataQuality | null;
|
||||
warnings: string[];
|
||||
metadata: AnalysisContextPackOverviewMetadata;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
- [新功能] Web 报告页新增同股历史趋势抽屉入口,历史列表摘要补充趋势、摘要、模型和分析时行情字段,支持按当前股票查看历史分析并加载更多。
|
||||
- [新功能] AnalysisContextPack P4 低敏 overview 接入历史详情、同步分析响应、completed 任务状态和 Web 报告页,展示数据块状态、来源、缺失原因与降级摘要。
|
||||
- [改进] AnalysisContextPack P5 增加数据质量评分、`fetch_failed` 状态、Prompt 数据限制区块和 Web 低敏质量展示。
|
||||
- [文档] 明确同股历史趋势新增模型字段为历史快照展示元数据,不影响运行时 LLM Provider/Model/Base URL 路由与配置迁移清理;回退方式为按常规发布回滚本变更。
|
||||
|
||||
- [修复] 收口 Web 中文界面残留英文文案与设置页 help 缺口,回测页改为中文展示,并让 Web 设置页仅展示已注册且带说明的配置项。
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
| [Bot 命令与接入](bot-command.md) | Bot 命令、Webhook、平台接入和回调说明 |
|
||||
| [Bot 平台配置](bot/) | 飞书、钉钉、Discord 等 Bot 配置截图和补充说明 |
|
||||
| [实时告警中心](alerts.md) | EventMonitor 基线、Web 规则管理、通知结果、冷却状态和 Phase 边界 |
|
||||
| [分析上下文包契约、运行态消费与可见性](analysis-context-pack.md) | AnalysisContextPack 首版范围、字段质量状态、P1/P2 内部契约、P3 Prompt 摘要消费、P4 历史/API/Web 低敏可见性和源码锚点 |
|
||||
| [分析上下文包契约、运行态消费与可见性](analysis-context-pack.md) | AnalysisContextPack 首版范围、字段质量状态、P1/P2 内部契约、P3 Prompt 摘要消费、P4 历史/API/Web 低敏可见性、P5 数据质量评分与源码锚点 |
|
||||
| [图片识别 Prompt](image-extract-prompt.md) | 图片识别股票信息的 Prompt 与使用边界 |
|
||||
| [OpenClaw Skill 集成](openclaw-skill-integration.md) | OpenClaw / Skill 外部集成说明 |
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ This is the entry point for project documentation. The README covers the project
|
||||
| [Bot Commands (EN)](bot-command_EN.md) | Bot commands, webhooks, platform integration, and callback behavior |
|
||||
| [Bot Platform Docs](bot/) <sub><sub></sub></sub> (Chinese-only) | Feishu, DingTalk, Discord, and related Bot configuration screenshots and notes |
|
||||
| [Real-Time Alert Center](alerts.md) <sub><sub></sub></sub> (Chinese-only) | EventMonitor baseline, Web rule management, notification attempts, cooldown state, and phase boundaries |
|
||||
| [Analysis Context Pack Contract, Runtime Consumption, And Visibility](analysis-context-pack.md) <sub><sub></sub></sub> (Chinese-only) | AnalysisContextPack first-scope boundaries, field quality states, P1/P2 internal contracts, P3 prompt-summary consumption, P4 history/API/Web low-sensitivity visibility, and source anchors |
|
||||
| [Analysis Context Pack Contract, Runtime Consumption, And Visibility](analysis-context-pack.md) <sub><sub></sub></sub> (Chinese-only) | AnalysisContextPack first-scope boundaries, field quality states, P1/P2 internal contracts, P3 prompt-summary consumption, P4 history/API/Web low-sensitivity visibility, P5 data-quality scoring, and source anchors |
|
||||
| [Image Extraction Prompt](image-extract-prompt.md) <sub><sub></sub></sub> (Chinese-only) | Prompt and boundaries for extracting stock information from images |
|
||||
| [OpenClaw Skill Integration](openclaw-skill-integration.md) <sub><sub></sub></sub> (Chinese-only) | OpenClaw / Skill external integration notes |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AnalysisContextPack:P0 盘点、P1/P2 契约、P3 Runtime Consumption 与 P4 可见性
|
||||
# AnalysisContextPack:P0 盘点、P1/P2 契约、P3 Runtime Consumption、P4 可见性与 P5 数据质量
|
||||
|
||||
本页是 Issue #1389 的专题文档,用于记录当前 DSA 分析上下文的真实来源、消费路径、字段状态边界,以及 `AnalysisContextPack` 内部契约、builder、运行态消费与低敏可见性边界。P0 负责现状盘点和契约边界;P1 只新增内部 schema/envelope、block catalog、类型约定和脱敏序列化;P2 只从 pipeline 已有 artifacts 组装 pack;P3 只把低敏摘要接入普通分析和 Agent 初始 Prompt;P4 只把低敏 overview 接入历史详情、同步分析响应、completed task status 和 Web 报告页。
|
||||
本页是 Issue #1389 的专题文档,用于记录当前 DSA 分析上下文的真实来源、消费路径、字段状态边界,以及 `AnalysisContextPack` 内部契约、builder、运行态消费、低敏可见性和数据质量评分边界。P0 负责现状盘点和契约边界;P1 只新增内部 schema/envelope、block catalog、类型约定和脱敏序列化;P2 只从 pipeline 已有 artifacts 组装 pack;P3 只把低敏摘要接入普通分析和 Agent 初始 Prompt;P4 只把低敏 overview 接入历史详情、同步分析响应、completed task status 和 Web 报告页;P5 在同一 `PACK_VERSION = "1.0"` 内补齐数据质量评分、`fetch_failed` 状态、Prompt 数据限制和 overview 低敏展示。
|
||||
|
||||
## 术语与边界
|
||||
|
||||
@@ -23,7 +23,7 @@ P0 的目标是让后续 P1/P2/P3 可以基于真实仓库边界设计 `Analysis
|
||||
- P0 不新增 builder,不新增配置项,不新增数据库字段,不改变 API、报告、历史或通知 payload。
|
||||
- P0 不接入 runtime,不改 `src/` 分析、Agent、告警、持仓、回测或通知逻辑。
|
||||
- P0 不 pack 化 `market_review`、`market_light` 或大盘红绿灯专题快照;这些只作为历史快照中的其他 `report_kind` / 专题消费边界记录。
|
||||
- P0 不把 `fetch_failed` 加入字段质量状态词;`fetch_failed` 与 `not_supported` 的细分留到 P5 数据质量评分与模型提示阶段。
|
||||
- P0 当时不把 `fetch_failed` 加入字段质量状态词;P5 已在同一 1.0 umbrella 内追加该状态,用于明确区分“不支持”和“本次抓取失败”。
|
||||
- P0 不在 README 扩写实现细节;本页作为专题文档,由 `docs/INDEX.md` / `docs/INDEX_EN.md` 入口发现。
|
||||
|
||||
## P1 内部契约
|
||||
@@ -33,11 +33,11 @@ P1 落地 `src/schemas/analysis_context_pack.py`,只定义内部 schema/envelo
|
||||
P1 schema 包含:
|
||||
|
||||
- `PACK_VERSION = "1.0"`,并通过 `AnalysisContextPack.pack_version` 标记契约版本。
|
||||
- `ContextFieldStatus`:只允许 `available`、`missing`、`not_supported`、`fallback`、`stale`、`estimated`、`partial`;`fetch_failed` 仍留到 P5。
|
||||
- `ContextFieldStatus`:P1 首版只允许 `available`、`missing`、`not_supported`、`fallback`、`stale`、`estimated`、`partial`;P5 已追加 `fetch_failed`,表示字段或数据块本次抓取明确失败,不代表整次分析失败。
|
||||
- `AnalysisSubject`:顶层身份槽,只包含 `code`、`stock_name`、`market`;`exchange`、`currency`、`industry` 留给后续扩展,P2 builder 不扩 P1 schema,也不重复新增 `identity` block。
|
||||
- `AnalysisContextItem`:字段级输入项,包含 `status`、`value`、`source`、`timestamp`、`fallback_from`、`missing_reason`、`warnings`、`metadata`。
|
||||
- `AnalysisContextBlock`:数据块级分组,包含 `status`、`items`、`source`、`timestamp`、`warnings`、`metadata`,其中 `items` 是 `Dict[str, AnalysisContextItem]`。
|
||||
- `DataQuality`:P1 只保留 `warnings` 与 `metadata` 容器,不做评分、聚合计数或模型置信度限制。
|
||||
- `DataQuality`:P1 只保留 `warnings` 与 `metadata` 容器;P5 已追加 `overall_score`、`level`、`block_scores`、`limitations`,仍保持低敏,不承载 raw payload。
|
||||
- `AnalysisContextPack`:顶层 envelope,包含 `pack_version`、`subject`、`phase`、`blocks`、`data_quality`、`metadata`、`created_at`。
|
||||
|
||||
时间字段约定:
|
||||
@@ -88,24 +88,24 @@ P2 block 组装边界:
|
||||
- `enhanced_context.today` 上的 `is_partial_bar`、`is_estimated`、`estimated_fields` 优先进入 `technical`;缺失时仍兼容 `enhanced_context.today.data_source` 为 `realtime:*` 的旧 heuristic。partial/estimated 只进入 `technical`,`daily_bars` 不承载 partial/estimated,warning 使用 `intraday_realtime_overlay`。
|
||||
- `technical` 优先复用 `trend_result.to_dict()`;无 trend artifact 时为 `missing`。
|
||||
- `chip` 复用 `chip_data.to_dict()`;无 chip artifact 默认 `missing`,只有输入 metadata/artifact 明确 not_supported 时才标 `not_supported`。
|
||||
- `fundamentals` 只读 `fundamental_context` 参数;`ok` 映射为 `available`,`not_supported` 映射为 `not_supported`,`partial` 映射为 `partial`,`failed` 映射为 `missing` + 稳定 reason code;不写入 `errors[]` 原文。
|
||||
- `fundamentals` 只读 `fundamental_context` 参数;`ok` 映射为 `available`,`not_supported` 映射为 `not_supported`,`partial` 映射为 `partial`,P5 后 `failed` 映射为 `fetch_failed` + 稳定 reason code `fundamental_pipeline_failed`;不写入 `errors[]` 原文。
|
||||
- `news` 非空白字符串为 `available`,空白或缺失为 `missing`;`news_result_count` 写入 pack metadata。
|
||||
|
||||
P2 不组装 `portfolio`、`events`、`market_context`,也不把 `capital_flow` 拆成独立 block;首版只把它保留在 fundamentals 的 coverage/source chain metadata 中。P2 也不改变 Prompt、不让普通分析或 Agent runtime 消费 pack、不写入 history/task/report metadata、不暴露完整 pack 到 API/Web/Bot/Desktop/通知,不做 P5 data-quality scoring、`fetch_failed` 细分或模型置信度限制。
|
||||
P2 不组装 `portfolio`、`events`、`market_context`,也不把 `capital_flow` 拆成独立 block;首版只把它保留在 fundamentals 的 coverage/source chain metadata 中。P2 当时也不改变 Prompt、不让普通分析或 Agent runtime 消费 pack、不写入 history/task/report metadata、不暴露完整 pack 到 API/Web/Bot/Desktop/通知;P5 只在现有 builder 上追加低敏评分、`fetch_failed` 细分和 Prompt 限制,不新增 fetcher。
|
||||
|
||||
## P3 Runtime Consumption
|
||||
|
||||
P3 在 P2 `AnalysisContextBuilder` 之后接入运行态消费,但消费面限定为低敏 `analysis_context_pack_summary`。`StockAnalysisPipeline` 是 summary 的唯一生产者:在普通分析路径和 Agent 路径内完成 `PipelineAnalysisArtifacts` -> `AnalysisContextBuilder.build()` -> `format_analysis_context_pack_prompt_section()`,下游 analyzer、single-agent、multi-agent 只接收 summary 字符串,不自行构造完整 pack,也不读取 `AnalysisContextPack.to_safe_dict()` 的 block item 原始值。
|
||||
|
||||
普通分析 Prompt 的顺序固定为:基础信息 -> #1386 `market_phase_context` 渲染区块 -> `analysis_context_pack_summary` -> 技术面、实时行情、新闻等既有区块。`analysis_context_pack_summary` 只包含 subject、`pack_version`、block `status` / `source` / `warnings` / `missing_reason`、`metadata.news_result_count` 和 `data_quality.warnings`,不得输出 `news.content`、`trend_result`、`chip`、`fundamental_context` 等原始 payload。
|
||||
普通分析 Prompt 的顺序固定为:基础信息 -> #1386 `market_phase_context` 渲染区块 -> `analysis_context_pack_summary` -> 技术面、实时行情、新闻等既有区块。`analysis_context_pack_summary` 只包含 subject、`pack_version`、block `status` / `source` / `warnings` / `missing_reason`、`metadata.news_result_count`、`data_quality.warnings` 和 P5 低敏数据限制,不得输出 `news.content`、`trend_result`、`chip`、`fundamental_context` 等原始 payload。
|
||||
|
||||
Agent 路径同样只传 summary。`AgentExecutor._build_user_message()` 在 market phase 段之后、pre-fetched JSON 之前插入 summary;`AgentOrchestrator._build_context()` 只把 summary 放入 `ctx.meta["analysis_context_pack_summary"]`,禁止写入 `ctx.data`;`BaseAgent._build_messages()` 在 market phase user message 之后、`_inject_cached_data()` 之前插入 summary。Agent 首轮没有复用普通分析新闻检索,`news` block 为 `missing` 是当前 P3 的预期状态。
|
||||
|
||||
P3 当时不持久化完整 pack,不新增 API/Web/Bot/Desktop 字段,不改变报告 JSON schema,不把 summary 写入 `analysis_history.context_snapshot`、task status 或 report metadata;history snapshot 和 diagnostic snapshot 会剥离 `market_phase_context`、`analysis_context_pack`、`analysis_context_pack_summary` 等 runtime prompt key。P4 在此基础上新增低敏 overview,可见性只覆盖历史详情、同步分析响应、completed task status 和 Web 报告页;Agent 工具级 pack cache 复用、通知展示和数据质量评分仍留给后续阶段。
|
||||
P3 当时不持久化完整 pack,不新增 API/Web/Bot/Desktop 字段,不改变报告 JSON schema,不把 summary 写入 `analysis_history.context_snapshot`、task status 或 report metadata;history snapshot 和 diagnostic snapshot 会剥离 `market_phase_context`、`analysis_context_pack`、`analysis_context_pack_summary` 等 runtime prompt key。P4 在此基础上新增低敏 overview,可见性只覆盖历史详情、同步分析响应、completed task status 和 Web 报告页;P5 继续复用 summary 消费路径,不改 LLM 输出 JSON schema。Agent 工具级 pack cache 复用仍是后续工作。
|
||||
|
||||
## P4 历史记录、任务状态与 Web 可见性
|
||||
|
||||
P4 把 P3 已构建的 `AnalysisContextPack` 投影为公共低敏 `analysis_context_pack_overview`。该 overview 由专用 renderer 生成,公共 API 不允许直接返回 `AnalysisContextPack.to_safe_dict()` 或完整 pack dump。renderer 只输出白名单字段:`pack_version`、`created_at`、`subject.code` / `stock_name` / `market`、数据块 `key` / `label` / `status` / `source` / `warnings` / `missing_reasons`、按 block status 计数的 `counts`、顶层 `data_quality.warnings` 和 `metadata.trigger_source` / `metadata.news_result_count`。
|
||||
P4 把 P3 已构建的 `AnalysisContextPack` 投影为公共低敏 `analysis_context_pack_overview`。该 overview 由专用 renderer 生成,公共 API 不允许直接返回 `AnalysisContextPack.to_safe_dict()` 或完整 pack dump。renderer 只输出白名单字段:`pack_version`、`created_at`、`subject.code` / `stock_name` / `market`、数据块 `key` / `label` / `status` / `source` / `warnings` / `missing_reasons`、按 block status 计数的 `counts`、顶层 `data_quality.warnings` 和 `metadata.trigger_source` / `metadata.news_result_count`。P5 在同一 overview 上追加 `data_quality` 低敏对象,不重复顶层 `warnings`。
|
||||
|
||||
overview 不输出 `blocks.*.items`、`items.value`、`news.content`、`trend_result`、`chip`、`fundamental_context` 原始 payload,也不输出 `api_key`、`token`、`cookie`、`webhook_url`、`password`、`secret`、`authorization`、`sendkey`、`license_key` 等敏感键或值。
|
||||
|
||||
@@ -119,11 +119,32 @@ P4 持久化面只在 `analysis_history.context_snapshot` 顶层写入 `analysis
|
||||
|
||||
API 返回给 Web 的 `details.context_snapshot` 会通过 `sanitize_context_snapshot_for_api()` 剥离顶层 `analysis_context_pack_overview`,避免 raw snapshot 面板重复展示或被当作完整上下文导出;overview 只从 `extract_analysis_context_pack_overview()` 单独取出。Agent 路径与普通分析路径写入同一 overview 形状,Agent 无新闻计数时 `metadata.news_result_count` 可为空。
|
||||
|
||||
P4 Web 展示只在报告详情页渲染 `AnalysisContextSummary`,位置在策略点位和资讯之后、运行诊断之前;该区域默认折叠,折叠头部展示可用数、缺失数、非零的其他状态计数和触发来源,展开后展示数据块状态 badge、来源、warning、missing reason、状态计数和新闻结果数。无 overview 时不渲染占位。P4 不覆盖 pending/processing TaskPanel 或 SSE 进行中可见性,不改通知摘要、Bot/Desktop 专属展示、`market_review` overview、P5 数据质量评分或 `fetch_failed` 细分。
|
||||
P4 Web 展示只在报告详情页渲染 `AnalysisContextSummary`,位置在策略点位和资讯之后、运行诊断之前;该区域默认折叠,折叠头部展示可用数、缺失数、非零的其他状态计数和触发来源,展开后展示数据块状态 badge、来源、warning、missing reason、状态计数和新闻结果数。P5 后折叠头部还会展示质量分/等级,展开后展示 `limitations` 和 `fetch_failed` 状态。无 overview 时不渲染占位。P4/P5 不覆盖 pending/processing TaskPanel 或 SSE 进行中可见性,不改通知摘要、Bot/Desktop 专属展示或 `market_review` overview。
|
||||
|
||||
## P5 数据质量评分与 Prompt 数据限制
|
||||
|
||||
P5 在不升级 `PACK_VERSION`、不新增 fetcher、不新增配置项、不做历史迁移的前提下补齐三件事:内部低敏数据质量评分、跨模型通用的 Prompt 数据限制区块,以及既有 `analysis_context_pack_overview` 的低敏可见性扩展。P5 不改变 LLM 输出 JSON schema,不做后处理强制改写,也不纳入 #1386 的盘中动作字段。
|
||||
|
||||
状态契约新增 `fetch_failed`,用于“当前字段或数据块本次抓取明确失败”。首版只在已有 artifact 明确失败时使用,例如 `fundamental_context.status == "failed"`;空新闻、未配置搜索、无实时 quote artifact 或 chip 缺失仍保持既有 `missing` / `not_supported` 语义,避免把未启用能力误报成抓取失败。`fetch_failed` 不代表整次分析失败。
|
||||
|
||||
`DataQuality` 追加以下低敏字段,并保留旧 `warnings` / `metadata`:
|
||||
|
||||
- `overall_score: Optional[int]`:0-100 总分。
|
||||
- `level: Optional["good"|"usable"|"limited"|"poor"]`:`>=85 good`、`>=70 usable`、`>=55 limited`,否则 `poor`。
|
||||
- `block_scores: Dict[str, int]`:固定六块的状态分。
|
||||
- `limitations: List[str]`:最多 5 条稳定限制说明,使用 `block: status` 形式。
|
||||
|
||||
评分只计算固定六块,不随辅助块缺失重归一化,未来新增 block 不自动影响总分。权重固定为 `quote=25`、`daily_bars=25`、`technical=25`、`news=10`、`fundamentals=10`、`chip=5`;状态分固定为 `available=100`、`partial=75`、`estimated=75`、`not_supported=70`、`fallback=65`、`stale=50`、`missing=35`、`fetch_failed=25`。总分公式为 `round(sum(block_score * weight) / 100)`。
|
||||
|
||||
`limitations` 优先列出核心块 `quote` / `daily_bars` / `technical` 的 `stale`、`fallback`、`missing`、`fetch_failed`、`partial`、`estimated`;其次列出辅助块 `news` / `fundamentals` / `chip` 的 `fetch_failed`、`fallback`、`stale`。辅助块单纯缺失不进入限制列表,避免把新闻缺失、未配置搜索或不支持能力解释成利好/利空。
|
||||
|
||||
Prompt 数据限制只在 `format_analysis_context_pack_prompt_section()` 内渲染,紧跟 pack summary,因此普通分析、single Agent 和 multi-agent 复用同一消费路径。中文输出 `数据限制`,英文输出 `Data Limitations`;只有真实 score 存在时才输出评分行。若 `quote`、`daily_bars` 或 `technical` 为 degraded 状态,Prompt 明确要求最终 JSON 的 `confidence_level` 不得为 `高` / `High`。Prompt 继续只使用 status/source/warnings/missing_reason/低敏评分,不输出 raw payload、新闻正文、趋势原始值、secret、token 或 webhook。
|
||||
|
||||
overview 只扩展现有公开面:`analysis_context_pack_overview.data_quality` 白名单包含 `overall_score`、`level`、`block_scores`、`limitations`,不重复公开 `warnings`。`render_analysis_context_pack_overview()` 与 `extract_analysis_context_pack_overview()` / persisted sanitizer 都会清洗该对象;旧 overview 缺少 `data_quality` 时仍正常读取。`details.context_snapshot` 继续剥离顶层 `analysis_context_pack_overview`,不公开完整 pack。
|
||||
|
||||
## 字段质量状态
|
||||
|
||||
未来 pack 的字段质量状态在 P0 只固定下列七词。它们描述字段或数据块的质量,不描述业务流程是否成功。
|
||||
未来 pack 的字段质量状态在 P0 先固定七词;P5 在同一 1.0 umbrella 内追加 `fetch_failed`。它们描述字段或数据块的质量,不描述业务流程是否成功。
|
||||
|
||||
| 状态 | 含义 | 示例边界 |
|
||||
| --- | --- | --- |
|
||||
@@ -134,6 +155,7 @@ P4 Web 展示只在报告详情页渲染 `AnalysisContextSummary`,位置在策
|
||||
| `stale` | 字段存在,但时间新鲜度不足。 | 持仓估值中的 `price_stale` / `fx_stale`。 |
|
||||
| `estimated` | 字段是估算值,不应当作完整事实。 | 盘中用实时价补今日 bar 后生成技术估计。 |
|
||||
| `partial` | 数据块部分可用、部分缺失。 | 大盘红绿灯 `data_quality=partial` 或工具返回 `partial_cache`。 |
|
||||
| `fetch_failed` | 当前路径确认尝试过抓取,但本次抓取失败。 | `fundamental_context.status == "failed"` 映射为基本面 block 抓取失败。 |
|
||||
|
||||
## 现有状态映射
|
||||
|
||||
@@ -150,7 +172,7 @@ P4 Web 展示只在报告详情页渲染 `AnalysisContextSummary`,位置在策
|
||||
| `insufficient_data` / `completed` / `error` | 回测服务 | 不映射 | 这是回测执行状态;可在 pack 摘要中解释触发原因。 |
|
||||
| `sent` / `no_channel` / `partial_failed` / `all_failed` | 通知发送 | 不映射 | 这是通知投递结果,不能反推分析输入质量。 |
|
||||
| `data_quality=ok/partial/unavailable` | 大盘红绿灯 | `partial` 可映射,`unavailable` 视字段场景映射到 `missing` 或 `not_supported` | P0 不把大盘红绿灯纳入首版单股 pack。 |
|
||||
| `fetch_failed` | 未来数据质量细分 | P0 不扩展 | P5 再区分 `not_supported` 与 `fetch_failed`。 |
|
||||
| `fetch_failed` | 数据质量细分 | P5 映射为 `fetch_failed` | 只在已有 artifact 明确失败时使用,不代表整次分析失败。 |
|
||||
|
||||
## 七路径盘点
|
||||
|
||||
@@ -211,7 +233,7 @@ P0 只记录历史消费面。完整 pack 不应默认公开到历史详情或
|
||||
## 兼容与安全边界
|
||||
|
||||
- `analysis_history.context_snapshot.enhanced_context.date` 是当前回测日期解析兼容点,P1/P2 不能在没有迁移的情况下破坏。
|
||||
- 完整 pack 不默认公开到历史、API、Web 或通知;P4 只公开 `analysis_context_pack_overview` 低敏摘要、来源、fallback、stale、missing reason 和 block status count。
|
||||
- 完整 pack 不默认公开到历史、API、Web 或通知;P4/P5 只公开 `analysis_context_pack_overview` 低敏摘要、来源、fallback、stale、missing reason、block status count 和 `data_quality` 低敏评分。
|
||||
- pack、日志、历史快照和 API 响应不得记录 API key、token、cookie、完整 webhook URL、邮箱密码、私有环境变量或其他密钥。
|
||||
- `source`、`timestamp`、`fallback`、`stale`、`partial` 等质量元数据只用于解释输入限制,不用于阻断分析;除非现有核心路径本来就是 fail-fast。
|
||||
- #1386 的盘前 / 盘中 phase 感知是后续 `phase` / `data_quality` 字段的重要背景;P0 只记录关系,不接入 runtime。
|
||||
|
||||
@@ -780,7 +780,15 @@ P3 当时不新增 API/Web/Bot 参数,不写入 history/task status/report met
|
||||
|
||||
P4 新增 `report.details.analysis_context_pack_overview`,历史详情、同步分析响应和 completed `/api/v1/analysis/status/{task_id}` 都会返回同一份低敏 overview;Web 端报告页在“策略点位”和“资讯”之后展示默认折叠的数据块摘要,折叠头部展示可用数、缺失数、非零的其他状态计数和触发来源,展开后展示数据块状态、来源、warning、missing reason、状态计数和新闻结果数。API 返回的 `details.context_snapshot` 会剥离顶层 `analysis_context_pack_overview`,避免透明度面板重复展示 raw snapshot。
|
||||
|
||||
该 overview 不包含完整 pack、`analysis_context_pack_summary` Prompt 字符串、`items.value`、新闻正文、`trend_result`、筹码或基本面原始 payload。`SAVE_CONTEXT_SNAPSHOT=false` 或旧历史记录缺少 overview 时字段为空,报告仍正常返回。本阶段不覆盖 pending/processing TaskPanel、SSE 进行中事件、通知摘要、Bot/Desktop 专属展示、`market_review` overview 或 P5 数据质量评分。
|
||||
该 overview 不包含完整 pack、`analysis_context_pack_summary` Prompt 字符串、`items.value`、新闻正文、`trend_result`、筹码或基本面原始 payload。`SAVE_CONTEXT_SNAPSHOT=false` 或旧历史记录缺少 overview 时字段为空,报告仍正常返回。本阶段不覆盖 pending/processing TaskPanel、SSE 进行中事件、通知摘要、Bot/Desktop 专属展示、`market_review` overview 或数据质量评分。
|
||||
|
||||
#### AnalysisContextPack 数据质量评分与 Prompt 数据限制(Issue #1389 P5)
|
||||
|
||||
P5 在不修改 `PACK_VERSION = "1.0"`、不新增数据源和不改变报告 JSON schema 的前提下,给 `AnalysisContextPack` 增加轻量数据质量评分与模型可读的数据限制区块。`ContextFieldStatus` 新增 `fetch_failed`,只表示字段或数据块本次抓取明确失败;首版仅把 `fundamental_context.status == "failed"` 映射为 `fetch_failed`,空新闻、未配置搜索、无实时 quote 或 chip 缺失仍按既有 `missing` / `not_supported` 处理。
|
||||
|
||||
`DataQuality` 现在包含 `overall_score`、`level`、`block_scores`、`limitations`,并保留旧 `warnings` / `metadata`。评分固定覆盖 `quote`、`daily_bars`、`technical`、`news`、`fundamentals`、`chip` 六块,不因辅助块缺失重归一化;核心块降级会在 Prompt 的“数据限制”区块中要求模型不要输出高置信度,辅助块缺失只限制对应分析段落,不应被解释为利好或利空。该 Prompt 区块由 `format_analysis_context_pack_prompt_section()` 统一生成,普通分析、single Agent 和 multi-agent 沿用同一低敏 summary,不暴露 raw payload、新闻正文、趋势原始值、secret、token 或 webhook。
|
||||
|
||||
历史详情、同步分析响应和 completed 任务状态继续只通过 `report.details.analysis_context_pack_overview` 暴露低敏字段;P5 只在该 overview 下新增 `data_quality`,包含 score、level、block_scores 和 limitations,不重复公开 `warnings`。Web 报告页仍默认折叠展示数据块摘要,折叠头部新增质量分/等级,展开后展示限制说明和 `fetch_failed` 状态;`details.context_snapshot` 继续剥离顶层 `analysis_context_pack_overview`。
|
||||
|
||||
#### 使用 Crontab
|
||||
|
||||
|
||||
@@ -657,7 +657,15 @@ P3 itself did not add API/Web/Bot parameters, persist fields into history/task s
|
||||
|
||||
P4 adds `report.details.analysis_context_pack_overview`. History detail, sync analysis responses, and completed `/api/v1/analysis/status/{task_id}` responses now return the same low-sensitivity overview; the Web report page renders a collapsed data-block summary after Strategy and News, with available/missing counts, non-zero other status counts, and trigger source in the header and data-block status, source, warnings, missing reasons, status counts, and news result count after expansion. API `details.context_snapshot` strips the top-level `analysis_context_pack_overview` so the raw snapshot panel does not duplicate the public overview.
|
||||
|
||||
The overview does not include the full pack, the `analysis_context_pack_summary` prompt string, `items.value`, news body text, `trend_result`, chip, or fundamentals raw payloads. When `SAVE_CONTEXT_SNAPSHOT=false` or older history records lack the overview, the field is empty and the report still loads. This phase does not cover pending/processing TaskPanel, in-progress SSE events, notification summaries, Bot/Desktop-specific rendering, `market_review` overview, or P5 data-quality scoring.
|
||||
The overview does not include the full pack, the `analysis_context_pack_summary` prompt string, `items.value`, news body text, `trend_result`, chip, or fundamentals raw payloads. When `SAVE_CONTEXT_SNAPSHOT=false` or older history records lack the overview, the field is empty and the report still loads. This phase does not cover pending/processing TaskPanel, in-progress SSE events, notification summaries, Bot/Desktop-specific rendering, `market_review` overview, or data-quality scoring.
|
||||
|
||||
### AnalysisContextPack Data Quality Scoring and Prompt Limitations (Issue #1389 P5)
|
||||
|
||||
P5 adds lightweight data-quality scoring and model-readable data limitations to `AnalysisContextPack` without changing `PACK_VERSION = "1.0"`, adding data sources, or changing the report JSON schema. `ContextFieldStatus` now includes `fetch_failed`, which only means a field or data block explicitly failed to fetch in this run; the first mapping only turns `fundamental_context.status == "failed"` into `fetch_failed`, while empty news, unconfigured search, missing realtime quote, or missing chip data keep the existing `missing` / `not_supported` semantics.
|
||||
|
||||
`DataQuality` now contains `overall_score`, `level`, `block_scores`, and `limitations`, while preserving the old `warnings` / `metadata` fields. Scoring is fixed to six blocks: `quote`, `daily_bars`, `technical`, `news`, `fundamentals`, and `chip`; auxiliary missing blocks are not re-normalized away. When core blocks are degraded, the prompt's `Data Limitations` section tells the model not to return high confidence; missing auxiliary blocks only constrain their matching analysis sections and must not be interpreted as bullish or bearish. The section is generated by `format_analysis_context_pack_prompt_section()`, so regular analysis, single Agent, and multi-agent paths reuse the same low-sensitivity summary without exposing raw payloads, news body text, raw trend values, secrets, tokens, or webhooks.
|
||||
|
||||
History detail, sync analysis responses, and completed task status responses still expose only `report.details.analysis_context_pack_overview`; P5 only adds a nested `data_quality` object with score, level, block_scores, and limitations, and does not duplicate `warnings`. The Web report page remains collapsed by default, adds quality score/level to the header, and shows limitations plus `fetch_failed` status after expansion; API `details.context_snapshot` continues to strip the top-level `analysis_context_pack_overview`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from src.schemas.analysis_context_pack import ContextFieldStatus
|
||||
|
||||
ANALYSIS_CONTEXT_PACK_OVERVIEW_KEY = "analysis_context_pack_overview"
|
||||
_ALL_STATUSES = tuple(status.value for status in ContextFieldStatus)
|
||||
_DATA_QUALITY_BLOCK_KEYS = {"quote", "daily_bars", "technical", "news", "fundamentals", "chip"}
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -77,6 +78,7 @@ def render_analysis_context_pack_overview(
|
||||
},
|
||||
"blocks": overview_blocks,
|
||||
"counts": counts,
|
||||
"data_quality": _sanitize_data_quality(payload.get("data_quality")),
|
||||
"warnings": _list_strings(_nested(payload, "data_quality", "warnings")),
|
||||
"metadata": {
|
||||
"trigger_source": _safe_text(metadata.get("trigger_source")) or None,
|
||||
@@ -159,7 +161,7 @@ def _sanitize_persisted_overview(overview: Mapping[str, Any]) -> Optional[Dict[s
|
||||
return None
|
||||
|
||||
metadata = overview.get("metadata") if isinstance(overview.get("metadata"), Mapping) else {}
|
||||
return {
|
||||
sanitized = {
|
||||
"pack_version": _safe_text(overview.get("pack_version")) or "1.0",
|
||||
"created_at": _safe_text(overview.get("created_at")) or None,
|
||||
"subject": {
|
||||
@@ -175,6 +177,20 @@ def _sanitize_persisted_overview(overview: Mapping[str, Any]) -> Optional[Dict[s
|
||||
"news_result_count": _safe_int(metadata.get("news_result_count")),
|
||||
},
|
||||
}
|
||||
if "data_quality" in overview:
|
||||
sanitized["data_quality"] = _sanitize_data_quality(overview.get("data_quality"))
|
||||
return sanitized
|
||||
|
||||
|
||||
def _sanitize_data_quality(value: Any) -> Optional[Dict[str, Any]]:
|
||||
if not isinstance(value, Mapping):
|
||||
return None
|
||||
return {
|
||||
"overall_score": _safe_score(value.get("overall_score")),
|
||||
"level": _safe_quality_level(value.get("level")),
|
||||
"block_scores": _safe_block_scores(value.get("block_scores")),
|
||||
"limitations": _list_strings(value.get("limitations"), limit=5),
|
||||
}
|
||||
|
||||
|
||||
def _safe_status(value: Any) -> Optional[str]:
|
||||
@@ -182,6 +198,31 @@ def _safe_status(value: Any) -> Optional[str]:
|
||||
return text if text in _ALL_STATUSES else None
|
||||
|
||||
|
||||
def _safe_quality_level(value: Any) -> Optional[str]:
|
||||
text = _safe_text(value)
|
||||
return text if text in {"good", "usable", "limited", "poor"} else None
|
||||
|
||||
|
||||
def _safe_score(value: Any) -> Optional[int]:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
return None
|
||||
if 0 <= value <= 100:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _safe_block_scores(value: Any) -> Dict[str, int]:
|
||||
if not isinstance(value, Mapping):
|
||||
return {}
|
||||
result: Dict[str, int] = {}
|
||||
for key, score in value.items():
|
||||
text_key = _safe_text(key)
|
||||
safe_score = _safe_score(score)
|
||||
if text_key in _DATA_QUALITY_BLOCK_KEYS and safe_score is not None:
|
||||
result[text_key] = safe_score
|
||||
return result
|
||||
|
||||
|
||||
def _safe_text(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
@@ -25,6 +25,51 @@ BLOCK_LABELS_EN = {
|
||||
"news": "news",
|
||||
}
|
||||
|
||||
STATUS_LABELS_ZH = {
|
||||
"available": "可用",
|
||||
"missing": "缺失",
|
||||
"not_supported": "不支持",
|
||||
"fallback": "降级",
|
||||
"stale": "过期",
|
||||
"estimated": "估算",
|
||||
"partial": "部分可用",
|
||||
"fetch_failed": "抓取失败",
|
||||
}
|
||||
|
||||
STATUS_LABELS_EN = {
|
||||
"available": "available",
|
||||
"missing": "missing",
|
||||
"not_supported": "not supported",
|
||||
"fallback": "fallback",
|
||||
"stale": "stale",
|
||||
"estimated": "estimated",
|
||||
"partial": "partial",
|
||||
"fetch_failed": "fetch failed",
|
||||
}
|
||||
|
||||
QUALITY_LEVEL_LABELS_ZH = {
|
||||
"good": "良好",
|
||||
"usable": "可用",
|
||||
"limited": "受限",
|
||||
"poor": "较差",
|
||||
}
|
||||
|
||||
QUALITY_LEVEL_LABELS_EN = {
|
||||
"good": "good",
|
||||
"usable": "usable",
|
||||
"limited": "limited",
|
||||
"poor": "poor",
|
||||
}
|
||||
|
||||
CORE_DEGRADED_STATUSES = {
|
||||
"stale",
|
||||
"fallback",
|
||||
"missing",
|
||||
"fetch_failed",
|
||||
"partial",
|
||||
"estimated",
|
||||
}
|
||||
|
||||
SENSITIVE_MARKERS = (
|
||||
"api_key",
|
||||
"access_token",
|
||||
@@ -115,6 +160,7 @@ def _format_zh(payload: Dict[str, Any]) -> str:
|
||||
warnings = _list_strings(_nested(payload, "data_quality", "warnings"))
|
||||
if warnings:
|
||||
lines.append(f"- 数据质量提醒:{_join_text(warnings, lang='zh')}")
|
||||
lines.extend(_data_limitation_lines(payload, lang="zh"))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@@ -131,6 +177,7 @@ def _format_en(payload: Dict[str, Any]) -> str:
|
||||
warnings = _list_strings(_nested(payload, "data_quality", "warnings"))
|
||||
if warnings:
|
||||
lines.append(f"- Data quality notes: {_join_text(warnings, lang='en')}")
|
||||
lines.extend(_data_limitation_lines(payload, lang="en"))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@@ -221,6 +268,117 @@ def _metadata_lines(payload: Dict[str, Any], *, lang: str) -> List[str]:
|
||||
]
|
||||
|
||||
|
||||
def _data_limitation_lines(payload: Dict[str, Any], *, lang: str) -> List[str]:
|
||||
lines = ["", "## Data Limitations" if lang == "en" else "## 数据限制"]
|
||||
data_quality = payload.get("data_quality")
|
||||
if not isinstance(data_quality, Mapping):
|
||||
data_quality = {}
|
||||
|
||||
score = _safe_score(data_quality.get("overall_score"))
|
||||
level = _safe_text(data_quality.get("level"))
|
||||
if score is not None:
|
||||
level_text = _quality_level_label(level, lang=lang)
|
||||
if lang == "en":
|
||||
line = f"- Data quality score: {score}/100"
|
||||
if level_text:
|
||||
line += f" ({level_text})"
|
||||
else:
|
||||
line = f"- 数据质量评分:{score}/100"
|
||||
if level_text:
|
||||
line += f"({level_text})"
|
||||
lines.append(line)
|
||||
|
||||
limitations = _localized_limitations(
|
||||
_list_strings(data_quality.get("limitations")),
|
||||
lang=lang,
|
||||
)
|
||||
if limitations:
|
||||
label = "Known limitations" if lang == "en" else "已知限制"
|
||||
separator = ": " if lang == "en" else ":"
|
||||
lines.append(f"- {label}{separator}{_join_text(limitations, lang=lang)}")
|
||||
|
||||
if _has_core_degraded_block(payload):
|
||||
if lang == "en":
|
||||
lines.append(
|
||||
"- Confidence rule: when quote, daily bars, or technical data is "
|
||||
"stale, fallback, missing, fetch_failed, partial, or estimated, "
|
||||
"the final JSON confidence_level must not be High."
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
"- 置信度规则:当 quote、daily_bars 或 technical 为 stale、fallback、missing、"
|
||||
"fetch_failed、partial 或 estimated 时,最终 JSON 的 confidence_level 不得为高。"
|
||||
)
|
||||
|
||||
if lang == "en":
|
||||
lines.append(
|
||||
"- Analysis rule: missing auxiliary blocks only limit their matching "
|
||||
"analysis sections; do not treat missing data itself as bullish or bearish."
|
||||
)
|
||||
lines.append(
|
||||
"- Safety rule: use only status, source, warnings, and missing_reason "
|
||||
"from this summary; do not reproduce raw payloads, news body text, "
|
||||
"raw trend values, secrets, tokens, or webhooks."
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
"- 分析规则:辅助数据块缺失只限制对应分析段落,不要把缺失本身解释为利好或利空。"
|
||||
)
|
||||
lines.append(
|
||||
"- 安全规则:只使用本摘要中的 status、source、warnings 和 missing_reason;"
|
||||
"不要复述 raw payload、新闻正文、趋势原始值、secret、token 或 webhook。"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def _localized_limitations(limitations: List[str], *, lang: str) -> List[str]:
|
||||
labels = get_analysis_context_pack_block_labels(lang)
|
||||
status_labels = STATUS_LABELS_EN if lang == "en" else STATUS_LABELS_ZH
|
||||
result: List[str] = []
|
||||
for item in limitations:
|
||||
key, separator, status = item.partition(":")
|
||||
if not separator:
|
||||
result.append(item)
|
||||
continue
|
||||
normalized_key = key.strip()
|
||||
normalized_status = status.strip()
|
||||
label = labels.get(normalized_key, _safe_text(normalized_key))
|
||||
status_label = status_labels.get(normalized_status, _safe_text(normalized_status))
|
||||
if not label or not status_label:
|
||||
continue
|
||||
result.append(
|
||||
f"{label}: {status_label}" if lang == "en" else f"{label}:{status_label}"
|
||||
)
|
||||
return result[:5]
|
||||
|
||||
|
||||
def _has_core_degraded_block(payload: Dict[str, Any]) -> bool:
|
||||
blocks = payload.get("blocks")
|
||||
if not isinstance(blocks, Mapping):
|
||||
return False
|
||||
for key in ("quote", "daily_bars", "technical"):
|
||||
block = blocks.get(key)
|
||||
if not isinstance(block, Mapping):
|
||||
continue
|
||||
status = _safe_text(block.get("status"))
|
||||
if status in CORE_DEGRADED_STATUSES:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _quality_level_label(level: str, *, lang: str) -> str:
|
||||
labels = QUALITY_LEVEL_LABELS_EN if lang == "en" else QUALITY_LEVEL_LABELS_ZH
|
||||
return labels.get(level, "")
|
||||
|
||||
|
||||
def _safe_score(value: Any) -> Optional[int]:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
return None
|
||||
if 0 <= value <= 100:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _first_item_field(items: Any, field: str) -> Optional[str]:
|
||||
if not isinstance(items, Mapping):
|
||||
return None
|
||||
|
||||
@@ -45,6 +45,7 @@ class ContextFieldStatus(str, Enum):
|
||||
STALE = "stale"
|
||||
ESTIMATED = "estimated"
|
||||
PARTIAL = "partial"
|
||||
FETCH_FAILED = "fetch_failed"
|
||||
|
||||
|
||||
class AnalysisSubject(_AnalysisContextModel):
|
||||
@@ -90,8 +91,12 @@ class AnalysisContextBlock(_AnalysisContextModel):
|
||||
|
||||
|
||||
class DataQuality(_AnalysisContextModel):
|
||||
"""Container for future quality summaries without P5 scoring semantics."""
|
||||
"""Low-sensitivity data quality summary for an AnalysisContextPack."""
|
||||
|
||||
overall_score: Optional[int] = Field(None, ge=0, le=100)
|
||||
level: Optional[Literal["good", "usable", "limited", "poor"]] = None
|
||||
block_scores: Dict[str, int] = Field(default_factory=dict)
|
||||
limitations: List[str] = Field(default_factory=list)
|
||||
warnings: List[str] = Field(default_factory=list)
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -21,6 +21,37 @@ from src.schemas.analysis_context_pack import (
|
||||
_REALTIME_OVERLAY_WARNING = "intraday_realtime_overlay"
|
||||
_REALTIME_FALLBACK_WARNING = "realtime_provider_fallback"
|
||||
_FUNDAMENTAL_FAILED_REASON = "fundamental_pipeline_failed"
|
||||
_QUALITY_BLOCK_WEIGHTS: Dict[str, int] = {
|
||||
"quote": 25,
|
||||
"daily_bars": 25,
|
||||
"technical": 25,
|
||||
"news": 10,
|
||||
"fundamentals": 10,
|
||||
"chip": 5,
|
||||
}
|
||||
_STATUS_SCORES: Dict[ContextFieldStatus, int] = {
|
||||
ContextFieldStatus.AVAILABLE: 100,
|
||||
ContextFieldStatus.PARTIAL: 75,
|
||||
ContextFieldStatus.ESTIMATED: 75,
|
||||
ContextFieldStatus.NOT_SUPPORTED: 70,
|
||||
ContextFieldStatus.FALLBACK: 65,
|
||||
ContextFieldStatus.STALE: 50,
|
||||
ContextFieldStatus.MISSING: 35,
|
||||
ContextFieldStatus.FETCH_FAILED: 25,
|
||||
}
|
||||
_CORE_LIMITATION_STATUSES = {
|
||||
ContextFieldStatus.STALE,
|
||||
ContextFieldStatus.FALLBACK,
|
||||
ContextFieldStatus.MISSING,
|
||||
ContextFieldStatus.FETCH_FAILED,
|
||||
ContextFieldStatus.PARTIAL,
|
||||
ContextFieldStatus.ESTIMATED,
|
||||
}
|
||||
_AUX_LIMITATION_STATUSES = {
|
||||
ContextFieldStatus.FETCH_FAILED,
|
||||
ContextFieldStatus.FALLBACK,
|
||||
ContextFieldStatus.STALE,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -62,6 +93,7 @@ class AnalysisContextBuilder:
|
||||
blocks["chip"] = _build_chip_block(artifacts)
|
||||
blocks["fundamentals"] = _build_fundamentals_block(artifacts)
|
||||
blocks["news"] = _build_news_block(artifacts)
|
||||
data_quality = _build_data_quality(blocks, warnings=data_quality_warnings)
|
||||
|
||||
return AnalysisContextPack(
|
||||
subject=AnalysisSubject(
|
||||
@@ -71,7 +103,7 @@ class AnalysisContextBuilder:
|
||||
),
|
||||
phase=artifacts.phase,
|
||||
blocks=blocks,
|
||||
data_quality=DataQuality(warnings=data_quality_warnings),
|
||||
data_quality=data_quality,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@@ -413,6 +445,70 @@ def _build_news_block(artifacts: PipelineAnalysisArtifacts) -> AnalysisContextBl
|
||||
)
|
||||
|
||||
|
||||
def _build_data_quality(
|
||||
blocks: Dict[str, AnalysisContextBlock],
|
||||
*,
|
||||
warnings: List[str],
|
||||
) -> DataQuality:
|
||||
block_scores: Dict[str, int] = {}
|
||||
weighted_sum = 0
|
||||
for key, weight in _QUALITY_BLOCK_WEIGHTS.items():
|
||||
status = _quality_block_status(blocks, key)
|
||||
score = _STATUS_SCORES.get(status, _STATUS_SCORES[ContextFieldStatus.MISSING])
|
||||
block_scores[key] = score
|
||||
weighted_sum += score * weight
|
||||
|
||||
overall_score = int(round(weighted_sum / 100))
|
||||
return DataQuality(
|
||||
overall_score=overall_score,
|
||||
level=_quality_level(overall_score),
|
||||
block_scores=block_scores,
|
||||
limitations=_quality_limitations(blocks),
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
|
||||
def _quality_block_status(
|
||||
blocks: Dict[str, AnalysisContextBlock],
|
||||
key: str,
|
||||
) -> ContextFieldStatus:
|
||||
block = blocks.get(key)
|
||||
if block is None:
|
||||
return ContextFieldStatus.MISSING
|
||||
status = block.status
|
||||
if isinstance(status, ContextFieldStatus):
|
||||
return status
|
||||
try:
|
||||
return ContextFieldStatus(str(status))
|
||||
except ValueError:
|
||||
return ContextFieldStatus.MISSING
|
||||
|
||||
|
||||
def _quality_level(score: int) -> str:
|
||||
if score >= 85:
|
||||
return "good"
|
||||
if score >= 70:
|
||||
return "usable"
|
||||
if score >= 55:
|
||||
return "limited"
|
||||
return "poor"
|
||||
|
||||
|
||||
def _quality_limitations(blocks: Dict[str, AnalysisContextBlock]) -> List[str]:
|
||||
limitations: List[str] = []
|
||||
for key in ("quote", "daily_bars", "technical"):
|
||||
status = _quality_block_status(blocks, key)
|
||||
if status in _CORE_LIMITATION_STATUSES:
|
||||
limitations.append(f"{key}: {status.value}")
|
||||
|
||||
for key in ("news", "fundamentals", "chip"):
|
||||
status = _quality_block_status(blocks, key)
|
||||
if status in _AUX_LIMITATION_STATUSES:
|
||||
limitations.append(f"{key}: {status.value}")
|
||||
|
||||
return limitations[:5]
|
||||
|
||||
|
||||
def _to_dict(value: Optional[Any]) -> Dict[str, Any]:
|
||||
if value is None:
|
||||
return {}
|
||||
@@ -589,6 +685,8 @@ def _fundamental_status(status: str) -> ContextFieldStatus:
|
||||
return ContextFieldStatus.NOT_SUPPORTED
|
||||
if status == "partial":
|
||||
return ContextFieldStatus.PARTIAL
|
||||
if status == "failed":
|
||||
return ContextFieldStatus.FETCH_FAILED
|
||||
return ContextFieldStatus.MISSING
|
||||
|
||||
|
||||
@@ -598,8 +696,11 @@ def _fundamental_payload_status(
|
||||
) -> ContextFieldStatus:
|
||||
if has_payload:
|
||||
return block_status
|
||||
if block_status == ContextFieldStatus.NOT_SUPPORTED:
|
||||
return ContextFieldStatus.NOT_SUPPORTED
|
||||
if block_status in {
|
||||
ContextFieldStatus.NOT_SUPPORTED,
|
||||
ContextFieldStatus.FETCH_FAILED,
|
||||
}:
|
||||
return block_status
|
||||
return ContextFieldStatus.MISSING
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,12 @@ from src.agent.executor import AgentExecutor, AgentResult
|
||||
from src.agent.llm_adapter import LLMResponse, ToolCall
|
||||
from src.agent.runner import parse_dashboard_json, run_agent_loop, serialize_tool_result
|
||||
from src.agent.tools.registry import ToolRegistry, ToolDefinition, ToolParameter
|
||||
from src.analysis_context_pack_prompt import format_analysis_context_pack_prompt_section
|
||||
from src.config import Config
|
||||
from src.services.analysis_context_builder import (
|
||||
AnalysisContextBuilder,
|
||||
PipelineAnalysisArtifacts,
|
||||
)
|
||||
from src.storage import DatabaseManager
|
||||
|
||||
|
||||
@@ -61,6 +66,44 @@ def _make_mock_adapter():
|
||||
return adapter
|
||||
|
||||
|
||||
def _build_analysis_context_pack_summary(
|
||||
*,
|
||||
realtime_quote=None,
|
||||
fundamental_context=None,
|
||||
) -> str:
|
||||
artifacts = PipelineAnalysisArtifacts(
|
||||
code="600519",
|
||||
stock_name="贵州茅台",
|
||||
market="cn",
|
||||
phase=None,
|
||||
base_context={
|
||||
"today": {"close": 1880.0},
|
||||
"yesterday": {"close": 1870.0},
|
||||
"date": "2026-03-26",
|
||||
},
|
||||
enhanced_context={},
|
||||
realtime_quote=realtime_quote
|
||||
if realtime_quote is not None
|
||||
else {"price": 1880.0, "source": "mock_quote"},
|
||||
trend_result={"trend_status": "available"},
|
||||
chip_data={"source": "mock_chip", "date": "2026-03-26"},
|
||||
fundamental_context=fundamental_context
|
||||
if fundamental_context is not None
|
||||
else {
|
||||
"status": "ok",
|
||||
"coverage": {"valuation": "ok"},
|
||||
"source_chain": [{"provider": "fundamental_pipeline"}],
|
||||
},
|
||||
news_context="新闻摘要",
|
||||
news_result_count=1,
|
||||
metadata={"trigger_source": "api"},
|
||||
)
|
||||
return format_analysis_context_pack_prompt_section(
|
||||
AnalysisContextBuilder.build(artifacts),
|
||||
report_language="zh",
|
||||
)
|
||||
|
||||
|
||||
SAMPLE_DASHBOARD = {
|
||||
"stock_name": "贵州茅台",
|
||||
"sentiment_score": 75,
|
||||
@@ -920,6 +963,13 @@ class TestBuildUserMessage(unittest.TestCase):
|
||||
self.assertIn("报告类型: daily", msg)
|
||||
|
||||
def test_message_renders_readable_market_phase_context_without_raw_keys(self):
|
||||
summary = _build_analysis_context_pack_summary(
|
||||
realtime_quote={
|
||||
"price": 1880.0,
|
||||
"source": "fallback",
|
||||
"fallback_from": "primary_realtime_provider",
|
||||
},
|
||||
)
|
||||
msg = self.executor._build_user_message(
|
||||
"Analyze",
|
||||
context={
|
||||
@@ -932,13 +982,16 @@ class TestBuildUserMessage(unittest.TestCase):
|
||||
"effective_daily_bar_date": "2026-03-26",
|
||||
"is_partial_bar": True,
|
||||
},
|
||||
"analysis_context_pack_summary": "\n## 分析上下文包摘要\n- 数据块状态:行情 available\n",
|
||||
"analysis_context_pack_summary": summary,
|
||||
"realtime_quote": {"price": 1880.0},
|
||||
},
|
||||
)
|
||||
self.assertIn("股票代码: 600519", msg)
|
||||
self.assertIn("市场阶段上下文", msg)
|
||||
self.assertIn("分析上下文包摘要", msg)
|
||||
self.assertIn("数据限制", msg)
|
||||
self.assertIn("已知限制:行情:降级", msg)
|
||||
self.assertIn("confidence_level 不得为高", msg)
|
||||
self.assertIn("盘中", msg)
|
||||
self.assertIn("不得当作完整日线复盘", msg)
|
||||
self.assertLess(msg.index("市场阶段上下文"), msg.index("分析上下文包摘要"))
|
||||
|
||||
@@ -77,6 +77,20 @@ def _analysis_context_pack_overview() -> dict:
|
||||
"stale": 0,
|
||||
"estimated": 0,
|
||||
"partial": 0,
|
||||
"fetch_failed": 0,
|
||||
},
|
||||
"data_quality": {
|
||||
"overall_score": 88,
|
||||
"level": "good",
|
||||
"block_scores": {
|
||||
"quote": 100,
|
||||
"daily_bars": 100,
|
||||
"technical": 100,
|
||||
"news": 35,
|
||||
"fundamentals": 100,
|
||||
"chip": 100,
|
||||
},
|
||||
"limitations": [],
|
||||
},
|
||||
"warnings": ["news_context_missing"],
|
||||
"metadata": {
|
||||
@@ -818,6 +832,10 @@ class AnalysisApiContractTestCase(unittest.TestCase):
|
||||
details["analysis_context_pack_overview"]["metadata"]["trigger_source"],
|
||||
"api",
|
||||
)
|
||||
self.assertEqual(
|
||||
details["analysis_context_pack_overview"]["data_quality"]["overall_score"],
|
||||
88,
|
||||
)
|
||||
self.assertNotIn("analysis_context_pack_overview", details["context_snapshot"])
|
||||
self.assertNotIn("market_phase_summary", details["context_snapshot"])
|
||||
|
||||
@@ -1001,6 +1019,10 @@ class AnalysisApiContractTestCase(unittest.TestCase):
|
||||
report.details.analysis_context_pack_overview.metadata.trigger_source,
|
||||
"api",
|
||||
)
|
||||
self.assertEqual(
|
||||
report.details.analysis_context_pack_overview.data_quality.overall_score,
|
||||
88,
|
||||
)
|
||||
self.assertEqual(
|
||||
report.details.analysis_context_pack_overview.blocks[1].missing_reasons,
|
||||
["news_context_missing"],
|
||||
@@ -1359,6 +1381,10 @@ class AnalysisApiContractTestCase(unittest.TestCase):
|
||||
status.result.report["details"]["analysis_context_pack_overview"]["metadata"]["trigger_source"],
|
||||
"api",
|
||||
)
|
||||
self.assertEqual(
|
||||
status.result.report["details"]["analysis_context_pack_overview"]["data_quality"]["overall_score"],
|
||||
88,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"analysis_context_pack_overview",
|
||||
status.result.report["details"]["context_snapshot"],
|
||||
@@ -1450,6 +1476,10 @@ class AnalysisApiContractTestCase(unittest.TestCase):
|
||||
status.result.report["details"]["analysis_context_pack_overview"]["metadata"]["trigger_source"],
|
||||
"api",
|
||||
)
|
||||
self.assertEqual(
|
||||
status.result.report["details"]["analysis_context_pack_overview"]["data_quality"]["overall_score"],
|
||||
88,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"analysis_context_pack_overview",
|
||||
status.result.report["details"]["context_snapshot"],
|
||||
|
||||
@@ -356,7 +356,7 @@ def test_chip_missing_defaults_to_missing_and_explicit_not_supported() -> None:
|
||||
("ok", ContextFieldStatus.AVAILABLE),
|
||||
("not_supported", ContextFieldStatus.NOT_SUPPORTED),
|
||||
("partial", ContextFieldStatus.PARTIAL),
|
||||
("failed", ContextFieldStatus.MISSING),
|
||||
("failed", ContextFieldStatus.FETCH_FAILED),
|
||||
),
|
||||
)
|
||||
def test_fundamentals_maps_supported_statuses_without_raw_errors(
|
||||
@@ -416,6 +416,46 @@ def test_news_block_treats_blank_as_missing_and_records_pack_metadata() -> None:
|
||||
assert available.metadata["news_result_count"] == 5
|
||||
|
||||
|
||||
def test_data_quality_scores_fixed_blocks_and_limits_auxiliary_missing() -> None:
|
||||
pack = AnalysisContextBuilder.build(_artifacts())
|
||||
|
||||
assert pack.data_quality.overall_score == 100
|
||||
assert pack.data_quality.level == "good"
|
||||
assert pack.data_quality.block_scores == {
|
||||
"quote": 100,
|
||||
"daily_bars": 100,
|
||||
"technical": 100,
|
||||
"news": 100,
|
||||
"fundamentals": 100,
|
||||
"chip": 100,
|
||||
}
|
||||
assert pack.data_quality.limitations == []
|
||||
|
||||
failed_fundamentals = AnalysisContextBuilder.build(
|
||||
_artifacts(
|
||||
fundamental_context={
|
||||
"status": "failed",
|
||||
"coverage": {"valuation": "failed"},
|
||||
"source_chain": [
|
||||
{"provider": "fundamental_pipeline", "result": "failed"}
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert failed_fundamentals.blocks["fundamentals"].status == ContextFieldStatus.FETCH_FAILED
|
||||
assert failed_fundamentals.data_quality.block_scores["fundamentals"] == 25
|
||||
assert failed_fundamentals.data_quality.overall_score == 92
|
||||
assert failed_fundamentals.data_quality.level == "good"
|
||||
assert failed_fundamentals.data_quality.limitations == ["fundamentals: fetch_failed"]
|
||||
|
||||
blank_news = AnalysisContextBuilder.build(
|
||||
_artifacts(news_context=" ", news_result_count=0)
|
||||
)
|
||||
assert blank_news.blocks["news"].status == ContextFieldStatus.MISSING
|
||||
assert blank_news.data_quality.block_scores["news"] == 35
|
||||
assert "news: missing" not in blank_news.data_quality.limitations
|
||||
|
||||
|
||||
def test_build_batch_returns_one_pack_per_artifact() -> None:
|
||||
packs = AnalysisContextBuilder.build_batch(
|
||||
[
|
||||
|
||||
@@ -30,6 +30,7 @@ def test_analysis_context_pack_doc_has_required_sections() -> None:
|
||||
"## P2 Builder 契约",
|
||||
"## P3 Runtime Consumption",
|
||||
"## P4 历史记录、任务状态与 Web 可见性",
|
||||
"## P5 数据质量评分与 Prompt 数据限制",
|
||||
"## 字段质量状态",
|
||||
"## 现有状态映射",
|
||||
"## 七路径盘点",
|
||||
@@ -65,9 +66,11 @@ def test_analysis_context_pack_doc_defines_p0_quality_states() -> None:
|
||||
"`stale`",
|
||||
"`estimated`",
|
||||
"`partial`",
|
||||
"`fetch_failed`",
|
||||
):
|
||||
assert state in section
|
||||
assert "`fetch_failed`" not in section
|
||||
assert "P0 先固定七词" in section
|
||||
assert "P5 在同一 1.0 umbrella 内追加 `fetch_failed`" in section
|
||||
|
||||
|
||||
def test_analysis_context_pack_doc_covers_seven_paths() -> None:
|
||||
@@ -108,7 +111,7 @@ def test_analysis_context_pack_doc_records_non_goals_and_safety_boundaries() ->
|
||||
"不公开完整 pack",
|
||||
"不 pack 化 `market_review`",
|
||||
"`market_light`",
|
||||
"`fetch_failed` 与 `not_supported` 的细分留到 P5",
|
||||
"P5 已在同一 1.0 umbrella 内追加该状态",
|
||||
"`analysis_history.context_snapshot.enhanced_context.date`",
|
||||
"完整 pack 不默认公开",
|
||||
"API key",
|
||||
@@ -276,7 +279,7 @@ def test_analysis_context_pack_doc_defines_p3_runtime_consumption_boundaries() -
|
||||
"`analysis_context_pack_summary`",
|
||||
"Agent 工具级 pack cache 复用",
|
||||
"P4 在此基础上新增低敏 overview",
|
||||
"通知展示和数据质量评分仍留给后续阶段",
|
||||
"P5 继续复用 summary 消费路径",
|
||||
):
|
||||
assert token in section
|
||||
|
||||
@@ -307,13 +310,37 @@ def test_analysis_context_pack_doc_defines_p4_visibility_contract() -> None:
|
||||
"非零的其他状态计数",
|
||||
"不覆盖 pending/processing TaskPanel",
|
||||
"不改通知摘要",
|
||||
"P5 数据质量评分",
|
||||
"质量分/等级",
|
||||
"`fetch_failed` 状态",
|
||||
):
|
||||
assert token in section
|
||||
|
||||
assert "运行诊断之后、策略点位之前" not in section
|
||||
|
||||
|
||||
def test_analysis_context_pack_doc_defines_p5_data_quality_contract() -> None:
|
||||
section = _section(_read_doc(), "P5 数据质量评分与 Prompt 数据限制")
|
||||
|
||||
for token in (
|
||||
"`PACK_VERSION`",
|
||||
"`fetch_failed`",
|
||||
"`fundamental_context.status == \"failed\"`",
|
||||
"`overall_score`",
|
||||
"`level`",
|
||||
"`block_scores`",
|
||||
"`limitations`",
|
||||
"`quote=25`",
|
||||
"`fetch_failed=25`",
|
||||
"`Data Limitations`",
|
||||
"`confidence_level` 不得为 `高` / `High`",
|
||||
"`analysis_context_pack_overview.data_quality`",
|
||||
"`details.context_snapshot`",
|
||||
"不新增 fetcher",
|
||||
"不改变 LLM 输出 JSON schema",
|
||||
):
|
||||
assert token in section
|
||||
|
||||
|
||||
def test_analysis_context_pack_doc_maps_existing_status_terms() -> None:
|
||||
section = _section(_read_doc(), "现有状态映射")
|
||||
|
||||
@@ -361,18 +388,19 @@ def test_analysis_context_pack_doc_updates_indexes_and_changelog() -> None:
|
||||
changelog = (PROJECT_ROOT / "docs" / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "[分析上下文包契约、运行态消费与可见性](analysis-context-pack.md)" in index
|
||||
assert "P1/P2 内部契约、P3 Prompt 摘要消费、P4 历史/API/Web 低敏可见性" in index
|
||||
assert "P1/P2 内部契约、P3 Prompt 摘要消费、P4 历史/API/Web 低敏可见性、P5 数据质量评分" in index
|
||||
assert (
|
||||
"[Analysis Context Pack Contract, Runtime Consumption, And Visibility](analysis-context-pack.md) "
|
||||
"<sub><sub></sub></sub> "
|
||||
"<sub><sub></sub></sub> "
|
||||
"(Chinese-only)"
|
||||
) in index_en
|
||||
assert "P1/P2 internal contracts, P3 prompt-summary consumption, P4 history/API/Web low-sensitivity visibility" in index_en
|
||||
assert "P1/P2 internal contracts, P3 prompt-summary consumption, P4 history/API/Web low-sensitivity visibility, P5 data-quality scoring" in index_en
|
||||
assert "新增 AnalysisContextPack P0 上下文盘点" in changelog
|
||||
assert "新增 AnalysisContextPack P1 内部契约与脱敏序列化测试" in changelog
|
||||
assert "新增 AnalysisContextPack P2 builder" in changelog
|
||||
assert "普通分析与 Agent 运行时 Prompt 接入 AnalysisContextPack 低敏摘要" in changelog
|
||||
assert "AnalysisContextPack P4 低敏 overview 接入历史详情" in changelog
|
||||
assert "AnalysisContextPack P5 增加数据质量评分" in changelog
|
||||
assert "优化 Web 报告详情页信息层级" in changelog
|
||||
|
||||
|
||||
@@ -389,6 +417,9 @@ def test_full_guides_clarify_pack_summary_does_not_replace_legacy_payload_channe
|
||||
assert "折叠头部展示可用数、缺失数、非零的其他状态计数和触发来源" in guide
|
||||
assert "Web 报告页在策略点位和资讯之后默认折叠展示数据块状态" in guide
|
||||
assert "`details.context_snapshot` 会剥离顶层 `analysis_context_pack_overview`" in guide
|
||||
assert "AnalysisContextPack 数据质量评分与 Prompt 数据限制(Issue #1389 P5)" in guide
|
||||
assert "`fetch_failed`" in guide
|
||||
assert "折叠头部新增质量分/等级" in guide
|
||||
assert "`report.meta.market_phase_summary`" in guide
|
||||
assert "`details.context_snapshot` 会剥离顶层 `market_phase_summary`" in guide
|
||||
|
||||
@@ -401,5 +432,8 @@ def test_full_guides_clarify_pack_summary_does_not_replace_legacy_payload_channe
|
||||
assert "available/missing counts, non-zero other status counts, and trigger source" in guide_en
|
||||
assert "the Web report page shows the data-block summary collapsed after Strategy and News" in guide_en
|
||||
assert "API `details.context_snapshot` strips the top-level `analysis_context_pack_overview`" in guide_en
|
||||
assert "AnalysisContextPack Data Quality Scoring and Prompt Limitations (Issue #1389 P5)" in guide_en
|
||||
assert "`fetch_failed`" in guide_en
|
||||
assert "adds quality score/level to the header" in guide_en
|
||||
assert "`report.meta.market_phase_summary`" in guide_en
|
||||
assert "API `details.context_snapshot` strips the top-level `market_phase_summary`" in guide_en
|
||||
|
||||
@@ -95,6 +95,17 @@ def _pack() -> AnalysisContextPack:
|
||||
),
|
||||
},
|
||||
data_quality=DataQuality(
|
||||
overall_score=76,
|
||||
level="usable",
|
||||
block_scores={
|
||||
"quote": 65,
|
||||
"daily_bars": 100,
|
||||
"technical": 75,
|
||||
"news": 35,
|
||||
"fundamentals": 100,
|
||||
"chip": 100,
|
||||
},
|
||||
limitations=["quote: fallback", "technical: partial"],
|
||||
warnings=["intraday_realtime_overlay", "intraday_realtime_overlay"]
|
||||
),
|
||||
metadata={
|
||||
@@ -129,6 +140,7 @@ def test_renderer_outputs_only_public_schema_fields() -> None:
|
||||
"subject",
|
||||
"blocks",
|
||||
"counts",
|
||||
"data_quality",
|
||||
"warnings",
|
||||
"metadata",
|
||||
}
|
||||
@@ -142,6 +154,25 @@ def test_renderer_outputs_only_public_schema_fields() -> None:
|
||||
"warnings",
|
||||
"missing_reasons",
|
||||
}
|
||||
assert set(overview["data_quality"]) == {
|
||||
"overall_score",
|
||||
"level",
|
||||
"block_scores",
|
||||
"limitations",
|
||||
}
|
||||
assert overview["data_quality"] == {
|
||||
"overall_score": 76,
|
||||
"level": "usable",
|
||||
"block_scores": {
|
||||
"quote": 65,
|
||||
"daily_bars": 100,
|
||||
"technical": 75,
|
||||
"news": 35,
|
||||
"fundamentals": 100,
|
||||
"chip": 100,
|
||||
},
|
||||
"limitations": ["quote: fallback", "technical: partial"],
|
||||
}
|
||||
|
||||
|
||||
def test_renderer_does_not_dump_items_values_payloads_or_sensitive_markers() -> None:
|
||||
@@ -175,6 +206,7 @@ def test_counts_are_by_block_status_and_missing_reasons_are_deduped() -> None:
|
||||
"stale": 0,
|
||||
"estimated": 0,
|
||||
"partial": 1,
|
||||
"fetch_failed": 0,
|
||||
}
|
||||
news_block = next(block for block in overview["blocks"] if block["key"] == "news")
|
||||
assert news_block["missing_reasons"] == [
|
||||
@@ -271,6 +303,24 @@ def test_extract_reprojects_persisted_overview_to_public_schema() -> None:
|
||||
"stale": 999,
|
||||
"estimated": 999,
|
||||
"partial": 999,
|
||||
"fetch_failed": 999,
|
||||
},
|
||||
"data_quality": {
|
||||
"overall_score": 76,
|
||||
"level": "usable",
|
||||
"block_scores": {
|
||||
"quote": 65,
|
||||
"news": 35,
|
||||
"api_key": 99,
|
||||
"technical": 999,
|
||||
},
|
||||
"limitations": [
|
||||
"quote: fallback",
|
||||
"token=secret should not pass",
|
||||
"technical: partial",
|
||||
"technical: partial",
|
||||
],
|
||||
"warnings": ["not-public"],
|
||||
},
|
||||
"warnings": ["top_warning", "top_warning"],
|
||||
"metadata": {
|
||||
@@ -293,6 +343,17 @@ def test_extract_reprojects_persisted_overview_to_public_schema() -> None:
|
||||
"stale": 0,
|
||||
"estimated": 0,
|
||||
"partial": 0,
|
||||
"fetch_failed": 0,
|
||||
}
|
||||
assert extracted["data_quality"] == {
|
||||
"overall_score": 76,
|
||||
"level": "usable",
|
||||
"block_scores": {"quote": 65, "news": 35},
|
||||
"limitations": [
|
||||
"quote: fallback",
|
||||
"[REDACTED]",
|
||||
"technical: partial",
|
||||
],
|
||||
}
|
||||
assert extracted["blocks"][1]["missing_reasons"] == [
|
||||
"news_context_missing",
|
||||
@@ -308,6 +369,32 @@ def test_extract_reprojects_persisted_overview_to_public_schema() -> None:
|
||||
assert "secret-key" not in rendered
|
||||
|
||||
|
||||
def test_extract_accepts_legacy_overview_without_data_quality() -> None:
|
||||
extracted = extract_analysis_context_pack_overview(
|
||||
{
|
||||
"analysis_context_pack_overview": {
|
||||
"pack_version": "1.0",
|
||||
"subject": {"code": "600519"},
|
||||
"blocks": [
|
||||
{
|
||||
"key": "quote",
|
||||
"label": "行情",
|
||||
"status": "available",
|
||||
"source": "mock",
|
||||
"warnings": [],
|
||||
"missing_reasons": [],
|
||||
}
|
||||
],
|
||||
"metadata": {},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert extracted is not None
|
||||
assert "data_quality" not in extracted
|
||||
assert extracted["counts"]["fetch_failed"] == 0
|
||||
|
||||
|
||||
def test_extract_returns_none_for_malformed_persisted_overview() -> None:
|
||||
assert extract_analysis_context_pack_overview(
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ from src.schemas.analysis_context_pack import (
|
||||
ContextFieldStatus,
|
||||
DataQuality,
|
||||
)
|
||||
from src.services.analysis_context_builder import (
|
||||
AnalysisContextBuilder,
|
||||
PipelineAnalysisArtifacts,
|
||||
)
|
||||
|
||||
|
||||
def _pack() -> AnalysisContextPack:
|
||||
@@ -71,7 +75,20 @@ def _pack() -> AnalysisContextPack:
|
||||
},
|
||||
),
|
||||
},
|
||||
data_quality=DataQuality(warnings=["intraday_realtime_overlay"]),
|
||||
data_quality=DataQuality(
|
||||
overall_score=76,
|
||||
level="usable",
|
||||
block_scores={
|
||||
"quote": 65,
|
||||
"daily_bars": 100,
|
||||
"technical": 75,
|
||||
"news": 35,
|
||||
"fundamentals": 100,
|
||||
"chip": 100,
|
||||
},
|
||||
limitations=["quote: fallback", "technical: partial"],
|
||||
warnings=["intraday_realtime_overlay"],
|
||||
),
|
||||
metadata={
|
||||
"query_id": "q-1",
|
||||
"trigger_source": "api",
|
||||
@@ -81,6 +98,28 @@ def _pack() -> AnalysisContextPack:
|
||||
)
|
||||
|
||||
|
||||
def _builder_artifacts(*, fundamental_context: dict) -> PipelineAnalysisArtifacts:
|
||||
return PipelineAnalysisArtifacts(
|
||||
code="600519",
|
||||
stock_name="贵州茅台",
|
||||
market="cn",
|
||||
phase=None,
|
||||
base_context={
|
||||
"today": {"close": 1880.0},
|
||||
"yesterday": {"close": 1870.0},
|
||||
"date": "2026-03-26",
|
||||
},
|
||||
enhanced_context={},
|
||||
realtime_quote={"price": 1880.0, "source": "mock_quote"},
|
||||
trend_result={"trend_status": "available"},
|
||||
chip_data={"source": "mock_chip", "date": "2026-03-26"},
|
||||
fundamental_context=fundamental_context,
|
||||
news_context="新闻摘要",
|
||||
news_result_count=1,
|
||||
metadata={"trigger_source": "api"},
|
||||
)
|
||||
|
||||
|
||||
def test_empty_or_invalid_pack_returns_empty_section() -> None:
|
||||
assert format_analysis_context_pack_prompt_section(None) == ""
|
||||
assert format_analysis_context_pack_prompt_section({}) == ""
|
||||
@@ -100,6 +139,10 @@ def test_chinese_summary_renders_low_sensitivity_pack_statuses() -> None:
|
||||
assert "news_context_missing" in section
|
||||
assert "新闻结果数:3" in section
|
||||
assert "intraday_realtime_overlay" in section
|
||||
assert "数据限制" in section
|
||||
assert "数据质量评分:76/100(可用)" in section
|
||||
assert "已知限制:行情:降级、技术:部分可用" in section
|
||||
assert "confidence_level 不得为高" in section
|
||||
|
||||
|
||||
def test_english_summary_renders_readable_statuses() -> None:
|
||||
@@ -113,6 +156,10 @@ def test_english_summary_renders_readable_statuses() -> None:
|
||||
assert "quote: fallback" in section
|
||||
assert "news: missing" in section
|
||||
assert "News result count: 3" in section
|
||||
assert "Data Limitations" in section
|
||||
assert "Data quality score: 76/100 (usable)" in section
|
||||
assert "Known limitations: quote: fallback, technical: partial" in section
|
||||
assert "confidence_level must not be High" in section
|
||||
|
||||
|
||||
def test_summary_does_not_dump_values_or_sensitive_payloads() -> None:
|
||||
@@ -125,3 +172,28 @@ def test_summary_does_not_dump_values_or_sensitive_payloads() -> None:
|
||||
assert "hooks.example.test" not in section
|
||||
assert "webhook_url" not in section
|
||||
assert "access_token" not in section
|
||||
assert "N/A" not in section
|
||||
assert "None" not in section
|
||||
|
||||
|
||||
def test_builder_to_prompt_renders_aux_fetch_failed_without_confidence_cap() -> None:
|
||||
pack = AnalysisContextBuilder.build(
|
||||
_builder_artifacts(
|
||||
fundamental_context={
|
||||
"status": "failed",
|
||||
"coverage": {"valuation": "failed"},
|
||||
"source_chain": [
|
||||
{"provider": "fundamental_pipeline", "result": "failed"}
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
section = format_analysis_context_pack_prompt_section(pack)
|
||||
|
||||
assert pack.data_quality.limitations == ["fundamentals: fetch_failed"]
|
||||
assert "数据限制" in section
|
||||
assert "数据质量评分:92/100(良好)" in section
|
||||
assert "已知限制:基本面:抓取失败" in section
|
||||
assert "置信度规则" not in section
|
||||
assert "confidence_level" not in section
|
||||
|
||||
@@ -42,7 +42,14 @@ def test_pack_defaults_and_json_serialization_are_stable() -> None:
|
||||
"market": "cn",
|
||||
}
|
||||
assert dumped["blocks"] == {}
|
||||
assert dumped["data_quality"] == {"warnings": [], "metadata": {}}
|
||||
assert dumped["data_quality"] == {
|
||||
"overall_score": None,
|
||||
"level": None,
|
||||
"block_scores": {},
|
||||
"limitations": [],
|
||||
"warnings": [],
|
||||
"metadata": {},
|
||||
}
|
||||
assert dumped["metadata"] == {}
|
||||
assert dumped["created_at"] == "2026-05-24T09:30:00Z"
|
||||
|
||||
@@ -140,22 +147,26 @@ def test_item_and_block_reject_invalid_assignment_updates() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
item.timestamp = "yesterday"
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
item.status = "fetch_failed"
|
||||
item.status = "fetch_failed"
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
block.timestamp = "2026/05/24"
|
||||
|
||||
block.status = "fetch_failed"
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
block.status = "fetch_failed"
|
||||
item.status = "bad_status"
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
block.status = "bad_status"
|
||||
|
||||
assert item.timestamp == "2026-05-24T09:30:00+08:00"
|
||||
assert item.status == ContextFieldStatus.AVAILABLE
|
||||
assert item.status == ContextFieldStatus.FETCH_FAILED
|
||||
assert block.timestamp == "2026-05-24T09:30:01+08:00"
|
||||
assert block.status == ContextFieldStatus.AVAILABLE
|
||||
assert block.status == ContextFieldStatus.FETCH_FAILED
|
||||
|
||||
|
||||
def test_context_field_status_allows_only_p0_quality_states() -> None:
|
||||
def test_context_field_status_allows_current_quality_states() -> None:
|
||||
for state in (
|
||||
"available",
|
||||
"missing",
|
||||
@@ -164,14 +175,15 @@ def test_context_field_status_allows_only_p0_quality_states() -> None:
|
||||
"stale",
|
||||
"estimated",
|
||||
"partial",
|
||||
"fetch_failed",
|
||||
):
|
||||
assert ContextFieldStatus(state).value == state
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ContextFieldStatus("fetch_failed")
|
||||
ContextFieldStatus("bad_status")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
AnalysisContextItem(status="fetch_failed")
|
||||
AnalysisContextItem(status="bad_status")
|
||||
|
||||
|
||||
def test_market_phase_context_dict_can_be_used_as_phase_slot() -> None:
|
||||
@@ -209,15 +221,23 @@ def test_block_and_item_status_are_independent_contract_fields() -> None:
|
||||
assert dumped["items"]["turnover_rate"]["status"] == "missing"
|
||||
|
||||
|
||||
def test_data_quality_is_container_only() -> None:
|
||||
def test_data_quality_serializes_p5_scoring_fields_and_legacy_fields() -> None:
|
||||
data_quality = DataQuality(
|
||||
overall_score=72,
|
||||
level="usable",
|
||||
block_scores={"quote": 65},
|
||||
limitations=["quote: fallback"],
|
||||
warnings=["quote_stale"],
|
||||
metadata={"note": "P1 does not define scoring"},
|
||||
metadata={"note": "P5 scoring is low sensitivity"},
|
||||
)
|
||||
|
||||
assert data_quality.model_dump(mode="json") == {
|
||||
"overall_score": 72,
|
||||
"level": "usable",
|
||||
"block_scores": {"quote": 65},
|
||||
"limitations": ["quote: fallback"],
|
||||
"warnings": ["quote_stale"],
|
||||
"metadata": {"note": "P1 does not define scoring"},
|
||||
"metadata": {"note": "P5 scoring is low sensitivity"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +66,20 @@ def _analysis_context_pack_overview() -> dict:
|
||||
"stale": 0,
|
||||
"estimated": 0,
|
||||
"partial": 0,
|
||||
"fetch_failed": 0,
|
||||
},
|
||||
"data_quality": {
|
||||
"overall_score": 100,
|
||||
"level": "good",
|
||||
"block_scores": {
|
||||
"quote": 100,
|
||||
"daily_bars": 100,
|
||||
"technical": 100,
|
||||
"news": 100,
|
||||
"fundamentals": 100,
|
||||
"chip": 100,
|
||||
},
|
||||
"limitations": [],
|
||||
},
|
||||
"warnings": [],
|
||||
"metadata": {
|
||||
@@ -901,6 +915,10 @@ class AnalysisHistoryTestCase(unittest.TestCase):
|
||||
report.details.analysis_context_pack_overview.metadata.trigger_source,
|
||||
"api",
|
||||
)
|
||||
self.assertEqual(
|
||||
report.details.analysis_context_pack_overview.data_quality.overall_score,
|
||||
100,
|
||||
)
|
||||
self.assertIsNotNone(report.meta.market_phase_summary)
|
||||
self.assertEqual(report.meta.market_phase_summary.phase, "intraday")
|
||||
self.assertEqual(report.meta.market_phase_summary.minutes_to_close, 300)
|
||||
|
||||
Reference in New Issue
Block a user