Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5cc6cca
fix: handle sparse minute kline history OK-60116
limichange Aug 17, 2026
00adb59
fix: address sparse kline review feedback OK-60116
limichange Aug 17, 2026
c93ecf1
fix: preload native kline history by viewport OK-60116
limichange Aug 18, 2026
b57cf1b
fix: batch sparse kline recovery requests OK-60116
limichange Aug 18, 2026
ffeb5e7
Merge branch 'x' into fix/native-kline-sparse-history
limichange Aug 18, 2026
bcf2151
fix: paginate capped sparse kline batches OK-60116
limichange Aug 18, 2026
c5f1c13
Merge branch 'x' into fix/native-kline-sparse-history
limichange Aug 18, 2026
d1ddd6b
fix: preserve sparse kline preload page OK-60116
limichange Aug 18, 2026
c23d4dc
fix: keep all returned kline history OK-60116
limichange Aug 18, 2026
22d94a7
fix: keep address kline history stable OK-60116
limichange Aug 18, 2026
2c7b5bf
fix: exhaust sparse kline history ranges OK-60116
limichange Aug 18, 2026
28a1540
fix: scan sparse kline history by time blocks OK-60116
limichange Aug 18, 2026
30a6aaf
Merge branch 'x' into fix/native-kline-sparse-history
limichange Aug 18, 2026
1aba8e8
fix: update TradingView source key assertion OK-60116
limichange Aug 18, 2026
2f3ff51
fix: avoid recursive sparse kline requests OK-60116
limichange Aug 18, 2026
a17846c
fix: cap consecutive empty kline windows OK-60116
limichange Aug 18, 2026
bfe2ae1
fix: keep kline history source consistent OK-60116
limichange Aug 18, 2026
909e9aa
fix: cap sparse kline recovery requests OK-60116
limichange Aug 19, 2026
237921b
Merge branch 'x' into fix/native-kline-sparse-history
limichange Aug 19, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -717,20 +717,242 @@ describe('TradingViewNative K-line data state machine', () => {
});
});

it('does not paginate when the initial batch is shorter than requested', async () => {
mockHistoryBatchSize = 299;
mockHistoryRequestCandleCount = 2000;
mockFetchHistory.mockResolvedValue(
buildSequentialResponse({ count: 298, firstTimestamp: 1_000_000 }),
);
it.each([
['1', 60],
['5', 5 * 60],
] as const)(
'backfills %s-minute Market history after a sparse initial page',
async (activeInterval, intervalSeconds) => {
mockHistoryBatchSize = 299;
mockHistoryRequestCandleCount = 2000;
mockReadTradingViewNativeActiveInterval.mockReturnValue(activeInterval);
let activeIntervalRequestCount = 0;
const firstScan =
createDeferred<ITradingViewNativeHistoryResponse | null>();
mockFetchHistory.mockImplementation(
async ({ interval, timeFrom, timeTo }) => {
if (interval.value === '1W') {
return buildResponse(50, 100_000);
}
if (interval.value === '1D') {
return buildResponse(60, 110_000);
}

activeIntervalRequestCount += 1;
if (activeIntervalRequestCount === 1) {
return buildMultiPointResponse([
{ close: 100, timestamp: 1_000_000 },
{ close: 110, timestamp: 1_000_000 + intervalSeconds },
]);
}
if (activeIntervalRequestCount === 2) {
expect({ timeFrom, timeTo }).toEqual({
timeFrom: 913_599,
timeTo: 999_999,
});
return firstScan.promise;
}
expect({ timeFrom, timeTo }).toEqual({
timeFrom: 827_198,
timeTo: 913_598,
});
return buildMultiPointResponse(
Array.from({ length: 196 }, (_, index) => ({
close: 90 + index,
timestamp: 830_000 + index * intervalSeconds,
})),
);
},
);
const { result } = renderHook(() =>
useTradingViewNativeKLine({ source: buildMarketSource() }),
);

await waitFor(() => expect(result.current.points).toHaveLength(2));
act(() => firstScan.resolve({ points: [], total: 0 }));
await waitFor(() => expect(result.current.points).toHaveLength(198));
expect(mockFetchHistory).toHaveBeenCalledTimes(5);
expect(result.current.points[0]?.t).toBe(830_000);
},
);

it.each([
['1', 60],
['5', 5 * 60],
] as const)(
'recovers %s-minute Market history across an empty time window',
async (activeInterval, intervalSeconds) => {
mockHistoryBatchSize = 2;
mockHistoryRequestCandleCount = 2;
mockReadTradingViewNativeActiveInterval.mockReturnValue(activeInterval);
jest.spyOn(Date, 'now').mockReturnValue(2_000_000_000);
let activeIntervalRequestCount = 0;
mockFetchHistory.mockImplementation(async ({ interval }) => {
if (interval.value === '1W') {
return buildResponse(50, 100_000);
}
if (interval.value === '1D') {
return buildResponse(60, 110_000);
}

activeIntervalRequestCount += 1;
if (activeIntervalRequestCount === 1) {
return buildMultiPointResponse([
{ close: 100, timestamp: 1_000_000 },
{ close: 110, timestamp: 1_000_000 + intervalSeconds },
]);
}
if (activeIntervalRequestCount === 2) {
return { points: [], total: 0 };
}
return buildMultiPointResponse(
Array.from({ length: 99 }, (_, index) => ({
close: 70 + index,
timestamp: 920_100 + index * intervalSeconds,
})),
);
});
const { result } = renderHook(() =>
useTradingViewNativeKLine({ source: buildMarketSource() }),
);

await waitFor(() => expect(result.current.points).toHaveLength(2));
act(() =>
result.current.handleVisiblePointRangeChange({ startIndex: 0 }),
);

await waitFor(() => expect(result.current.points).toHaveLength(101));
expect(result.current.points[0]?.t).toBe(920_100);
const recoveryTimeTo = 999_999 - 99 * intervalSeconds - 1;
expect(
mockFetchHistory.mock.calls.filter(
([request]) => request.interval.value === activeInterval,
)[2]?.[0],
).toEqual(
expect.objectContaining({
timeFrom: recoveryTimeTo - 86_400,
timeTo: recoveryTimeTo,
}),
);
expect(result.current.calendarAvailableTimeRange).toEqual({
from: 110_000,
});
expect(
mockFetchHistory.mock.calls.filter(
([request]) => request.interval.value === '1W',
),
).toHaveLength(1);
expect(
mockFetchHistory.mock.calls.filter(
([request]) => request.interval.value === '1D',
),
).toHaveLength(1);
},
);

it.each([
['1', 60],
['5', 5 * 60],
] as const)(
'continues %s-minute Market load-more after a short non-empty page',
async (activeInterval, intervalSeconds) => {
mockHistoryBatchSize = 2;
mockHistoryRequestCandleCount = 2;
mockReadTradingViewNativeActiveInterval.mockReturnValue(activeInterval);
let activeIntervalRequestCount = 0;
mockFetchHistory.mockImplementation(async ({ interval }) => {
if (interval.value === '1W') {
return buildResponse(50, 100_000);
}
if (interval.value === '1D') {
return buildResponse(60, 110_000);
}

activeIntervalRequestCount += 1;
if (activeIntervalRequestCount === 1) {
return buildMultiPointResponse([
{ close: 100, timestamp: 1_000_000 },
{ close: 110, timestamp: 1_000_000 + intervalSeconds },
]);
}
if (activeIntervalRequestCount === 2) {
return buildResponse(90, 995_000);
}
return buildMultiPointResponse(
Array.from({ length: 98 }, (_, index) => ({
close: 70 + index,
timestamp: 920_100 + index * intervalSeconds,
})),
);
});
const { result } = renderHook(() =>
useTradingViewNativeKLine({ source: buildMarketSource() }),
);

await waitFor(() => expect(result.current.points).toHaveLength(2));
act(() =>
result.current.handleVisiblePointRangeChange({ startIndex: 0 }),
);

await waitFor(() => expect(result.current.points).toHaveLength(101));
expect(result.current.points[0]?.t).toBe(920_100);
const recoveryTimeTo = 999_999 - 99 * intervalSeconds - 1;
expect(
mockFetchHistory.mock.calls.filter(
([request]) => request.interval.value === activeInterval,
)[2]?.[0],
).toEqual(
expect.objectContaining({
timeFrom: recoveryTimeTo - 86_400,
timeTo: recoveryTimeTo,
}),
);
expect(result.current.points.some((point) => point.t === 995_000)).toBe(
true,
);
expect(mockFetchHistory).toHaveBeenCalledTimes(5);
},
);

it('stops sparse Market pagination at the refined daily boundary', async () => {
mockHistoryBatchSize = 2;
mockHistoryRequestCandleCount = 2;
mockReadTradingViewNativeActiveInterval.mockReturnValue('1');
jest.spyOn(Date, 'now').mockReturnValue(2_000_000_000);
mockFetchHistory.mockImplementation(async ({ interval }) => {
if (interval.value === '1W') {
return buildResponse(50, 100_000);
}
if (interval.value === '1D') {
return buildResponse(60, 110_000);
}
if (interval.value === '1') {
return mockFetchHistory.mock.calls.filter(
([request]) => request.interval.value === '1',
).length === 1
? buildMultiPointResponse([
{ close: 100, timestamp: 110_000 },
{ close: 110, timestamp: 110_060 },
])
: { points: [], total: 0 };
}
return { points: [], total: 0 };
});
const { result } = renderHook(() =>
useTradingViewNativeKLine({ source: buildMarketSource() }),
);

await waitFor(() => expect(result.current.points).toHaveLength(298));
await waitFor(() => expect(result.current.points).toHaveLength(2));
act(() => result.current.handleVisiblePointRangeChange({ startIndex: 0 }));
await waitFor(() =>
expect(result.current.calendarAvailableTimeRange).toEqual({
from: 110_000,
}),
);
expect(mockFetchHistory).toHaveBeenCalledTimes(4);

expect(mockFetchHistory).toHaveBeenCalledTimes(1);
act(() => result.current.handleVisiblePointRangeChange({ startIndex: 0 }));
expect(mockFetchHistory).toHaveBeenCalledTimes(4);
});

it('does not paginate CoinGecko fallback data as Market history', async () => {
Expand Down Expand Up @@ -3222,7 +3444,8 @@ describe('TradingViewNative K-line data state machine', () => {
firstTimestamp: 7_850_800,
startingClose: 400,
}),
);
)
.mockResolvedValueOnce(buildResponse(399, 7_847_200));
const { result } = renderHook(() =>
useTradingViewNativeKLine({ source: buildMarketSource() }),
);
Expand All @@ -3247,10 +3470,17 @@ describe('TradingViewNative K-line data state machine', () => {
);
await waitFor(() => expect(result.current.points).toHaveLength(896));
act(() => result.current.handleVisiblePointRangeChange({ startIndex: 0 }));
expect(mockFetchHistory).toHaveBeenCalledTimes(3);
await waitFor(() => expect(mockFetchHistory).toHaveBeenCalledTimes(4));
expect(mockFetchHistory.mock.calls[3]?.[0]).toEqual(
expect.objectContaining({
timeFrom: 650_799,
timeTo: 7_850_799,
}),
);
await waitFor(() => expect(result.current.points).toHaveLength(897));
});

it('continues native Market pagination after a full 200-point page', async () => {
it('continues native Market pagination after a sparse 199-point page', async () => {
mockHistoryBatchSize = 200;
mockHistoryRequestCandleCount = 2000;
mockFetchHistory
Expand All @@ -3267,7 +3497,8 @@ describe('TradingViewNative K-line data state machine', () => {
firstTimestamp: 9_283_600,
startingClose: 800,
}),
);
)
.mockResolvedValueOnce(buildResponse(799, 9_280_000));
const { result } = renderHook(() =>
useTradingViewNativeKLine({
source: buildMarketSource({ tokenAddress: '' }),
Expand All @@ -3280,7 +3511,8 @@ describe('TradingViewNative K-line data state machine', () => {
await waitFor(() => expect(result.current.points).toHaveLength(399));

act(() => result.current.handleVisiblePointRangeChange({ startIndex: 0 }));
expect(mockFetchHistory).toHaveBeenCalledTimes(2);
await waitFor(() => expect(mockFetchHistory).toHaveBeenCalledTimes(3));
await waitFor(() => expect(result.current.points).toHaveLength(400));
});

it('loads older history through the Hyperliquid provider path', async () => {
Expand Down
Loading
Loading