diff --git a/src/bot.test.ts b/src/bot.test.ts index 8bd001e..f45691b 100644 --- a/src/bot.test.ts +++ b/src/bot.test.ts @@ -9,9 +9,9 @@ function mockAlpaca(overrides: Partial = {}): 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); + }); }); }); diff --git a/src/trading.test.ts b/src/trading.test.ts index 04e0715..2f086ca 100644 --- a/src/trading.test.ts +++ b/src/trading.test.ts @@ -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); }); }); diff --git a/src/trading.ts b/src/trading.ts index 88ed033..60663a4 100644 --- a/src/trading.ts +++ b/src/trading.ts @@ -37,10 +37,17 @@ export async function isMarketOpen(alpaca: Alpaca): Promise { 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); + } }