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:
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user