From 4c2ed5455f4b9d09e089dbb438e2e8debb7c53ed Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 13 Feb 2026 10:46:20 -0700 Subject: [PATCH] Increase fill poll timeout to 2min, always sell after buy waitForFill now polls 60 times at 2s intervals (2 min total) instead of 30x1s, and uses retry on the getOrder calls. The strategy wraps the hold loop in try/finally so if anything fails after a buy, we still sell the position instead of leaving it orphaned. Co-Authored-By: Claude Opus 4.6 --- src/alpaca.ts | 9 +++---- src/momentum-strategy.ts | 51 +++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/alpaca.ts b/src/alpaca.ts index b7fe20b..344b21b 100644 --- a/src/alpaca.ts +++ b/src/alpaca.ts @@ -153,17 +153,18 @@ export class Alpaca { } private async waitForFill(orderId: string): Promise { - const maxAttempts = 30; + const maxAttempts = 60; + const pollInterval = 2000; for (let i = 0; i < maxAttempts; i++) { - const order = await this.alpaca.getOrder(orderId); + const order = await this.retry('getOrder', () => this.alpaca.getOrder(orderId)); if (order.status === 'filled') return order; if (order.status === 'canceled' || order.status === 'expired' || order.status === 'rejected') { throw new Error(`Order ${orderId} ${order.status}`); } logger.debug(`order ${orderId} status: ${order.status}, waiting...`); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, pollInterval)); } - throw new Error(`Order ${orderId} not filled after ${maxAttempts}s`); + throw new Error(`Order ${orderId} not filled after ${maxAttempts * pollInterval / 1000}s`); } public async buy(symbol: string, dollarAmount: number): Promise { diff --git a/src/momentum-strategy.ts b/src/momentum-strategy.ts index fc763f2..011bf99 100644 --- a/src/momentum-strategy.ts +++ b/src/momentum-strategy.ts @@ -36,32 +36,35 @@ export class MomentumStrategy implements Strategy { const fill = await alpaca.buy(symbol, capitalAmount); logger.info(`[${this.name}] entered ${symbol} at price ${fill.price}, qty ${fill.qty}`); - const targetPrice = fill.price * (1 + this.config.targetGain); - let deadline = Date.now() + this.config.holdTime; + try { + const targetPrice = fill.price * (1 + this.config.targetGain); + 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'; - - while (Date.now() < deadline) { - await wait(this.config.pollInterval); - - const bid = await alpaca.getLatestBid(symbol); - logger.debug(`${symbol} bid: ${bid} / target: ${targetPrice}`); - if (bid >= targetPrice) { - reason = 'target'; - break; + 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'; + + while (Date.now() < deadline) { + await wait(this.config.pollInterval); + + const bid = await alpaca.getLatestBid(symbol); + logger.debug(`${symbol} bid: ${bid} / target: ${targetPrice}`); + if (bid >= targetPrice) { + reason = 'target'; + break; + } + } + + logger.info(`[${this.name}] exit ${symbol} — reason: ${reason}`); + } finally { + logger.info(`[${this.name}] selling ${fill.qty} shares of ${symbol}`); + await alpaca.sell(symbol, fill.qty); } - - logger.info(`[${this.name}] exit ${symbol} — reason: ${reason}`); - - await alpaca.sell(symbol, fill.qty); } }