update our alpaca calls to be separate bid and ask

This commit is contained in:
Jon
2026-02-03 13:58:42 -07:00
parent 8872803563
commit e32e30af47
11 changed files with 116 additions and 71 deletions

View File

@@ -7,7 +7,7 @@ function mockClient(overrides: Partial<AlpacaClient> = {}): AlpacaClient {
getAssets: vi.fn().mockResolvedValue([]), getAssets: vi.fn().mockResolvedValue([]),
getAsset: vi.fn().mockResolvedValue({ symbol: 'TQQQ', fractionable: true, status: 'active', asset_class: 'us_equity' }), getAsset: vi.fn().mockResolvedValue({ symbol: 'TQQQ', fractionable: true, status: 'active', asset_class: 'us_equity' }),
getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: '2025-01-01T14:30:00Z', next_close: '2025-01-01T21:00:00Z' }), getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: '2025-01-01T14:30:00Z', next_close: '2025-01-01T21:00:00Z' }),
getLatestQuote: vi.fn().mockResolvedValue({ ap: 50.00, bp: 49.90 }), getLatestQuote: vi.fn().mockResolvedValue({ AskPrice: 50.00, BidPrice: 49.90 }),
getLatestTrades: vi.fn().mockResolvedValue(new Map()), getLatestTrades: vi.fn().mockResolvedValue(new Map()),
...overrides, ...overrides,
}; };
@@ -41,11 +41,27 @@ describe('Alpaca', () => {
expect(client.getClock).toHaveBeenCalled(); expect(client.getClock).toHaveBeenCalled();
}); });
it('delegates getLatestQuote to the underlying client', async () => { it('getLatestAsk returns the ask price', async () => {
const client = mockClient(); const client = mockClient();
const alpaca = new Alpaca(false, client); const alpaca = new Alpaca(false, client);
const quote = await alpaca.getLatestQuote('TQQQ'); const ask = await alpaca.getLatestAsk('TQQQ');
expect(quote.ap).toBe(50.00); expect(ask).toBe(50.00);
expect(client.getLatestQuote).toHaveBeenCalledWith('TQQQ');
});
it('getLatestBid returns the bid price', async () => {
const client = mockClient();
const alpaca = new Alpaca(false, client);
const bid = await alpaca.getLatestBid('TQQQ');
expect(bid).toBe(49.90);
expect(client.getLatestQuote).toHaveBeenCalledWith('TQQQ');
});
it('getLatestSpread returns ask minus bid', async () => {
const client = mockClient();
const alpaca = new Alpaca(false, client);
const spread = await alpaca.getLatestSpread('TQQQ');
expect(spread).toBeCloseTo(0.10);
expect(client.getLatestQuote).toHaveBeenCalledWith('TQQQ'); expect(client.getLatestQuote).toHaveBeenCalledWith('TQQQ');
}); });

View File

@@ -32,8 +32,8 @@ export interface AlpacaClock {
} }
export interface AlpacaQuote { export interface AlpacaQuote {
ap: number; // ask price AskPrice: number;
bp: number; // bid price BidPrice: number;
} }
export interface AlpacaTrade { export interface AlpacaTrade {
@@ -83,8 +83,17 @@ export class Alpaca {
return this.alpaca.getClock(); return this.alpaca.getClock();
} }
public async getLatestQuote(symbol: string) { public async getLatestAsk(symbol: string): Promise<number> {
return this.alpaca.getLatestQuote(symbol); const quote = await this.alpaca.getLatestQuote(symbol);
return quote.AskPrice;
}
public async getLatestBid(symbol: string): Promise<number> {
const quote = await this.alpaca.getLatestQuote(symbol);
return quote.BidPrice;
}
public async getLatestSpread(symbol: string): Promise<number> {
const quote = await this.alpaca.getLatestQuote(symbol);
return quote.AskPrice - quote.BidPrice;
} }
public async getLatestTrades(symbols: string[]) { public async getLatestTrades(symbols: string[]) {
return this.alpaca.getLatestTrades(symbols); return this.alpaca.getLatestTrades(symbols);

View File

@@ -13,7 +13,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
next_open: new Date().toISOString(), next_open: new Date().toISOString(),
next_close: new Date().toISOString(), next_close: new Date().toISOString(),
}), }),
getLatestQuote: vi.fn(), getLatestAsk: vi.fn(),
getLatestBid: vi.fn(),
getLatestSpread: vi.fn(),
getLatestTrades: vi.fn(), getLatestTrades: vi.fn(),
...overrides, ...overrides,
} as unknown as Alpaca; } as unknown as Alpaca;

View File

@@ -1,7 +1,7 @@
import { Alpaca } from "./alpaca"; import { Alpaca } from "./alpaca";
import { Strategy } from "./strategy"; import { Strategy } from "./strategy";
import { Executor } from "./executor"; import { Executor } from "./executor";
import { waitForNextOpen } from "./trading"; import { isMarketOpen, waitForNextOpen } from "./trading";
export class Bot { export class Bot {
private alpaca: Alpaca; private alpaca: Alpaca;
@@ -21,15 +21,18 @@ export class Bot {
} }
async runDay(): Promise<void> { async runDay(): Promise<void> {
console.log('waiting for open'); const open = await isMarketOpen(this.alpaca);
await waitForNextOpen(this.alpaca); if (!open) {
console.log('waiting for open');
const account = await this.alpaca.getAccount(); await waitForNextOpen(this.alpaca);
const totalCapital = parseFloat(account.cash); }
const account = await this.alpaca.getAccount();
const totalCapital = parseFloat(account.cash);
await Promise.all( await Promise.all(
this.strategies.map(async (strategy) => { this.strategies.map(async (strategy) => {
const signals = await strategy.execute(this.alpaca); const signals = await strategy.execute(this.alpaca);
await this.executor.executeSignals(strategy, signals, totalCapital); await this.executor.executeSignals(strategy, signals, totalCapital);
}) })
); );

View File

@@ -9,7 +9,9 @@ function mockAlpaca(): Alpaca {
getAssets: vi.fn(), getAssets: vi.fn(),
getAsset: vi.fn(), getAsset: vi.fn(),
getClock: vi.fn(), getClock: vi.fn(),
getLatestQuote: vi.fn(), getLatestAsk: vi.fn(),
getLatestBid: vi.fn(),
getLatestSpread: vi.fn(),
getLatestTrades: vi.fn(), getLatestTrades: vi.fn(),
} as unknown as Alpaca; } as unknown as Alpaca;
} }

View File

@@ -8,7 +8,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
getAssets: vi.fn(), getAssets: vi.fn(),
getAsset: vi.fn(), getAsset: vi.fn(),
getClock: vi.fn(), getClock: vi.fn(),
getLatestQuote: vi.fn(), getLatestAsk: vi.fn(),
getLatestBid: vi.fn(),
getLatestSpread: vi.fn(),
getLatestTrades: vi.fn(), getLatestTrades: vi.fn(),
...overrides, ...overrides,
} as unknown as Alpaca; } as unknown as Alpaca;
@@ -26,9 +28,9 @@ afterEach(() => {
describe('MomentumIndicator', () => { describe('MomentumIndicator', () => {
it('returns up when second quote is higher', async () => { it('returns up when second quote is higher', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
.mockResolvedValueOnce({ ap: 101, bp: 100 }), .mockResolvedValueOnce(101),
}); });
const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 }); const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 });
@@ -43,9 +45,9 @@ describe('MomentumIndicator', () => {
it('returns down when second quote is lower', async () => { it('returns down when second quote is lower', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
.mockResolvedValueOnce({ ap: 99, bp: 98 }), .mockResolvedValueOnce(99),
}); });
const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 }); const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 });
@@ -60,9 +62,9 @@ describe('MomentumIndicator', () => {
it('returns up when prices are equal', async () => { it('returns up when prices are equal', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
.mockResolvedValueOnce({ ap: 100, bp: 99 }), .mockResolvedValueOnce(100),
}); });
const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 }); const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 });
@@ -74,17 +76,17 @@ describe('MomentumIndicator', () => {
}); });
it('uses configured symbol', async () => { it('uses configured symbol', async () => {
const getLatestQuote = vi.fn() const getLatestAsk = vi.fn()
.mockResolvedValueOnce({ ap: 50, bp: 49 }) .mockResolvedValueOnce(50)
.mockResolvedValueOnce({ ap: 51, bp: 50 }); .mockResolvedValueOnce(51);
const alpaca = mockAlpaca({ getLatestQuote }); const alpaca = mockAlpaca({ getLatestAsk });
const indicator = new MomentumIndicator({ symbol: 'SPY', settleDelay: 0, sampleDelay: 100 }); const indicator = new MomentumIndicator({ symbol: 'SPY', settleDelay: 0, sampleDelay: 100 });
const promise = indicator.evaluate(alpaca); const promise = indicator.evaluate(alpaca);
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
await promise; await promise;
expect(getLatestQuote).toHaveBeenCalledWith('SPY'); expect(getLatestAsk).toHaveBeenCalledWith('SPY');
expect(getLatestQuote).toHaveBeenCalledTimes(2); expect(getLatestAsk).toHaveBeenCalledTimes(2);
}); });
}); });

View File

@@ -31,13 +31,11 @@ export class MomentumIndicator implements Indicator<MomentumResult> {
async evaluate(alpaca: Alpaca): Promise<MomentumResult> { async evaluate(alpaca: Alpaca): Promise<MomentumResult> {
await wait(this.config.settleDelay); await wait(this.config.settleDelay);
const before = await alpaca.getLatestQuote(this.config.symbol); const priceBefore = await alpaca.getLatestAsk(this.config.symbol);
const priceBefore = before.ap;
await wait(this.config.sampleDelay); await wait(this.config.sampleDelay);
const after = await alpaca.getLatestQuote(this.config.symbol); const priceAfter = await alpaca.getLatestAsk(this.config.symbol);
const priceAfter = after.ap;
const direction = priceAfter >= priceBefore ? 'up' : 'down'; const direction = priceAfter >= priceBefore ? 'up' : 'down';

View File

@@ -8,7 +8,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
getAssets: vi.fn(), getAssets: vi.fn(),
getAsset: vi.fn(), getAsset: vi.fn(),
getClock: vi.fn(), getClock: vi.fn(),
getLatestQuote: vi.fn(), getLatestAsk: vi.fn(),
getLatestBid: vi.fn(),
getLatestSpread: vi.fn(),
getLatestTrades: vi.fn(), getLatestTrades: vi.fn(),
...overrides, ...overrides,
} as unknown as Alpaca; } as unknown as Alpaca;
@@ -34,15 +36,16 @@ afterEach(() => {
describe('MomentumStrategy', () => { describe('MomentumStrategy', () => {
it('buys TQQQ when QQQ goes up', async () => { it('buys TQQQ when QQQ goes up', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
// indicator: QQQ before // indicator: QQQ before
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
// indicator: QQQ after (up) // indicator: QQQ after (up)
.mockResolvedValueOnce({ ap: 101, bp: 100 }) .mockResolvedValueOnce(101)
// entry quote for TQQQ // entry quote for TQQQ
.mockResolvedValueOnce({ ap: 50, bp: 49 }) .mockResolvedValueOnce(50),
getLatestBid: vi.fn()
// poll: hit target immediately // poll: hit target immediately
.mockResolvedValueOnce({ ap: 51, bp: 50.50 }), .mockResolvedValueOnce(50.50),
}); });
const strategy = new MomentumStrategy(1.0, fastConfig); const strategy = new MomentumStrategy(1.0, fastConfig);
@@ -55,15 +58,16 @@ describe('MomentumStrategy', () => {
it('buys SQQQ when QQQ goes down', async () => { it('buys SQQQ when QQQ goes down', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
// indicator: QQQ before // indicator: QQQ before
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
// indicator: QQQ after (down) // indicator: QQQ after (down)
.mockResolvedValueOnce({ ap: 99, bp: 98 }) .mockResolvedValueOnce(99)
// entry quote for SQQQ // entry quote for SQQQ
.mockResolvedValueOnce({ ap: 30, bp: 29 }) .mockResolvedValueOnce(30),
getLatestBid: vi.fn()
// poll: hit target immediately // poll: hit target immediately
.mockResolvedValueOnce({ ap: 31, bp: 30.30 }), .mockResolvedValueOnce(30.30),
}); });
const strategy = new MomentumStrategy(1.0, fastConfig); const strategy = new MomentumStrategy(1.0, fastConfig);
@@ -76,16 +80,17 @@ describe('MomentumStrategy', () => {
it('sells when bid hits 1% target', async () => { it('sells when bid hits 1% target', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
// indicator: QQQ samples // indicator: QQQ samples
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
.mockResolvedValueOnce({ ap: 101, bp: 100 }) .mockResolvedValueOnce(101)
// entry quote: ask = 50 // entry quote: ask = 50
.mockResolvedValueOnce({ ap: 50, bp: 49 }) .mockResolvedValueOnce(50),
getLatestBid: vi.fn()
// poll 1: not yet (target = 50.50) // poll 1: not yet (target = 50.50)
.mockResolvedValueOnce({ ap: 50.20, bp: 50.10 }) .mockResolvedValueOnce(50.10)
// poll 2: hit target // poll 2: hit target
.mockResolvedValueOnce({ ap: 51, bp: 50.50 }), .mockResolvedValueOnce(50.50),
}); });
const strategy = new MomentumStrategy(1.0, fastConfig); const strategy = new MomentumStrategy(1.0, fastConfig);
@@ -99,14 +104,15 @@ describe('MomentumStrategy', () => {
it('sells on timeout when target not reached', async () => { it('sells on timeout when target not reached', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
// indicator: QQQ samples // indicator: QQQ samples
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
.mockResolvedValueOnce({ ap: 101, bp: 100 }) .mockResolvedValueOnce(101)
// entry quote: ask = 50 // entry quote: ask = 50
.mockResolvedValueOnce({ ap: 50, bp: 49 }) .mockResolvedValueOnce(50),
getLatestBid: vi.fn()
// all polls: never reach target // all polls: never reach target
.mockResolvedValue({ ap: 50.10, bp: 49.90 }), .mockResolvedValue(49.90),
}); });
const strategy = new MomentumStrategy(1.0, fastConfig); const strategy = new MomentumStrategy(1.0, fastConfig);
@@ -120,11 +126,12 @@ describe('MomentumStrategy', () => {
it('returns both buy and sell signals', async () => { it('returns both buy and sell signals', async () => {
const alpaca = mockAlpaca({ const alpaca = mockAlpaca({
getLatestQuote: vi.fn() getLatestAsk: vi.fn()
.mockResolvedValueOnce({ ap: 100, bp: 99 }) .mockResolvedValueOnce(100)
.mockResolvedValueOnce({ ap: 101, bp: 100 }) .mockResolvedValueOnce(101)
.mockResolvedValueOnce({ ap: 50, bp: 49 }) .mockResolvedValueOnce(50),
.mockResolvedValueOnce({ ap: 51, bp: 50.50 }), getLatestBid: vi.fn()
.mockResolvedValueOnce(50.50),
}); });
const strategy = new MomentumStrategy(1.0, fastConfig); const strategy = new MomentumStrategy(1.0, fastConfig);

View File

@@ -32,8 +32,7 @@ export class MomentumStrategy implements Strategy {
const result = await this.indicator.evaluate(alpaca); const result = await this.indicator.evaluate(alpaca);
const symbol = result.direction === 'up' ? 'TQQQ' : 'SQQQ'; const symbol = result.direction === 'up' ? 'TQQQ' : 'SQQQ';
const entryQuote = await alpaca.getLatestQuote(symbol); const entryPrice = await alpaca.getLatestAsk(symbol);
const entryPrice = entryQuote.ap;
const buy: Signal = { symbol, direction: 'buy', allocation: 1.0 }; const buy: Signal = { symbol, direction: 'buy', allocation: 1.0 };
@@ -45,8 +44,8 @@ export class MomentumStrategy implements Strategy {
while (Date.now() < deadline) { while (Date.now() < deadline) {
await wait(this.config.pollInterval); await wait(this.config.pollInterval);
const quote = await alpaca.getLatestQuote(symbol); const bid = await alpaca.getLatestBid(symbol);
if (quote.bp >= targetPrice) { if (bid >= targetPrice) {
reason = 'target'; reason = 'target';
break; break;
} }

View File

@@ -8,7 +8,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
getAssets: vi.fn(), getAssets: vi.fn(),
getAsset: vi.fn(), getAsset: vi.fn(),
getClock: vi.fn(), getClock: vi.fn(),
getLatestQuote: vi.fn(), getLatestAsk: vi.fn(),
getLatestBid: vi.fn(),
getLatestSpread: vi.fn(),
getLatestTrades: vi.fn(), getLatestTrades: vi.fn(),
...overrides, ...overrides,
} as unknown as Alpaca; } as unknown as Alpaca;

View File

@@ -19,6 +19,11 @@ export async function accountBalance(alpaca: Alpaca) {
return account.cash; return account.cash;
} }
export async function isMarketOpen(alpaca: Alpaca): Promise<boolean> {
const clock = await alpaca.getClock();
return clock.is_open;
}
export async function waitForNextOpen(alpaca: Alpaca) { export async function waitForNextOpen(alpaca: Alpaca) {
const clock = await alpaca.getClock(); const clock = await alpaca.getClock();
return wait(new Date(clock.next_open).valueOf() - new Date().valueOf()); return wait(new Date(clock.next_open).valueOf() - new Date().valueOf());