Files
fit/src/trading.test.ts
Jon 4ca7073d77 Sell by qty instead of notional, poll for buy fill
The buy order can return before filling, giving null price/qty. Now
buy polls getOrder until filled. Sell takes the actual qty from the
buy fill instead of the original dollar amount, which avoids the
"insufficient qty" error when the price moves between buy and sell.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 12:37:12 -07:00

129 lines
4.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { printAsset, accountBalance, waitForNextOpen, wait, setWaitFn, resetWaitFn } from './trading';
import type { Alpaca } from './alpaca';
function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
return {
getAccount: vi.fn(),
getAssets: vi.fn(),
getAsset: vi.fn(),
getClock: vi.fn(),
getLatestAsk: vi.fn(),
getLatestBid: vi.fn(),
getLatestSpread: vi.fn(),
getLatestTrades: vi.fn(),
buy: vi.fn(),
sell: vi.fn(),
getOrder: vi.fn(),
...overrides,
} as unknown as Alpaca;
}
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
resetWaitFn();
});
describe('printAsset', () => {
beforeEach(() => {
vi.spyOn(console, 'debug').mockImplementation(() => {});
});
it('logs fractional when asset is fractionable', async () => {
const alpaca = mockAlpaca({
getAsset: vi.fn().mockResolvedValue({ symbol: 'TQQQ', fractionable: true, status: 'active', asset_class: 'us_equity' }),
});
await printAsset(alpaca, 'TQQQ');
expect(console.debug).toHaveBeenCalledWith(expect.stringContaining('[DEBUG]'), 'TQQQ is fractional');
});
it('logs not fractional when asset is not fractionable', async () => {
const alpaca = mockAlpaca({
getAsset: vi.fn().mockResolvedValue({ symbol: 'BRK.A', fractionable: false, status: 'active', asset_class: 'us_equity' }),
});
await printAsset(alpaca, 'BRK.A');
expect(console.debug).toHaveBeenCalledWith(expect.stringContaining('[DEBUG]'), 'BRK.A is not fractional');
});
});
describe('accountBalance', () => {
it('returns the cash value from the account', async () => {
const alpaca = mockAlpaca({
getAccount: vi.fn().mockResolvedValue({ cash: '10000.00' }),
});
const result = await accountBalance(alpaca);
expect(result).toBe('10000.00');
});
});
describe('waitForNextOpen', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'debug').mockImplementation(() => {});
});
it('polls until market opens', async () => {
const futureDate = new Date(Date.now() + 60000).toISOString();
const alpaca = mockAlpaca({
getClock: vi.fn()
.mockResolvedValueOnce({ is_open: false, next_open: futureDate, next_close: futureDate })
.mockResolvedValueOnce({ is_open: false, next_open: futureDate, next_close: futureDate })
.mockResolvedValueOnce({ is_open: true, next_open: futureDate, next_close: futureDate }),
});
const promise = waitForNextOpen(alpaca, 100);
await vi.advanceTimersByTimeAsync(300);
await promise;
expect(alpaca.getClock).toHaveBeenCalledTimes(3);
});
it('sleeps at most pollInterval even when open is far away', async () => {
const farFuture = new Date(Date.now() + 86_400_000).toISOString();
const alpaca = mockAlpaca({
getClock: vi.fn()
.mockResolvedValueOnce({ is_open: false, next_open: farFuture, next_close: farFuture })
.mockResolvedValueOnce({ is_open: true, next_open: farFuture, next_close: farFuture }),
});
const promise = waitForNextOpen(alpaca, 200);
await vi.advanceTimersByTimeAsync(300);
await promise;
// Should have polled twice: first sleep capped to 200ms, then saw is_open
expect(alpaca.getClock).toHaveBeenCalledTimes(2);
});
});
describe('setWaitFn / resetWaitFn', () => {
it('uses custom wait function when set', async () => {
const calls: number[] = [];
setWaitFn(async (ms) => { calls.push(ms); });
await wait(5000);
await wait(3000);
expect(calls).toEqual([5000, 3000]);
});
it('restores default wait after resetWaitFn', async () => {
setWaitFn(async () => {});
resetWaitFn();
// After reset, wait should use real setTimeout again
const promise = wait(1000);
await vi.advanceTimersByTimeAsync(1000);
await promise;
// If we got here without hanging, default wait is restored
});
});