Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions packages/shared/src/capabilities/manifests.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
6 changes: 5 additions & 1 deletion packages/shared/src/capabilities/manifests/market-kline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
5 changes: 4 additions & 1 deletion packages/shared/src/capabilities/manifests/research-news.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading