diff --git a/src/momentum-strategy.test.ts b/src/momentum-strategy.test.ts index 714273d..69aed17 100644 --- a/src/momentum-strategy.test.ts +++ b/src/momentum-strategy.test.ts @@ -7,7 +7,11 @@ function mockAlpaca(overrides: Partial = {}): Alpaca { getAccount: vi.fn(), getAssets: vi.fn(), getAsset: vi.fn(), - getClock: vi.fn(), + getClock: vi.fn().mockResolvedValue({ + is_open: true, + next_open: new Date().toISOString(), + next_close: new Date(Date.now() + 86_400_000).toISOString(), + }), getLatestAsk: vi.fn(), getLatestBid: vi.fn(), getLatestSpread: vi.fn(), @@ -113,6 +117,38 @@ describe('MomentumStrategy', () => { expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000); }); + it('caps hold time to 2 min before market close', async () => { + // Market closes in 500ms, but holdTime is 1000ms + // So the strategy should sell after ~380ms (500 - 120 = 380ms safe close) + // rather than waiting for the full 1000ms holdTime + const closeTime = new Date(Date.now() + 500).toISOString(); + const alpaca = mockAlpaca({ + getLatestAsk: vi.fn() + .mockResolvedValueOnce(100) + .mockResolvedValueOnce(101), + buy: vi.fn().mockResolvedValue(50), + getClock: vi.fn().mockResolvedValue({ + is_open: true, + next_open: new Date().toISOString(), + next_close: closeTime, + }), + getLatestBid: vi.fn().mockResolvedValue(49.90), + sell: vi.fn().mockResolvedValue(49.90), + }); + + const strategy = new MomentumStrategy(fastConfig); + const promise = strategy.execute(alpaca, 5000); + // Advance enough for the capped deadline (close - 120_000 is in the past, + // so the loop should exit immediately without any polls) + await vi.advanceTimersByTimeAsync(500); + await promise; + + expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000); + // With close only 500ms away, safeClose = close - 120s is already past, + // so the loop body should never run (no bid checks) + expect(alpaca.getLatestBid).not.toHaveBeenCalled(); + }); + it('uses actual fill price for target calculation', async () => { const alpaca = mockAlpaca({ getLatestAsk: vi.fn() diff --git a/src/momentum-strategy.ts b/src/momentum-strategy.ts index b034e75..e3af955 100644 --- a/src/momentum-strategy.ts +++ b/src/momentum-strategy.ts @@ -37,7 +37,14 @@ export class MomentumStrategy implements Strategy { logger.info(`[${this.name}] entered ${symbol} at price ${entryPrice}`); const targetPrice = entryPrice * (1 + this.config.targetGain); - const deadline = Date.now() + this.config.holdTime; + let deadline = Date.now() + this.config.holdTime; + + const clock = await alpaca.getClock(); + const safeClose = new Date(clock.next_close).getTime() - 120_000; + if (safeClose < deadline) { + logger.info(`[${this.name}] capping hold time to 2 min before market close`); + deadline = safeClose; + } logger.debug(`[${this.name}] monitoring ${symbol} for target price ${targetPrice} or timeout at ${new Date(deadline).toISOString()}`); let reason: 'target' | 'timeout' = 'timeout';