Cap strategy hold time to 2 min before market close

Prevents sell orders from being rejected after market close by fetching
the market clock after entry and capping the hold deadline to next_close
minus a 2-minute buffer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-02-10 15:37:19 -07:00
parent 4f1e745534
commit 23e5437402
2 changed files with 45 additions and 2 deletions

View File

@@ -7,7 +7,11 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): 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()

View File

@@ -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';