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