diff --git a/packages/shared/src/capabilities/manifests.test.ts b/packages/shared/src/capabilities/manifests.test.ts index 20ceca5..dcc308c 100644 --- a/packages/shared/src/capabilities/manifests.test.ts +++ b/packages/shared/src/capabilities/manifests.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'bun:test'; import { createMarketQuoteCapability } from './manifests/market-quote.ts'; +import { createMarketKlineCapability } from './manifests/market-kline.ts'; +import { createMarketIntradayCapability } from './manifests/market-intraday.ts'; +import { createResearchNewsCapability } from './manifests/research-news.ts'; import type { CapabilityFetchers } from './fetchers.ts'; const quote = { @@ -81,3 +84,62 @@ describe('market.quote manifest', () => { expect(cap.riskLevel).toBe('read'); }); }); + +// Kline / intraday / news timestamps are epoch SECONDS (each manifest's own +// formatter does `timestamp * 1000`), but `provenance.marketTime` is epoch MS +// everywhere else — `market.quote` writes `quote.timestamp * 1000` and the +// longbridge adapter's `marketTimeMsFrom()` multiplies by 1000. +const SECOND_TS = 1710000000; + +describe('provenance.marketTime is epoch milliseconds', () => { + it('market.kline converts the last bar timestamp from seconds', async () => { + const cap = createMarketKlineCapability( + fetchers({ + getKline: async () => [ + { symbol: 'AAPL.US', timestamp: SECOND_TS - 86400, open: 1, high: 1, low: 1, close: 1, volume: 1 }, + { symbol: 'AAPL.US', timestamp: SECOND_TS, open: 2, high: 2, low: 2, close: 2, volume: 2 }, + ], + }) + ); + const result = await cap.execute({ symbol: 'AAPL.US' }, { now: () => 12345 }); + expect(result.provenance.marketTime).toBe(SECOND_TS * 1000); + }); + + it('market.intraday converts the last tick timestamp from seconds', async () => { + const cap = createMarketIntradayCapability( + fetchers({ + getIntraday: async () => [ + { symbol: 'AAPL.US', timestamp: SECOND_TS - 60, price: 199, volume: 10 }, + { symbol: 'AAPL.US', timestamp: SECOND_TS, price: 200, volume: 20 }, + ], + }) + ); + const result = await cap.execute({ symbol: 'AAPL.US' }, { now: () => 12345 }); + expect(result.provenance.marketTime).toBe(SECOND_TS * 1000); + }); + + it('research.news converts the latest item timestamp from seconds', async () => { + const cap = createResearchNewsCapability( + fetchers({ + getNews: async () => [ + { + id: 'n-1', + title: 'Apple ships', + summary: 'A summary.', + url: 'https://example.com/n1', + timestamp: SECOND_TS, + symbols: ['AAPL.US'], + }, + ], + }) + ); + const result = await cap.execute({ symbol: 'AAPL.US' }, { now: () => 12345 }); + expect(result.provenance.marketTime).toBe(SECOND_TS * 1000); + }); + + it('leaves marketTime undefined when the series is empty', async () => { + const cap = createMarketKlineCapability(fetchers({ getKline: async () => [] })); + const result = await cap.execute({ symbol: 'AAPL.US' }, { now: () => 12345 }); + expect(result.provenance.marketTime).toBeUndefined(); + }); +}); diff --git a/packages/shared/src/capabilities/manifests/market-intraday.ts b/packages/shared/src/capabilities/manifests/market-intraday.ts index 9af28d5..1170bba 100644 --- a/packages/shared/src/capabilities/manifests/market-intraday.ts +++ b/packages/shared/src/capabilities/manifests/market-intraday.ts @@ -24,12 +24,15 @@ export function createMarketIntradayCapability( async execute(input, ctx) { const symbol = normalizeSymbol(input.symbol); const data = await fetchers.getIntraday(symbol); + // IntradayData.timestamp is epoch SECONDS (see formatIntraday below); + // provenance.marketTime is epoch MS everywhere else. + const latest = data[data.length - 1]; return { data, provenance: { provider: 'longbridge', fetchedAt: (ctx?.now ?? Date.now)(), - marketTime: data[data.length - 1]?.timestamp, + marketTime: latest === undefined ? undefined : latest.timestamp * 1000, stale: false, }, summary: formatIntraday(symbol, data), diff --git a/packages/shared/src/capabilities/manifests/market-kline.ts b/packages/shared/src/capabilities/manifests/market-kline.ts index 280c50d..46a39d3 100644 --- a/packages/shared/src/capabilities/manifests/market-kline.ts +++ b/packages/shared/src/capabilities/manifests/market-kline.ts @@ -43,12 +43,16 @@ export function createMarketKlineCapability( const period = input.period ?? '1d'; const limit = input.limit ?? 100; const klines = await fetchers.getKline({ symbol, period, limit }); + // Kline.timestamp is epoch SECONDS (see formatKline below); every other + // producer writes provenance.marketTime in epoch MS (market.quote does + // `timestamp * 1000`, longbridge's marketTimeMsFrom() the same). + const latest = klines[klines.length - 1]; return { data: klines, provenance: { provider: 'longbridge', fetchedAt: (ctx?.now ?? Date.now)(), - marketTime: klines[klines.length - 1]?.timestamp, + marketTime: latest === undefined ? undefined : latest.timestamp * 1000, stale: false, }, summary: formatKline(symbol, period, klines), diff --git a/packages/shared/src/capabilities/manifests/research-news.ts b/packages/shared/src/capabilities/manifests/research-news.ts index f55eb18..44374df 100644 --- a/packages/shared/src/capabilities/manifests/research-news.ts +++ b/packages/shared/src/capabilities/manifests/research-news.ts @@ -29,12 +29,15 @@ export function createResearchNewsCapability( // summaries, research data bundles, Copilot tool results) sees // neutralized text only. const news = sanitizeNewsItems(await fetchers.getNews(symbol)); + // NewsItem.timestamp is epoch SECONDS (see formatNews below); + // provenance.marketTime is epoch MS everywhere else. + const latest = news[0]; return { data: news, provenance: { provider: 'longbridge', fetchedAt: (ctx?.now ?? Date.now)(), - marketTime: news[0]?.timestamp, + marketTime: latest === undefined ? undefined : latest.timestamp * 1000, stale: false, }, summary: formatNews(symbol, news),