Poll for market open instead of single long setTimeout
macOS sleep suspends the Docker VM, causing long setTimeout calls to never fire. Replace the one-shot wait with a polling loop that checks getClock every 30s (capped to time-until-open), so the bot resumes promptly after the host wakes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
||||
getAssets: vi.fn(),
|
||||
getAsset: vi.fn(),
|
||||
getClock: vi.fn().mockResolvedValue({
|
||||
is_open: false,
|
||||
is_open: true,
|
||||
next_open: new Date().toISOString(),
|
||||
next_close: new Date().toISOString(),
|
||||
next_close: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
}),
|
||||
getLatestAsk: vi.fn(),
|
||||
getLatestBid: vi.fn(),
|
||||
@@ -103,17 +103,11 @@ describe('Bot', () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await promise;
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), 'waiting for open');
|
||||
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), 'market is open, running strategies');
|
||||
});
|
||||
|
||||
it('skips waiting when market is already open', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getClock: vi.fn().mockResolvedValue({
|
||||
is_open: true,
|
||||
next_open: new Date().toISOString(),
|
||||
next_close: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
const alpaca = mockAlpaca();
|
||||
const strategy = mockStrategy();
|
||||
const bot = new Bot(alpaca, [{ strategy, capitalAllocation: 1.0 }]);
|
||||
|
||||
@@ -124,5 +118,24 @@ describe('Bot', () => {
|
||||
expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('[INFO]'), 'waiting for open');
|
||||
expect(strategy.execute).toHaveBeenCalledWith(alpaca, 10000);
|
||||
});
|
||||
|
||||
it('waits for market open when closed', async () => {
|
||||
const futureDate = new Date(Date.now() + 60000).toISOString();
|
||||
const alpaca = mockAlpaca({
|
||||
getClock: vi.fn()
|
||||
.mockResolvedValueOnce({ is_open: false, next_open: futureDate, next_close: futureDate })
|
||||
.mockResolvedValueOnce({ is_open: false, next_open: futureDate, next_close: futureDate })
|
||||
.mockResolvedValue({ is_open: true, next_open: futureDate, next_close: new Date(Date.now() + 86_400_000).toISOString() }),
|
||||
});
|
||||
const strategy = mockStrategy();
|
||||
const bot = new Bot(alpaca, [{ strategy, capitalAllocation: 1.0 }]);
|
||||
|
||||
const promise = bot.runDay();
|
||||
await vi.advanceTimersByTimeAsync(120000);
|
||||
await promise;
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), 'waiting for open');
|
||||
expect(strategy.execute).toHaveBeenCalledWith(alpaca, 10000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,17 +69,36 @@ describe('waitForNextOpen', () => {
|
||||
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('calls getClock and waits until next_open', async () => {
|
||||
it('polls until market opens', async () => {
|
||||
const futureDate = new Date(Date.now() + 60000).toISOString();
|
||||
const alpaca = mockAlpaca({
|
||||
getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: futureDate, next_close: futureDate }),
|
||||
getClock: vi.fn()
|
||||
.mockResolvedValueOnce({ is_open: false, next_open: futureDate, next_close: futureDate })
|
||||
.mockResolvedValueOnce({ is_open: false, next_open: futureDate, next_close: futureDate })
|
||||
.mockResolvedValueOnce({ is_open: true, next_open: futureDate, next_close: futureDate }),
|
||||
});
|
||||
|
||||
const promise = waitForNextOpen(alpaca);
|
||||
await vi.advanceTimersByTimeAsync(60000);
|
||||
const promise = waitForNextOpen(alpaca, 100);
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
await promise;
|
||||
|
||||
expect(alpaca.getClock).toHaveBeenCalled();
|
||||
expect(alpaca.getClock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('sleeps at most pollInterval even when open is far away', async () => {
|
||||
const farFuture = new Date(Date.now() + 86_400_000).toISOString();
|
||||
const alpaca = mockAlpaca({
|
||||
getClock: vi.fn()
|
||||
.mockResolvedValueOnce({ is_open: false, next_open: farFuture, next_close: farFuture })
|
||||
.mockResolvedValueOnce({ is_open: true, next_open: farFuture, next_close: farFuture }),
|
||||
});
|
||||
|
||||
const promise = waitForNextOpen(alpaca, 200);
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
await promise;
|
||||
|
||||
// Should have polled twice: first sleep capped to 200ms, then saw is_open
|
||||
expect(alpaca.getClock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -37,10 +37,17 @@ export async function isMarketOpen(alpaca: Alpaca): Promise<boolean> {
|
||||
return clock.is_open;
|
||||
}
|
||||
|
||||
export async function waitForNextOpen(alpaca: Alpaca) {
|
||||
const clock = await alpaca.getClock();
|
||||
const ms = new Date(clock.next_open).valueOf() - new Date().valueOf();
|
||||
logger.info(`market closed, next open: ${clock.next_open} (waiting ${ms}ms)`);
|
||||
return wait(ms);
|
||||
export async function waitForNextOpen(alpaca: Alpaca, pollInterval = 30_000) {
|
||||
while (true) {
|
||||
const clock = await alpaca.getClock();
|
||||
if (clock.is_open) {
|
||||
logger.info('market is now open');
|
||||
return;
|
||||
}
|
||||
const ms = new Date(clock.next_open).valueOf() - Date.now();
|
||||
const sleepMs = Math.min(Math.max(ms, 0), pollInterval);
|
||||
logger.info(`market closed, next open: ${clock.next_open} (sleeping ${sleepMs}ms)`);
|
||||
await wait(sleepMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user