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(),
|
getAccount: vi.fn(),
|
||||||
getAssets: vi.fn(),
|
getAssets: vi.fn(),
|
||||||
getAsset: 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(),
|
getLatestAsk: vi.fn(),
|
||||||
getLatestBid: vi.fn(),
|
getLatestBid: vi.fn(),
|
||||||
getLatestSpread: vi.fn(),
|
getLatestSpread: vi.fn(),
|
||||||
@@ -113,6 +117,38 @@ describe('MomentumStrategy', () => {
|
|||||||
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000);
|
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 () => {
|
it('uses actual fill price for target calculation', async () => {
|
||||||
const alpaca = mockAlpaca({
|
const alpaca = mockAlpaca({
|
||||||
getLatestAsk: vi.fn()
|
getLatestAsk: vi.fn()
|
||||||
|
|||||||
@@ -37,7 +37,14 @@ export class MomentumStrategy implements Strategy {
|
|||||||
logger.info(`[${this.name}] entered ${symbol} at price ${entryPrice}`);
|
logger.info(`[${this.name}] entered ${symbol} at price ${entryPrice}`);
|
||||||
|
|
||||||
const targetPrice = entryPrice * (1 + this.config.targetGain);
|
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()}`);
|
logger.debug(`[${this.name}] monitoring ${symbol} for target price ${targetPrice} or timeout at ${new Date(deadline).toISOString()}`);
|
||||||
let reason: 'target' | 'timeout' = 'timeout';
|
let reason: 'target' | 'timeout' = 'timeout';
|
||||||
|
|||||||
Reference in New Issue
Block a user