Skip to content

fix(capabilities): kline/intraday/news 的 provenance.marketTime 从秒改为毫秒 - #163

Merged
helsome merged 1 commit into
helsome:mainfrom
wxrbyte:codex/fix-capability-markettime-ms
Sep 22, 2026
Merged

helsome merged 1 commit into
helsome:mainfrom
wxrbyte:codex/fix-capability-markettime-ms

Conversation

@wxrbyte

@wxrbyte wxrbyte commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

问题

三个 capability manifest 直接把 epoch 的时间戳写进 provenance.marketTime
而该字段在所有其它产出方都是 epoch 毫秒(同一个根因:manifest 漏做秒→毫秒换算)。

// packages/shared/src/capabilities/manifests/market-kline.ts:51
marketTime: klines[klines.length - 1]?.timestamp
// packages/shared/src/capabilities/manifests/market-intraday.ts:32
marketTime: data[data.length - 1]?.timestamp
// packages/shared/src/capabilities/manifests/research-news.ts:37
marketTime: news[0]?.timestamp

判为 bug 的依据(单位证据)

marketTime 是毫秒:

  1. market-quote.ts:51marketTime: quote.timestamp * 1000
  2. longbridge adapter 的 marketTimeMsFrom() 名字带 Ms,实现是 return seconds * 1000
  3. 同一 provenance 对象里的 fetchedAtDate.now(),即毫秒。

这些 timestamp 是秒(同文件格式化函数即为证据):

  • market-kline.ts:66new Date(kline.timestamp * 1000).toLocaleDateString()
  • market-intraday.ts:47new Date(item.timestamp * 1000).toLocaleTimeString()
  • research-news.ts:54new Date(item.timestamp * 1000).toLocaleString()

实际 / 预期

const cap = createMarketKlineCapability(fetchers({
  getKline: async () => [{ symbol: 'AAPL.US', timestamp: 1710000000, open: 2, high: 2, low: 2, close: 2, volume: 2 }],
}))
;(await cap.execute({ symbol: 'AAPL.US' }, { now: () => 12345 })).provenance.marketTime
  • 实际:1710000000
  • 预期:1710000000000

影响

buildFinancialEvidence()packages/shared/src/evidence/financial-evidence.ts:54):

const asOf = numberValue(provenance.marketTime) ?? inferAsOf(result.data);

直接把 marketTime 当作证据信封的 asOf。kline / intraday / news 三类证据的 asOf
因此比正确值小 1000 倍(1.71e9 而非 1.71e12)——按日期渲染或做新鲜度比较时会落到
1970 年。research/runner.ts:355 也把这个值原样带上。

修复

三处统一改为 latest.timestamp * 1000,序列为空时保持 undefined

const latest = klines[klines.length - 1];
marketTime: latest === undefined ? undefined : latest.timestamp * 1000,

范围

仅这三个 manifest 的 marketTime 赋值 + 一条注释。不改 data、不改 summary 格式化。

未一并改动的同类写法:phase-two.tsmarket.trades / market.capitalFlow 也是
latest?.timestamp / flow.timestamp,但这些类型的时间戳单位没有同文件证据可确认,
留给单独确认,不混入本 PR。

无可见 UI 变化(数据是错的,改成正确值之后展示才正确)。

测试

packages/shared/src/capabilities/manifests.test.ts 新增 provenance.marketTime is epoch milliseconds 一组 4 个用例:

用例 期望
market.kline converts the last bar timestamp from seconds 1710000000 * 1000
market.intraday converts the last tick timestamp from seconds 1710000000 * 1000
research.news converts the latest item timestamp from seconds 1710000000 * 1000
leaves marketTime undefined when the series is empty undefined

回归证明(修复前):

bun test packages/shared/src/capabilities --isolate
  -> 37 pass / 3 fail   (前三个用例失败,空序列用例本来就过)

修复后:

bun test packages/shared/src/capabilities --isolate
  -> 40 pass / 0 fail  (Ran 40 tests across 8 files)

bun run typecheck
  -> @finagent/core / i18n / shared / ui / electron 全部 exit 0

基线说明(Full unit tests advisory):干净 origin/main7c9b550)上

bun test packages/shared --isolate
  -> 1069 pass / 5 fail

5 项失败在 main 上可稳定复现,与本改动无关:

  1. ExperimentService.runExperiment > creates + updates the experiment and persists runs and results
  2. ResearchService > runs a report end-to-end and persists it
  3. ResearchService > lists runs newest-first
  4. ResearchService > plans from the strategy and persists strategyId onto the report
  5. langfuse backend > does not throw when Langfuse is down — agent path keeps a diagnostic

本改动未引入任何新的失败。

环境

  • Bun 1.4.2
  • Windows

Closes #162

三个 capability manifest 直接把 epoch **秒**的时间戳写进 `provenance.marketTime`,
而该字段在所有其它产出方都是 epoch **毫秒**:

- `market-quote.ts:51` 写 `quote.timestamp * 1000`
- longbridge adapter 的 `marketTimeMsFrom()`(`marketTimeMsFrom` 命名 + `* 1000`)
- 同一对象里的 `fetchedAt` 也是 `Date.now()`(毫秒)

涉事三处(同一个根因:manifest 漏做秒→毫秒换算):

```ts
// market-kline.ts:51
marketTime: klines[klines.length - 1]?.timestamp
// market-intraday.ts:32
marketTime: data[data.length - 1]?.timestamp
// research-news.ts:37
marketTime: news[0]?.timestamp
```

这些时间戳确实是秒——**同文件**的格式化函数就是证据:

- `market-kline.ts:66`  `new Date(kline.timestamp * 1000)`
- `market-intraday.ts:47` `new Date(item.timestamp * 1000)`
- `research-news.ts:54` `new Date(item.timestamp * 1000)`

## 影响

`buildFinancialEvidence()`(`packages/shared/src/evidence/financial-evidence.ts:54`)
直接把 `provenance.marketTime` 当作证据信封的 `asOf`。于是 kline/intraday/news
三类证据的 `asOf` 比正确值小 1000 倍(如 1.71e9 而非 1.71e12),任何按日期渲染
或做新鲜度比较的地方都会落到 1970 年。research runner 也会把这个值原样带上
(`research/runner.ts:355`)。

## 修复

三处统一改为 `latest.timestamp * 1000`,并在序列为空时保持 `undefined`。

范围:仅这三个 manifest 的 `marketTime` 赋值,不改 `data`、不改 summary 格式化,
也不动 phase-two 里未经证实的同类写法。无可见 UI 变化(数据正确后展示才正确)。

## 测试

`packages/shared/src/capabilities/manifests.test.ts` 新增 4 个用例:
kline / intraday / news 各自校验 `marketTime === 1710000000 * 1000`,
以及空序列时 `marketTime` 为 `undefined`。

回归证明(修复前):

```
bun test packages/shared/src/capabilities --isolate
  -> 37 pass / 3 fail
```

修复后:

```
bun test packages/shared/src/capabilities --isolate
  -> 40 pass / 0 fail  (Ran 40 tests across 8 files)

bun run typecheck
  -> @finagent/core / i18n / shared / ui / electron 全部 exit 0
```

基线说明:干净 `origin/main`(`7c9b550`)上 `bun test packages/shared --isolate`
为 1069 pass / 5 fail(`ExperimentService.runExperiment` ×1、`ResearchService` ×3、
`langfuse backend` ×1)。这些失败在 main 上可复现,与本改动无关;本改动未引入
任何新的失败。
@helsome
helsome merged commit 291c458 into helsome:main Sep 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

capabilities: kline/intraday/news 把 epoch 秒写进 provenance.marketTime(应为毫秒)

2 participants