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>
This commit is contained in:
@@ -9,6 +9,7 @@ function mockClient(overrides: Partial<AlpacaClient> = {}): AlpacaClient {
|
|||||||
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({ AskPrice: 50.00, BidPrice: 49.90 }),
|
getLatestQuote: vi.fn().mockResolvedValue({ AskPrice: 50.00, BidPrice: 49.90 }),
|
||||||
getLatestTrades: vi.fn().mockResolvedValue(new Map()),
|
getLatestTrades: vi.fn().mockResolvedValue(new Map()),
|
||||||
|
getOrder: vi.fn().mockResolvedValue({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: '50.25', filled_qty: '10', side: 'buy', status: 'filled' }),
|
||||||
createOrder: vi.fn().mockResolvedValue({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: '50.25', filled_qty: '10', side: 'buy', status: 'filled' }),
|
createOrder: vi.fn().mockResolvedValue({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: '50.25', filled_qty: '10', side: 'buy', status: 'filled' }),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@@ -89,11 +90,12 @@ describe('Alpaca', () => {
|
|||||||
expect(client.getLatestTrades).toHaveBeenCalledWith(['TQQQ', 'SPY']);
|
expect(client.getLatestTrades).toHaveBeenCalledWith(['TQQQ', 'SPY']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('buy places a market buy order and returns fill price', async () => {
|
it('buy places a market buy order and returns fill', async () => {
|
||||||
const client = mockClient();
|
const client = mockClient();
|
||||||
const alpaca = new Alpaca(false, client);
|
const alpaca = new Alpaca(false, client);
|
||||||
const price = await alpaca.buy('TQQQ', 5000);
|
const fill = await alpaca.buy('TQQQ', 5000);
|
||||||
expect(price).toBe(50.25);
|
expect(fill.price).toBe(50.25);
|
||||||
|
expect(fill.qty).toBe(10);
|
||||||
expect(client.createOrder).toHaveBeenCalledWith({
|
expect(client.createOrder).toHaveBeenCalledWith({
|
||||||
symbol: 'TQQQ',
|
symbol: 'TQQQ',
|
||||||
notional: 5000,
|
notional: 5000,
|
||||||
@@ -103,16 +105,31 @@ describe('Alpaca', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sell places a market sell order and returns fill price', async () => {
|
it('buy polls getOrder when createOrder returns unfilled', async () => {
|
||||||
|
const client = mockClient({
|
||||||
|
createOrder: vi.fn().mockResolvedValue({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: null, filled_qty: '0', side: 'buy', status: 'accepted' }),
|
||||||
|
getOrder: vi.fn()
|
||||||
|
.mockResolvedValueOnce({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: null, filled_qty: '0', side: 'buy', status: 'partially_filled' })
|
||||||
|
.mockResolvedValueOnce({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: '50.25', filled_qty: '10', side: 'buy', status: 'filled' }),
|
||||||
|
});
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
const fill = await alpaca.buy('TQQQ', 5000);
|
||||||
|
expect(fill.price).toBe(50.25);
|
||||||
|
expect(fill.qty).toBe(10);
|
||||||
|
expect(client.getOrder).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sell places a market sell order by qty and returns fill', async () => {
|
||||||
const client = mockClient({
|
const client = mockClient({
|
||||||
createOrder: vi.fn().mockResolvedValue({ id: 'order-2', symbol: 'TQQQ', filled_avg_price: '51.00', filled_qty: '10', side: 'sell', status: 'filled' }),
|
createOrder: vi.fn().mockResolvedValue({ id: 'order-2', symbol: 'TQQQ', filled_avg_price: '51.00', filled_qty: '10', side: 'sell', status: 'filled' }),
|
||||||
});
|
});
|
||||||
const alpaca = new Alpaca(false, client);
|
const alpaca = new Alpaca(false, client);
|
||||||
const price = await alpaca.sell('TQQQ', 5000);
|
const fill = await alpaca.sell('TQQQ', 10);
|
||||||
expect(price).toBe(51.00);
|
expect(fill.price).toBe(51.00);
|
||||||
|
expect(fill.qty).toBe(10);
|
||||||
expect(client.createOrder).toHaveBeenCalledWith({
|
expect(client.createOrder).toHaveBeenCalledWith({
|
||||||
symbol: 'TQQQ',
|
symbol: 'TQQQ',
|
||||||
notional: 5000,
|
qty: 10,
|
||||||
side: 'sell',
|
side: 'sell',
|
||||||
type: 'market',
|
type: 'market',
|
||||||
time_in_force: 'day',
|
time_in_force: 'day',
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ export interface AlpacaTrade {
|
|||||||
t: string; // timestamp
|
t: string; // timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrderFill {
|
||||||
|
price: number;
|
||||||
|
qty: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AlpacaClient {
|
export interface AlpacaClient {
|
||||||
getAccount(): Promise<AlpacaAccount>;
|
getAccount(): Promise<AlpacaAccount>;
|
||||||
getAssets(params: { status: string; asset_class: string }): Promise<AlpacaAsset[]>;
|
getAssets(params: { status: string; asset_class: string }): Promise<AlpacaAsset[]>;
|
||||||
@@ -60,9 +65,11 @@ export interface AlpacaClient {
|
|||||||
getClock(): Promise<AlpacaClock>;
|
getClock(): Promise<AlpacaClock>;
|
||||||
getLatestQuote(symbol: string): Promise<AlpacaQuote>;
|
getLatestQuote(symbol: string): Promise<AlpacaQuote>;
|
||||||
getLatestTrades(symbols: string[]): Promise<Map<string, AlpacaTrade>>;
|
getLatestTrades(symbols: string[]): Promise<Map<string, AlpacaTrade>>;
|
||||||
|
getOrder(id: string): Promise<AlpacaOrder>;
|
||||||
createOrder(order: {
|
createOrder(order: {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
notional: number;
|
notional?: number;
|
||||||
|
qty?: number;
|
||||||
side: 'buy' | 'sell';
|
side: 'buy' | 'sell';
|
||||||
type: string;
|
type: string;
|
||||||
time_in_force: string;
|
time_in_force: string;
|
||||||
@@ -121,7 +128,25 @@ export class Alpaca {
|
|||||||
return this.alpaca.getLatestTrades(symbols);
|
return this.alpaca.getLatestTrades(symbols);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async buy(symbol: string, dollarAmount: number): Promise<number> {
|
public async getOrder(id: string): Promise<AlpacaOrder> {
|
||||||
|
return this.alpaca.getOrder(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForFill(orderId: string): Promise<AlpacaOrder> {
|
||||||
|
const maxAttempts = 30;
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const order = await this.alpaca.getOrder(orderId);
|
||||||
|
if (order.status === 'filled') return order;
|
||||||
|
if (order.status === 'canceled' || order.status === 'expired' || order.status === 'rejected') {
|
||||||
|
throw new Error(`Order ${orderId} ${order.status}`);
|
||||||
|
}
|
||||||
|
logger.debug(`order ${orderId} status: ${order.status}, waiting...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
throw new Error(`Order ${orderId} not filled after ${maxAttempts}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buy(symbol: string, dollarAmount: number): Promise<OrderFill> {
|
||||||
logger.info(`buying ${symbol} for $${dollarAmount}`);
|
logger.info(`buying ${symbol} for $${dollarAmount}`);
|
||||||
const order = await this.alpaca.createOrder({
|
const order = await this.alpaca.createOrder({
|
||||||
symbol,
|
symbol,
|
||||||
@@ -130,20 +155,22 @@ export class Alpaca {
|
|||||||
type: 'market',
|
type: 'market',
|
||||||
time_in_force: 'day',
|
time_in_force: 'day',
|
||||||
});
|
});
|
||||||
logger.info(`bought ${symbol} — filled at ${order.filled_avg_price}, qty ${order.filled_qty}, order ${order.id}`);
|
const filled = order.status === 'filled' ? order : await this.waitForFill(order.id);
|
||||||
return parseFloat(order.filled_avg_price);
|
logger.info(`bought ${symbol} — filled at ${filled.filled_avg_price}, qty ${filled.filled_qty}, order ${filled.id}`);
|
||||||
|
return { price: parseFloat(filled.filled_avg_price), qty: parseFloat(filled.filled_qty) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sell(symbol: string, dollarAmount: number): Promise<number> {
|
public async sell(symbol: string, qty: number): Promise<OrderFill> {
|
||||||
logger.info(`selling ${symbol} for $${dollarAmount}`);
|
logger.info(`selling ${qty} shares of ${symbol}`);
|
||||||
const order = await this.alpaca.createOrder({
|
const order = await this.alpaca.createOrder({
|
||||||
symbol,
|
symbol,
|
||||||
notional: dollarAmount,
|
qty,
|
||||||
side: 'sell',
|
side: 'sell',
|
||||||
type: 'market',
|
type: 'market',
|
||||||
time_in_force: 'day',
|
time_in_force: 'day',
|
||||||
});
|
});
|
||||||
logger.info(`sold ${symbol} — filled at ${order.filled_avg_price}, qty ${order.filled_qty}, order ${order.id}`);
|
const filled = order.status === 'filled' ? order : await this.waitForFill(order.id);
|
||||||
return parseFloat(order.filled_avg_price);
|
logger.info(`sold ${symbol} — filled at ${filled.filled_avg_price}, qty ${filled.filled_qty}, order ${filled.id}`);
|
||||||
|
return { price: parseFloat(filled.filled_avg_price), qty: parseFloat(filled.filled_qty) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,9 +155,23 @@ export class BacktestClient implements AlpacaClient {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrder(id: string): Promise<AlpacaOrder> {
|
||||||
|
const fill = this._fills.find((_, i) => String(i + 1) === id);
|
||||||
|
if (!fill) throw new Error(`Order ${id} not found`);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
symbol: fill.symbol,
|
||||||
|
filled_avg_price: fill.price.toFixed(4),
|
||||||
|
filled_qty: fill.qty.toFixed(6),
|
||||||
|
side: fill.side,
|
||||||
|
status: "filled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async createOrder(order: {
|
async createOrder(order: {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
notional: number;
|
notional?: number;
|
||||||
|
qty?: number;
|
||||||
side: "buy" | "sell";
|
side: "buy" | "sell";
|
||||||
type: string;
|
type: string;
|
||||||
time_in_force: string;
|
time_in_force: string;
|
||||||
@@ -168,18 +182,19 @@ export class BacktestClient implements AlpacaClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const price = bar.ClosePrice;
|
const price = bar.ClosePrice;
|
||||||
const qty = order.notional / price;
|
const qty = order.qty ?? (order.notional! / price);
|
||||||
|
const notional = order.notional ?? (order.qty! * price);
|
||||||
const id = String(this.nextOrderId++);
|
const id = String(this.nextOrderId++);
|
||||||
|
|
||||||
if (order.side === "buy") {
|
if (order.side === "buy") {
|
||||||
this.cash -= order.notional;
|
this.cash -= notional;
|
||||||
const pos = this.positions.get(order.symbol) ?? { qty: 0, avgCost: 0 };
|
const pos = this.positions.get(order.symbol) ?? { qty: 0, avgCost: 0 };
|
||||||
const totalCost = pos.avgCost * pos.qty + price * qty;
|
const totalCost = pos.avgCost * pos.qty + price * qty;
|
||||||
pos.qty += qty;
|
pos.qty += qty;
|
||||||
pos.avgCost = pos.qty > 0 ? totalCost / pos.qty : 0;
|
pos.avgCost = pos.qty > 0 ? totalCost / pos.qty : 0;
|
||||||
this.positions.set(order.symbol, pos);
|
this.positions.set(order.symbol, pos);
|
||||||
} else {
|
} else {
|
||||||
this.cash += order.notional;
|
this.cash += notional;
|
||||||
const pos = this.positions.get(order.symbol);
|
const pos = this.positions.get(order.symbol);
|
||||||
if (pos) {
|
if (pos) {
|
||||||
pos.qty -= qty;
|
pos.qty -= qty;
|
||||||
@@ -194,7 +209,7 @@ export class BacktestClient implements AlpacaClient {
|
|||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
side: order.side,
|
side: order.side,
|
||||||
price,
|
price,
|
||||||
notional: order.notional,
|
notional,
|
||||||
qty,
|
qty,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
|||||||
getLatestTrades: vi.fn(),
|
getLatestTrades: vi.fn(),
|
||||||
buy: vi.fn(),
|
buy: vi.fn(),
|
||||||
sell: vi.fn(),
|
sell: vi.fn(),
|
||||||
|
getOrder: vi.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as Alpaca;
|
} as unknown as Alpaca;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
|||||||
getLatestBid: vi.fn(),
|
getLatestBid: vi.fn(),
|
||||||
getLatestSpread: vi.fn(),
|
getLatestSpread: vi.fn(),
|
||||||
getLatestTrades: vi.fn(),
|
getLatestTrades: vi.fn(),
|
||||||
buy: vi.fn().mockResolvedValue(50),
|
buy: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
sell: vi.fn().mockResolvedValue(50),
|
sell: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
|
getOrder: vi.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as Alpaca;
|
} as unknown as Alpaca;
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@ describe('MomentumStrategy', () => {
|
|||||||
getLatestAsk: vi.fn()
|
getLatestAsk: vi.fn()
|
||||||
.mockResolvedValueOnce(100)
|
.mockResolvedValueOnce(100)
|
||||||
.mockResolvedValueOnce(101),
|
.mockResolvedValueOnce(101),
|
||||||
buy: vi.fn().mockResolvedValue(50),
|
buy: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
getLatestBid: vi.fn().mockResolvedValue(50.50),
|
getLatestBid: vi.fn().mockResolvedValue(50.50),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ describe('MomentumStrategy', () => {
|
|||||||
getLatestAsk: vi.fn()
|
getLatestAsk: vi.fn()
|
||||||
.mockResolvedValueOnce(100)
|
.mockResolvedValueOnce(100)
|
||||||
.mockResolvedValueOnce(99),
|
.mockResolvedValueOnce(99),
|
||||||
buy: vi.fn().mockResolvedValue(30),
|
buy: vi.fn().mockResolvedValue({ price: 30, qty: 166.67 }),
|
||||||
getLatestBid: vi.fn().mockResolvedValue(30.30),
|
getLatestBid: vi.fn().mockResolvedValue(30.30),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,13 +81,13 @@ describe('MomentumStrategy', () => {
|
|||||||
getLatestAsk: vi.fn()
|
getLatestAsk: vi.fn()
|
||||||
.mockResolvedValueOnce(100)
|
.mockResolvedValueOnce(100)
|
||||||
.mockResolvedValueOnce(101),
|
.mockResolvedValueOnce(101),
|
||||||
buy: vi.fn().mockResolvedValue(50),
|
buy: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
getLatestBid: vi.fn()
|
getLatestBid: vi.fn()
|
||||||
// poll 1: not yet (target = 50.50)
|
// poll 1: not yet (target = 50.50)
|
||||||
.mockResolvedValueOnce(50.10)
|
.mockResolvedValueOnce(50.10)
|
||||||
// poll 2: hit target
|
// poll 2: hit target
|
||||||
.mockResolvedValueOnce(50.50),
|
.mockResolvedValueOnce(50.50),
|
||||||
sell: vi.fn().mockResolvedValue(50.50),
|
sell: vi.fn().mockResolvedValue({ price: 50.50, qty: 100 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const strategy = new MomentumStrategy(fastConfig);
|
const strategy = new MomentumStrategy(fastConfig);
|
||||||
@@ -95,7 +96,7 @@ describe('MomentumStrategy', () => {
|
|||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), '[momentum] exit TQQQ — reason: target');
|
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), '[momentum] exit TQQQ — reason: target');
|
||||||
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000);
|
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sells on timeout when target not reached', async () => {
|
it('sells on timeout when target not reached', async () => {
|
||||||
@@ -103,9 +104,9 @@ describe('MomentumStrategy', () => {
|
|||||||
getLatestAsk: vi.fn()
|
getLatestAsk: vi.fn()
|
||||||
.mockResolvedValueOnce(100)
|
.mockResolvedValueOnce(100)
|
||||||
.mockResolvedValueOnce(101),
|
.mockResolvedValueOnce(101),
|
||||||
buy: vi.fn().mockResolvedValue(50),
|
buy: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
getLatestBid: vi.fn().mockResolvedValue(49.90),
|
getLatestBid: vi.fn().mockResolvedValue(49.90),
|
||||||
sell: vi.fn().mockResolvedValue(49.90),
|
sell: vi.fn().mockResolvedValue({ price: 49.90, qty: 100 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const strategy = new MomentumStrategy(fastConfig);
|
const strategy = new MomentumStrategy(fastConfig);
|
||||||
@@ -114,7 +115,7 @@ describe('MomentumStrategy', () => {
|
|||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), '[momentum] exit TQQQ — reason: timeout');
|
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), '[momentum] exit TQQQ — reason: timeout');
|
||||||
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000);
|
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('caps hold time to 2 min before market close', async () => {
|
it('caps hold time to 2 min before market close', async () => {
|
||||||
@@ -126,14 +127,14 @@ describe('MomentumStrategy', () => {
|
|||||||
getLatestAsk: vi.fn()
|
getLatestAsk: vi.fn()
|
||||||
.mockResolvedValueOnce(100)
|
.mockResolvedValueOnce(100)
|
||||||
.mockResolvedValueOnce(101),
|
.mockResolvedValueOnce(101),
|
||||||
buy: vi.fn().mockResolvedValue(50),
|
buy: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
getClock: vi.fn().mockResolvedValue({
|
getClock: vi.fn().mockResolvedValue({
|
||||||
is_open: true,
|
is_open: true,
|
||||||
next_open: new Date().toISOString(),
|
next_open: new Date().toISOString(),
|
||||||
next_close: closeTime,
|
next_close: closeTime,
|
||||||
}),
|
}),
|
||||||
getLatestBid: vi.fn().mockResolvedValue(49.90),
|
getLatestBid: vi.fn().mockResolvedValue(49.90),
|
||||||
sell: vi.fn().mockResolvedValue(49.90),
|
sell: vi.fn().mockResolvedValue({ price: 49.90, qty: 100 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const strategy = new MomentumStrategy(fastConfig);
|
const strategy = new MomentumStrategy(fastConfig);
|
||||||
@@ -143,7 +144,7 @@ describe('MomentumStrategy', () => {
|
|||||||
await vi.advanceTimersByTimeAsync(500);
|
await vi.advanceTimersByTimeAsync(500);
|
||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000);
|
expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 100);
|
||||||
// With close only 500ms away, safeClose = close - 120s is already past,
|
// With close only 500ms away, safeClose = close - 120s is already past,
|
||||||
// so the loop body should never run (no bid checks)
|
// so the loop body should never run (no bid checks)
|
||||||
expect(alpaca.getLatestBid).not.toHaveBeenCalled();
|
expect(alpaca.getLatestBid).not.toHaveBeenCalled();
|
||||||
@@ -155,13 +156,13 @@ describe('MomentumStrategy', () => {
|
|||||||
.mockResolvedValueOnce(100)
|
.mockResolvedValueOnce(100)
|
||||||
.mockResolvedValueOnce(101),
|
.mockResolvedValueOnce(101),
|
||||||
// fill price is 50, so 1% target = 50.50
|
// fill price is 50, so 1% target = 50.50
|
||||||
buy: vi.fn().mockResolvedValue(50),
|
buy: vi.fn().mockResolvedValue({ price: 50, qty: 100 }),
|
||||||
getLatestBid: vi.fn()
|
getLatestBid: vi.fn()
|
||||||
// 50.49 is below target
|
// 50.49 is below target
|
||||||
.mockResolvedValueOnce(50.49)
|
.mockResolvedValueOnce(50.49)
|
||||||
// 50.50 hits target
|
// 50.50 hits target
|
||||||
.mockResolvedValueOnce(50.50),
|
.mockResolvedValueOnce(50.50),
|
||||||
sell: vi.fn().mockResolvedValue(50.50),
|
sell: vi.fn().mockResolvedValue({ price: 50.50, qty: 100 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const strategy = new MomentumStrategy(fastConfig);
|
const strategy = new MomentumStrategy(fastConfig);
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ export class MomentumStrategy implements Strategy {
|
|||||||
logger.debug(`[${this.name}] indicator result: ${JSON.stringify(result)}`);
|
logger.debug(`[${this.name}] indicator result: ${JSON.stringify(result)}`);
|
||||||
const symbol = result.direction === 'up' ? 'TQQQ' : 'SQQQ';
|
const symbol = result.direction === 'up' ? 'TQQQ' : 'SQQQ';
|
||||||
|
|
||||||
const entryPrice = await alpaca.buy(symbol, capitalAmount);
|
const fill = await alpaca.buy(symbol, capitalAmount);
|
||||||
logger.info(`[${this.name}] entered ${symbol} at price ${entryPrice}`);
|
logger.info(`[${this.name}] entered ${symbol} at price ${fill.price}, qty ${fill.qty}`);
|
||||||
|
|
||||||
const targetPrice = entryPrice * (1 + this.config.targetGain);
|
const targetPrice = fill.price * (1 + this.config.targetGain);
|
||||||
let deadline = Date.now() + this.config.holdTime;
|
let deadline = Date.now() + this.config.holdTime;
|
||||||
|
|
||||||
const clock = await alpaca.getClock();
|
const clock = await alpaca.getClock();
|
||||||
@@ -62,6 +62,6 @@ export class MomentumStrategy implements Strategy {
|
|||||||
|
|
||||||
logger.info(`[${this.name}] exit ${symbol} — reason: ${reason}`);
|
logger.info(`[${this.name}] exit ${symbol} — reason: ${reason}`);
|
||||||
|
|
||||||
await alpaca.sell(symbol, capitalAmount);
|
await alpaca.sell(symbol, fill.qty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
|
|||||||
getLatestTrades: vi.fn(),
|
getLatestTrades: vi.fn(),
|
||||||
buy: vi.fn(),
|
buy: vi.fn(),
|
||||||
sell: vi.fn(),
|
sell: vi.fn(),
|
||||||
|
getOrder: vi.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as Alpaca;
|
} as unknown as Alpaca;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user