update our alpaca calls to be separate bid and ask
This commit is contained in:
@@ -7,7 +7,7 @@ function mockClient(overrides: Partial<AlpacaClient> = {}): AlpacaClient {
|
||||
getAssets: vi.fn().mockResolvedValue([]),
|
||||
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' }),
|
||||
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()),
|
||||
...overrides,
|
||||
};
|
||||
@@ -41,11 +41,27 @@ describe('Alpaca', () => {
|
||||
expect(client.getClock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('delegates getLatestQuote to the underlying client', async () => {
|
||||
it('getLatestAsk returns the ask price', async () => {
|
||||
const client = mockClient();
|
||||
const alpaca = new Alpaca(false, client);
|
||||
const quote = await alpaca.getLatestQuote('TQQQ');
|
||||
expect(quote.ap).toBe(50.00);
|
||||
const ask = await alpaca.getLatestAsk('TQQQ');
|
||||
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');
|
||||
});
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ export interface AlpacaClock {
|
||||
}
|
||||
|
||||
export interface AlpacaQuote {
|
||||
ap: number; // ask price
|
||||
bp: number; // bid price
|
||||
AskPrice: number;
|
||||
BidPrice: number;
|
||||
}
|
||||
|
||||
export interface AlpacaTrade {
|
||||
@@ -83,8 +83,17 @@ export class Alpaca {
|
||||
return this.alpaca.getClock();
|
||||
}
|
||||
|
||||
public async getLatestQuote(symbol: string) {
|
||||
return this.alpaca.getLatestQuote(symbol);
|
||||
public async getLatestAsk(symbol: string): Promise<number> {
|
||||
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[]) {
|
||||
return this.alpaca.getLatestTrades(symbols);
|
||||
|
||||
@@ -13,7 +13,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
||||
next_open: new Date().toISOString(),
|
||||
next_close: new Date().toISOString(),
|
||||
}),
|
||||
getLatestQuote: vi.fn(),
|
||||
getLatestAsk: vi.fn(),
|
||||
getLatestBid: vi.fn(),
|
||||
getLatestSpread: vi.fn(),
|
||||
getLatestTrades: vi.fn(),
|
||||
...overrides,
|
||||
} as unknown as Alpaca;
|
||||
|
||||
17
src/bot.ts
17
src/bot.ts
@@ -1,7 +1,7 @@
|
||||
import { Alpaca } from "./alpaca";
|
||||
import { Strategy } from "./strategy";
|
||||
import { Executor } from "./executor";
|
||||
import { waitForNextOpen } from "./trading";
|
||||
import { isMarketOpen, waitForNextOpen } from "./trading";
|
||||
|
||||
export class Bot {
|
||||
private alpaca: Alpaca;
|
||||
@@ -21,15 +21,18 @@ export class Bot {
|
||||
}
|
||||
|
||||
async runDay(): Promise<void> {
|
||||
console.log('waiting for open');
|
||||
await waitForNextOpen(this.alpaca);
|
||||
|
||||
const account = await this.alpaca.getAccount();
|
||||
const totalCapital = parseFloat(account.cash);
|
||||
|
||||
const open = await isMarketOpen(this.alpaca);
|
||||
if (!open) {
|
||||
console.log('waiting for open');
|
||||
await waitForNextOpen(this.alpaca);
|
||||
}
|
||||
const account = await this.alpaca.getAccount();
|
||||
const totalCapital = parseFloat(account.cash);
|
||||
|
||||
await Promise.all(
|
||||
this.strategies.map(async (strategy) => {
|
||||
const signals = await strategy.execute(this.alpaca);
|
||||
|
||||
await this.executor.executeSignals(strategy, signals, totalCapital);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -9,7 +9,9 @@ function mockAlpaca(): Alpaca {
|
||||
getAssets: vi.fn(),
|
||||
getAsset: vi.fn(),
|
||||
getClock: vi.fn(),
|
||||
getLatestQuote: vi.fn(),
|
||||
getLatestAsk: vi.fn(),
|
||||
getLatestBid: vi.fn(),
|
||||
getLatestSpread: vi.fn(),
|
||||
getLatestTrades: vi.fn(),
|
||||
} as unknown as Alpaca;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
||||
getAssets: vi.fn(),
|
||||
getAsset: vi.fn(),
|
||||
getClock: vi.fn(),
|
||||
getLatestQuote: vi.fn(),
|
||||
getLatestAsk: vi.fn(),
|
||||
getLatestBid: vi.fn(),
|
||||
getLatestSpread: vi.fn(),
|
||||
getLatestTrades: vi.fn(),
|
||||
...overrides,
|
||||
} as unknown as Alpaca;
|
||||
@@ -26,9 +28,9 @@ afterEach(() => {
|
||||
describe('MomentumIndicator', () => {
|
||||
it('returns up when second quote is higher', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce({ ap: 101, bp: 100 }),
|
||||
getLatestAsk: vi.fn()
|
||||
.mockResolvedValueOnce(100)
|
||||
.mockResolvedValueOnce(101),
|
||||
});
|
||||
|
||||
const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 });
|
||||
@@ -43,9 +45,9 @@ describe('MomentumIndicator', () => {
|
||||
|
||||
it('returns down when second quote is lower', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce({ ap: 99, bp: 98 }),
|
||||
getLatestAsk: vi.fn()
|
||||
.mockResolvedValueOnce(100)
|
||||
.mockResolvedValueOnce(99),
|
||||
});
|
||||
|
||||
const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 });
|
||||
@@ -60,9 +62,9 @@ describe('MomentumIndicator', () => {
|
||||
|
||||
it('returns up when prices are equal', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 }),
|
||||
getLatestAsk: vi.fn()
|
||||
.mockResolvedValueOnce(100)
|
||||
.mockResolvedValueOnce(100),
|
||||
});
|
||||
|
||||
const indicator = new MomentumIndicator({ settleDelay: 0, sampleDelay: 100 });
|
||||
@@ -74,17 +76,17 @@ describe('MomentumIndicator', () => {
|
||||
});
|
||||
|
||||
it('uses configured symbol', async () => {
|
||||
const getLatestQuote = vi.fn()
|
||||
.mockResolvedValueOnce({ ap: 50, bp: 49 })
|
||||
.mockResolvedValueOnce({ ap: 51, bp: 50 });
|
||||
const alpaca = mockAlpaca({ getLatestQuote });
|
||||
const getLatestAsk = vi.fn()
|
||||
.mockResolvedValueOnce(50)
|
||||
.mockResolvedValueOnce(51);
|
||||
const alpaca = mockAlpaca({ getLatestAsk });
|
||||
|
||||
const indicator = new MomentumIndicator({ symbol: 'SPY', settleDelay: 0, sampleDelay: 100 });
|
||||
const promise = indicator.evaluate(alpaca);
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
await promise;
|
||||
|
||||
expect(getLatestQuote).toHaveBeenCalledWith('SPY');
|
||||
expect(getLatestQuote).toHaveBeenCalledTimes(2);
|
||||
expect(getLatestAsk).toHaveBeenCalledWith('SPY');
|
||||
expect(getLatestAsk).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,14 +31,12 @@ export class MomentumIndicator implements Indicator<MomentumResult> {
|
||||
async evaluate(alpaca: Alpaca): Promise<MomentumResult> {
|
||||
await wait(this.config.settleDelay);
|
||||
|
||||
const before = await alpaca.getLatestQuote(this.config.symbol);
|
||||
const priceBefore = before.ap;
|
||||
|
||||
const priceBefore = await alpaca.getLatestAsk(this.config.symbol);
|
||||
|
||||
await wait(this.config.sampleDelay);
|
||||
|
||||
const after = await alpaca.getLatestQuote(this.config.symbol);
|
||||
const priceAfter = after.ap;
|
||||
|
||||
const priceAfter = await alpaca.getLatestAsk(this.config.symbol);
|
||||
|
||||
const direction = priceAfter >= priceBefore ? 'up' : 'down';
|
||||
|
||||
return { direction, priceBefore, priceAfter };
|
||||
|
||||
@@ -8,7 +8,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
||||
getAssets: vi.fn(),
|
||||
getAsset: vi.fn(),
|
||||
getClock: vi.fn(),
|
||||
getLatestQuote: vi.fn(),
|
||||
getLatestAsk: vi.fn(),
|
||||
getLatestBid: vi.fn(),
|
||||
getLatestSpread: vi.fn(),
|
||||
getLatestTrades: vi.fn(),
|
||||
...overrides,
|
||||
} as unknown as Alpaca;
|
||||
@@ -34,15 +36,16 @@ afterEach(() => {
|
||||
describe('MomentumStrategy', () => {
|
||||
it('buys TQQQ when QQQ goes up', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
getLatestAsk: vi.fn()
|
||||
// indicator: QQQ before
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce(100)
|
||||
// indicator: QQQ after (up)
|
||||
.mockResolvedValueOnce({ ap: 101, bp: 100 })
|
||||
.mockResolvedValueOnce(101)
|
||||
// entry quote for TQQQ
|
||||
.mockResolvedValueOnce({ ap: 50, bp: 49 })
|
||||
.mockResolvedValueOnce(50),
|
||||
getLatestBid: vi.fn()
|
||||
// poll: hit target immediately
|
||||
.mockResolvedValueOnce({ ap: 51, bp: 50.50 }),
|
||||
.mockResolvedValueOnce(50.50),
|
||||
});
|
||||
|
||||
const strategy = new MomentumStrategy(1.0, fastConfig);
|
||||
@@ -55,15 +58,16 @@ describe('MomentumStrategy', () => {
|
||||
|
||||
it('buys SQQQ when QQQ goes down', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
getLatestAsk: vi.fn()
|
||||
// indicator: QQQ before
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce(100)
|
||||
// indicator: QQQ after (down)
|
||||
.mockResolvedValueOnce({ ap: 99, bp: 98 })
|
||||
.mockResolvedValueOnce(99)
|
||||
// entry quote for SQQQ
|
||||
.mockResolvedValueOnce({ ap: 30, bp: 29 })
|
||||
.mockResolvedValueOnce(30),
|
||||
getLatestBid: vi.fn()
|
||||
// poll: hit target immediately
|
||||
.mockResolvedValueOnce({ ap: 31, bp: 30.30 }),
|
||||
.mockResolvedValueOnce(30.30),
|
||||
});
|
||||
|
||||
const strategy = new MomentumStrategy(1.0, fastConfig);
|
||||
@@ -76,16 +80,17 @@ describe('MomentumStrategy', () => {
|
||||
|
||||
it('sells when bid hits 1% target', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
getLatestAsk: vi.fn()
|
||||
// indicator: QQQ samples
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce({ ap: 101, bp: 100 })
|
||||
.mockResolvedValueOnce(100)
|
||||
.mockResolvedValueOnce(101)
|
||||
// entry quote: ask = 50
|
||||
.mockResolvedValueOnce({ ap: 50, bp: 49 })
|
||||
.mockResolvedValueOnce(50),
|
||||
getLatestBid: vi.fn()
|
||||
// poll 1: not yet (target = 50.50)
|
||||
.mockResolvedValueOnce({ ap: 50.20, bp: 50.10 })
|
||||
.mockResolvedValueOnce(50.10)
|
||||
// poll 2: hit target
|
||||
.mockResolvedValueOnce({ ap: 51, bp: 50.50 }),
|
||||
.mockResolvedValueOnce(50.50),
|
||||
});
|
||||
|
||||
const strategy = new MomentumStrategy(1.0, fastConfig);
|
||||
@@ -99,14 +104,15 @@ describe('MomentumStrategy', () => {
|
||||
|
||||
it('sells on timeout when target not reached', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
getLatestAsk: vi.fn()
|
||||
// indicator: QQQ samples
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce({ ap: 101, bp: 100 })
|
||||
.mockResolvedValueOnce(100)
|
||||
.mockResolvedValueOnce(101)
|
||||
// entry quote: ask = 50
|
||||
.mockResolvedValueOnce({ ap: 50, bp: 49 })
|
||||
.mockResolvedValueOnce(50),
|
||||
getLatestBid: vi.fn()
|
||||
// all polls: never reach target
|
||||
.mockResolvedValue({ ap: 50.10, bp: 49.90 }),
|
||||
.mockResolvedValue(49.90),
|
||||
});
|
||||
|
||||
const strategy = new MomentumStrategy(1.0, fastConfig);
|
||||
@@ -120,11 +126,12 @@ describe('MomentumStrategy', () => {
|
||||
|
||||
it('returns both buy and sell signals', async () => {
|
||||
const alpaca = mockAlpaca({
|
||||
getLatestQuote: vi.fn()
|
||||
.mockResolvedValueOnce({ ap: 100, bp: 99 })
|
||||
.mockResolvedValueOnce({ ap: 101, bp: 100 })
|
||||
.mockResolvedValueOnce({ ap: 50, bp: 49 })
|
||||
.mockResolvedValueOnce({ ap: 51, bp: 50.50 }),
|
||||
getLatestAsk: vi.fn()
|
||||
.mockResolvedValueOnce(100)
|
||||
.mockResolvedValueOnce(101)
|
||||
.mockResolvedValueOnce(50),
|
||||
getLatestBid: vi.fn()
|
||||
.mockResolvedValueOnce(50.50),
|
||||
});
|
||||
|
||||
const strategy = new MomentumStrategy(1.0, fastConfig);
|
||||
|
||||
@@ -32,8 +32,7 @@ export class MomentumStrategy implements Strategy {
|
||||
const result = await this.indicator.evaluate(alpaca);
|
||||
const symbol = result.direction === 'up' ? 'TQQQ' : 'SQQQ';
|
||||
|
||||
const entryQuote = await alpaca.getLatestQuote(symbol);
|
||||
const entryPrice = entryQuote.ap;
|
||||
const entryPrice = await alpaca.getLatestAsk(symbol);
|
||||
|
||||
const buy: Signal = { symbol, direction: 'buy', allocation: 1.0 };
|
||||
|
||||
@@ -45,8 +44,8 @@ export class MomentumStrategy implements Strategy {
|
||||
while (Date.now() < deadline) {
|
||||
await wait(this.config.pollInterval);
|
||||
|
||||
const quote = await alpaca.getLatestQuote(symbol);
|
||||
if (quote.bp >= targetPrice) {
|
||||
const bid = await alpaca.getLatestBid(symbol);
|
||||
if (bid >= targetPrice) {
|
||||
reason = 'target';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
||||
getAssets: vi.fn(),
|
||||
getAsset: vi.fn(),
|
||||
getClock: vi.fn(),
|
||||
getLatestQuote: vi.fn(),
|
||||
getLatestAsk: vi.fn(),
|
||||
getLatestBid: vi.fn(),
|
||||
getLatestSpread: vi.fn(),
|
||||
getLatestTrades: vi.fn(),
|
||||
...overrides,
|
||||
} as unknown as Alpaca;
|
||||
|
||||
@@ -19,6 +19,11 @@ export async function accountBalance(alpaca: Alpaca) {
|
||||
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) {
|
||||
const clock = await alpaca.getClock();
|
||||
return wait(new Date(clock.next_open).valueOf() - new Date().valueOf());
|
||||
|
||||
Reference in New Issue
Block a user