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 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-02-13 10:46:20 -07:00
parent 019b630d95
commit 4c2ed5455f
2 changed files with 32 additions and 28 deletions

View File

@@ -153,17 +153,18 @@ export class Alpaca {
}
private async waitForFill(orderId: string): Promise<AlpacaOrder> {
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<OrderFill> {

View File

@@ -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);
}
}