From 075572a01c1276c429687fccef002483af1284f2 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 4 Feb 2026 11:05:21 -0700 Subject: [PATCH] Remove executor, add buy/sell to Alpaca, move capital allocation to Bot Replace the Executor logging placeholder with real buy/sell methods on the Alpaca class that place market orders via createOrder and return the fill price. Strategies now receive their capital amount directly and place orders themselves. Bot accepts StrategyAllocation[] to decouple capital allocation from strategy definition. Co-Authored-By: Claude Opus 4.5 --- src/alpaca.test.ts | 31 ++++++++++ src/alpaca.ts | 39 +++++++++++- src/bot.test.ts | 106 +++++++++++++++------------------ src/bot.ts | 29 ++++----- src/executor.test.ts | 83 -------------------------- src/executor.ts | 23 ------- src/index.ts | 4 +- src/momentum-indicator.test.ts | 2 + src/momentum-strategy.test.ts | 95 +++++++++++++---------------- src/momentum-strategy.ts | 16 ++--- src/strategy.ts | 11 +--- src/trading.test.ts | 2 + 12 files changed, 186 insertions(+), 255 deletions(-) delete mode 100644 src/executor.test.ts delete mode 100644 src/executor.ts diff --git a/src/alpaca.test.ts b/src/alpaca.test.ts index 86f44c2..9c3fbf4 100644 --- a/src/alpaca.test.ts +++ b/src/alpaca.test.ts @@ -9,6 +9,7 @@ function mockClient(overrides: Partial = {}): AlpacaClient { 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 }), getLatestTrades: vi.fn().mockResolvedValue(new Map()), + createOrder: vi.fn().mockResolvedValue({ id: 'order-1', symbol: 'TQQQ', filled_avg_price: '50.25', filled_qty: '10', side: 'buy', status: 'filled' }), ...overrides, }; } @@ -78,4 +79,34 @@ describe('Alpaca', () => { await alpaca.getLatestTrades(['TQQQ', 'SPY']); expect(client.getLatestTrades).toHaveBeenCalledWith(['TQQQ', 'SPY']); }); + + it('buy places a market buy order and returns fill price', async () => { + const client = mockClient(); + const alpaca = new Alpaca(false, client); + const price = await alpaca.buy('TQQQ', 5000); + expect(price).toBe(50.25); + expect(client.createOrder).toHaveBeenCalledWith({ + symbol: 'TQQQ', + notional: 5000, + side: 'buy', + type: 'market', + time_in_force: 'day', + }); + }); + + it('sell places a market sell order and returns fill price', async () => { + const client = mockClient({ + 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 price = await alpaca.sell('TQQQ', 5000); + expect(price).toBe(51.00); + expect(client.createOrder).toHaveBeenCalledWith({ + symbol: 'TQQQ', + notional: 5000, + side: 'sell', + type: 'market', + time_in_force: 'day', + }); + }); }); diff --git a/src/alpaca.ts b/src/alpaca.ts index c5b16d5..9485fbe 100644 --- a/src/alpaca.ts +++ b/src/alpaca.ts @@ -36,6 +36,15 @@ export interface AlpacaQuote { BidPrice: number; } +export interface AlpacaOrder { + id: string; + symbol: string; + filled_avg_price: string; + filled_qty: string; + side: string; + status: string; +} + export interface AlpacaTrade { p: number; // price s: number; // size @@ -49,6 +58,13 @@ export interface AlpacaClient { getClock(): Promise; getLatestQuote(symbol: string): Promise; getLatestTrades(symbols: string[]): Promise>; + createOrder(order: { + symbol: string; + notional: number; + side: 'buy' | 'sell'; + type: string; + time_in_force: string; + }): Promise; } export class Alpaca { @@ -98,5 +114,26 @@ export class Alpaca { public async getLatestTrades(symbols: string[]) { return this.alpaca.getLatestTrades(symbols); } - + + public async buy(symbol: string, dollarAmount: number): Promise { + const order = await this.alpaca.createOrder({ + symbol, + notional: dollarAmount, + side: 'buy', + type: 'market', + time_in_force: 'day', + }); + return parseFloat(order.filled_avg_price); + } + + public async sell(symbol: string, dollarAmount: number): Promise { + const order = await this.alpaca.createOrder({ + symbol, + notional: dollarAmount, + side: 'sell', + type: 'market', + time_in_force: 'day', + }); + return parseFloat(order.filled_avg_price); + } } \ No newline at end of file diff --git a/src/bot.test.ts b/src/bot.test.ts index 30d9d44..2c4bcef 100644 --- a/src/bot.test.ts +++ b/src/bot.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { Bot } from './bot'; +import { Bot, StrategyAllocation } from './bot'; import type { Alpaca } from './alpaca'; -import type { Strategy, Signal } from './strategy'; +import type { Strategy } from './strategy'; function mockAlpaca(overrides: Partial = {}): Alpaca { return { @@ -17,6 +17,8 @@ function mockAlpaca(overrides: Partial = {}): Alpaca { getLatestBid: vi.fn(), getLatestSpread: vi.fn(), getLatestTrades: vi.fn(), + buy: vi.fn(), + sell: vi.fn(), ...overrides, } as unknown as Alpaca; } @@ -24,8 +26,7 @@ function mockAlpaca(overrides: Partial = {}): Alpaca { function mockStrategy(overrides: Partial = {}): Strategy { return { name: 'test-strategy', - capitalAllocation: 0.5, - execute: vi.fn().mockResolvedValue([]), + execute: vi.fn().mockResolvedValue(undefined), ...overrides, }; } @@ -44,83 +45,53 @@ describe('Bot', () => { describe('constructor', () => { it('throws when capital allocations exceed 1.0', () => { const alpaca = mockAlpaca(); - const strategies = [ - mockStrategy({ capitalAllocation: 0.6 }), - mockStrategy({ capitalAllocation: 0.5 }), + const allocations: StrategyAllocation[] = [ + { strategy: mockStrategy(), capitalAllocation: 0.6 }, + { strategy: mockStrategy(), capitalAllocation: 0.5 }, ]; - expect(() => new Bot(alpaca, strategies)).toThrow( + expect(() => new Bot(alpaca, allocations)).toThrow( 'Capital allocations sum to 1.1, which exceeds 1.0' ); }); it('accepts allocations that sum to exactly 1.0', () => { const alpaca = mockAlpaca(); - const strategies = [ - mockStrategy({ capitalAllocation: 0.5 }), - mockStrategy({ capitalAllocation: 0.5 }), + const allocations: StrategyAllocation[] = [ + { strategy: mockStrategy(), capitalAllocation: 0.5 }, + { strategy: mockStrategy(), capitalAllocation: 0.5 }, ]; - expect(() => new Bot(alpaca, strategies)).not.toThrow(); + expect(() => new Bot(alpaca, allocations)).not.toThrow(); }); it('accepts allocations that sum to less than 1.0', () => { const alpaca = mockAlpaca(); - const strategies = [ - mockStrategy({ capitalAllocation: 0.3 }), - mockStrategy({ capitalAllocation: 0.2 }), + const allocations: StrategyAllocation[] = [ + { strategy: mockStrategy(), capitalAllocation: 0.3 }, + { strategy: mockStrategy(), capitalAllocation: 0.2 }, ]; - expect(() => new Bot(alpaca, strategies)).not.toThrow(); + expect(() => new Bot(alpaca, allocations)).not.toThrow(); }); }); describe('runDay', () => { - it('runs multiple strategies concurrently', async () => { + it('passes correct capital amount to each strategy', async () => { const alpaca = mockAlpaca(); - const strategyA = mockStrategy({ - name: 'A', - capitalAllocation: 0.3, - execute: vi.fn().mockResolvedValue([ - { symbol: 'TQQQ', direction: 'buy', allocation: 1.0 }, - ]), - }); - const strategyB = mockStrategy({ - name: 'B', - capitalAllocation: 0.2, - execute: vi.fn().mockResolvedValue([ - { symbol: 'SPY', direction: 'sell', allocation: 0.5 }, - ]), - }); - - const bot = new Bot(alpaca, [strategyA, strategyB]); - const promise = bot.runDay(); - await vi.advanceTimersByTimeAsync(0); - await promise; - - expect(strategyA.execute).toHaveBeenCalledWith(alpaca); - expect(strategyB.execute).toHaveBeenCalledWith(alpaca); - // A: 10000 * 0.3 * 1.0 = 3000 - expect(console.log).toHaveBeenCalledWith('[A] BUY TQQQ — $3000.00'); - // B: 10000 * 0.2 * 0.5 = 1000 - expect(console.log).toHaveBeenCalledWith('[B] SELL SPY — $1000.00'); - }); - - it('passes signals to executor', async () => { - const alpaca = mockAlpaca(); - const signals: Signal[] = [ - { symbol: 'TQQQ', direction: 'buy', allocation: 0.5 }, + const strategyA = mockStrategy({ name: 'A' }); + const strategyB = mockStrategy({ name: 'B' }); + const allocations: StrategyAllocation[] = [ + { strategy: strategyA, capitalAllocation: 0.3 }, + { strategy: strategyB, capitalAllocation: 0.2 }, ]; - const strategy = mockStrategy({ - name: 'test', - capitalAllocation: 0.4, - execute: vi.fn().mockResolvedValue(signals), - }); - const bot = new Bot(alpaca, [strategy]); + const bot = new Bot(alpaca, allocations); const promise = bot.runDay(); await vi.advanceTimersByTimeAsync(0); await promise; - // 10000 * 0.4 * 0.5 = 2000 - expect(console.log).toHaveBeenCalledWith('[test] BUY TQQQ — $2000.00'); + // 10000 * 0.3 = 3000 + expect(strategyA.execute).toHaveBeenCalledWith(alpaca, 3000); + // 10000 * 0.2 = 2000 + expect(strategyB.execute).toHaveBeenCalledWith(alpaca, 2000); }); it('works with zero strategies', async () => { @@ -131,9 +102,26 @@ describe('Bot', () => { await vi.advanceTimersByTimeAsync(0); await promise; - // Only the "waiting for open" log, no strategy logs - expect(console.log).toHaveBeenCalledTimes(1); expect(console.log).toHaveBeenCalledWith('waiting for open'); }); + + it('skips waiting when market is already open', async () => { + const alpaca = mockAlpaca({ + getClock: vi.fn().mockResolvedValue({ + is_open: true, + next_open: new Date().toISOString(), + next_close: new Date().toISOString(), + }), + }); + const strategy = mockStrategy(); + const bot = new Bot(alpaca, [{ strategy, capitalAllocation: 1.0 }]); + + const promise = bot.runDay(); + await vi.advanceTimersByTimeAsync(0); + await promise; + + expect(console.log).not.toHaveBeenCalledWith('waiting for open'); + expect(strategy.execute).toHaveBeenCalledWith(alpaca, 10000); + }); }); }); diff --git a/src/bot.ts b/src/bot.ts index 1484fe0..325cbc4 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,23 +1,25 @@ import { Alpaca } from "./alpaca"; import { Strategy } from "./strategy"; -import { Executor } from "./executor"; import { isMarketOpen, waitForNextOpen } from "./trading"; +export interface StrategyAllocation { + strategy: Strategy; + capitalAllocation: number; +} + export class Bot { private alpaca: Alpaca; - private strategies: Strategy[]; - private executor: Executor; + private allocations: StrategyAllocation[]; - constructor(alpaca: Alpaca, strategies: Strategy[]) { - const totalAllocation = strategies.reduce((sum, s) => sum + s.capitalAllocation, 0); + constructor(alpaca: Alpaca, allocations: StrategyAllocation[]) { + const totalAllocation = allocations.reduce((sum, a) => sum + a.capitalAllocation, 0); if (totalAllocation > 1.0) { throw new Error( `Capital allocations sum to ${totalAllocation}, which exceeds 1.0` ); } this.alpaca = alpaca; - this.strategies = strategies; - this.executor = new Executor(alpaca); + this.allocations = allocations; } async runDay(): Promise { @@ -26,14 +28,13 @@ export class Bot { 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); + const account = await this.alpaca.getAccount(); + const totalCapital = parseFloat(account.cash); - await this.executor.executeSignals(strategy, signals, totalCapital); + await Promise.all( + this.allocations.map(async ({ strategy, capitalAllocation }) => { + const capitalAmount = totalCapital * capitalAllocation; + await strategy.execute(this.alpaca, capitalAmount); }) ); } diff --git a/src/executor.test.ts b/src/executor.test.ts deleted file mode 100644 index b60558e..0000000 --- a/src/executor.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { Executor } from './executor'; -import type { Alpaca } from './alpaca'; -import type { Strategy, Signal } from './strategy'; - -function mockAlpaca(): 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(), - } as unknown as Alpaca; -} - -function mockStrategy(overrides: Partial = {}): Strategy { - return { - name: 'test-strategy', - capitalAllocation: 0.5, - execute: vi.fn().mockResolvedValue([]), - ...overrides, - }; -} - -describe('Executor', () => { - beforeEach(() => { - vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('logs a buy signal with correct dollar amount', async () => { - const executor = new Executor(mockAlpaca()); - const strategy = mockStrategy({ name: 'momentum', capitalAllocation: 0.5 }); - const signals: Signal[] = [{ symbol: 'TQQQ', direction: 'buy', allocation: 0.8 }]; - - await executor.executeSignals(strategy, signals, 10000); - - // 10000 * 0.5 * 0.8 = 4000 - expect(console.log).toHaveBeenCalledWith('[momentum] BUY TQQQ — $4000.00'); - }); - - it('logs a sell signal with correct dollar amount', async () => { - const executor = new Executor(mockAlpaca()); - const strategy = mockStrategy({ name: 'rebalance', capitalAllocation: 0.3 }); - const signals: Signal[] = [{ symbol: 'SPY', direction: 'sell', allocation: 1.0 }]; - - await executor.executeSignals(strategy, signals, 20000); - - // 20000 * 0.3 * 1.0 = 6000 - expect(console.log).toHaveBeenCalledWith('[rebalance] SELL SPY — $6000.00'); - }); - - it('ignores signals with 0 allocation', async () => { - const executor = new Executor(mockAlpaca()); - const strategy = mockStrategy(); - const signals: Signal[] = [{ symbol: 'TQQQ', direction: 'buy', allocation: 0 }]; - - await executor.executeSignals(strategy, signals, 10000); - - expect(console.log).not.toHaveBeenCalled(); - }); - - it('handles multiple signals in one batch', async () => { - const executor = new Executor(mockAlpaca()); - const strategy = mockStrategy({ name: 'multi', capitalAllocation: 0.4 }); - const signals: Signal[] = [ - { symbol: 'TQQQ', direction: 'buy', allocation: 0.5 }, - { symbol: 'SPY', direction: 'sell', allocation: 0.5 }, - ]; - - await executor.executeSignals(strategy, signals, 10000); - - // 10000 * 0.4 * 0.5 = 2000 each - expect(console.log).toHaveBeenCalledWith('[multi] BUY TQQQ — $2000.00'); - expect(console.log).toHaveBeenCalledWith('[multi] SELL SPY — $2000.00'); - }); -}); diff --git a/src/executor.ts b/src/executor.ts deleted file mode 100644 index 16b9429..0000000 --- a/src/executor.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Alpaca } from "./alpaca"; -import { Strategy, Signal } from "./strategy"; - -export class Executor { - private alpaca: Alpaca; - - constructor(alpaca: Alpaca) { - this.alpaca = alpaca; - } - - async executeSignals(strategy: Strategy, signals: Signal[], totalCapital: number): Promise { - const strategyCapital = totalCapital * strategy.capitalAllocation; - - for (const signal of signals) { - if (signal.allocation === 0) continue; - - const dollarAmount = strategyCapital * signal.allocation; - console.log( - `[${strategy.name}] ${signal.direction.toUpperCase()} ${signal.symbol} — $${dollarAmount.toFixed(2)}` - ); - } - } -} diff --git a/src/index.ts b/src/index.ts index 1b60b25..27ee854 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,8 @@ import { wait } from "./trading"; import { MomentumStrategy } from "./momentum-strategy"; const alpaca = new Alpaca(false); -const momentum = new MomentumStrategy(1.0); -const bot = new Bot(alpaca, [momentum]); +const momentum = new MomentumStrategy(); +const bot = new Bot(alpaca, [{ strategy: momentum, capitalAllocation: 1.0 }]); async function main() { while(true) { diff --git a/src/momentum-indicator.test.ts b/src/momentum-indicator.test.ts index 25b736d..a90f390 100644 --- a/src/momentum-indicator.test.ts +++ b/src/momentum-indicator.test.ts @@ -12,6 +12,8 @@ function mockAlpaca(overrides: Partial = {}): Alpaca { getLatestBid: vi.fn(), getLatestSpread: vi.fn(), getLatestTrades: vi.fn(), + buy: vi.fn(), + sell: vi.fn(), ...overrides, } as unknown as Alpaca; } diff --git a/src/momentum-strategy.test.ts b/src/momentum-strategy.test.ts index 7c60b31..2c1e146 100644 --- a/src/momentum-strategy.test.ts +++ b/src/momentum-strategy.test.ts @@ -12,6 +12,8 @@ function mockAlpaca(overrides: Partial = {}): Alpaca { getLatestBid: vi.fn(), getLatestSpread: vi.fn(), getLatestTrades: vi.fn(), + buy: vi.fn().mockResolvedValue(50), + sell: vi.fn().mockResolvedValue(50), ...overrides, } as unknown as Alpaca; } @@ -37,110 +39,99 @@ describe('MomentumStrategy', () => { it('buys TQQQ when QQQ goes up', async () => { const alpaca = mockAlpaca({ getLatestAsk: vi.fn() - // indicator: QQQ before .mockResolvedValueOnce(100) - // indicator: QQQ after (up) - .mockResolvedValueOnce(101) - // entry quote for TQQQ - .mockResolvedValueOnce(50), - getLatestBid: vi.fn() - // poll: hit target immediately - .mockResolvedValueOnce(50.50), + .mockResolvedValueOnce(101), + buy: vi.fn().mockResolvedValue(50), + getLatestBid: vi.fn().mockResolvedValue(50.50), }); - const strategy = new MomentumStrategy(1.0, fastConfig); - const promise = strategy.execute(alpaca); + const strategy = new MomentumStrategy(fastConfig); + const promise = strategy.execute(alpaca, 5000); await vi.advanceTimersByTimeAsync(1000); - const signals = await promise; + await promise; - expect(signals[0]).toEqual({ symbol: 'TQQQ', direction: 'buy', allocation: 1.0 }); + expect(alpaca.buy).toHaveBeenCalledWith('TQQQ', 5000); }); it('buys SQQQ when QQQ goes down', async () => { const alpaca = mockAlpaca({ getLatestAsk: vi.fn() - // indicator: QQQ before .mockResolvedValueOnce(100) - // indicator: QQQ after (down) - .mockResolvedValueOnce(99) - // entry quote for SQQQ - .mockResolvedValueOnce(30), - getLatestBid: vi.fn() - // poll: hit target immediately - .mockResolvedValueOnce(30.30), + .mockResolvedValueOnce(99), + buy: vi.fn().mockResolvedValue(30), + getLatestBid: vi.fn().mockResolvedValue(30.30), }); - const strategy = new MomentumStrategy(1.0, fastConfig); - const promise = strategy.execute(alpaca); + const strategy = new MomentumStrategy(fastConfig); + const promise = strategy.execute(alpaca, 5000); await vi.advanceTimersByTimeAsync(1000); - const signals = await promise; + await promise; - expect(signals[0]).toEqual({ symbol: 'SQQQ', direction: 'buy', allocation: 1.0 }); + expect(alpaca.buy).toHaveBeenCalledWith('SQQQ', 5000); }); it('sells when bid hits 1% target', async () => { const alpaca = mockAlpaca({ getLatestAsk: vi.fn() - // indicator: QQQ samples .mockResolvedValueOnce(100) - .mockResolvedValueOnce(101) - // entry quote: ask = 50 - .mockResolvedValueOnce(50), + .mockResolvedValueOnce(101), + buy: vi.fn().mockResolvedValue(50), getLatestBid: vi.fn() // poll 1: not yet (target = 50.50) .mockResolvedValueOnce(50.10) // poll 2: hit target .mockResolvedValueOnce(50.50), + sell: vi.fn().mockResolvedValue(50.50), }); - const strategy = new MomentumStrategy(1.0, fastConfig); - const promise = strategy.execute(alpaca); + const strategy = new MomentumStrategy(fastConfig); + const promise = strategy.execute(alpaca, 5000); await vi.advanceTimersByTimeAsync(1000); - const signals = await promise; + await promise; - expect(signals[1]).toEqual({ symbol: 'TQQQ', direction: 'sell', allocation: 1.0 }); expect(console.log).toHaveBeenCalledWith('[momentum] exit TQQQ — reason: target'); + expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000); }); it('sells on timeout when target not reached', async () => { const alpaca = mockAlpaca({ getLatestAsk: vi.fn() - // indicator: QQQ samples .mockResolvedValueOnce(100) - .mockResolvedValueOnce(101) - // entry quote: ask = 50 - .mockResolvedValueOnce(50), - getLatestBid: vi.fn() - // all polls: never reach target - .mockResolvedValue(49.90), + .mockResolvedValueOnce(101), + buy: vi.fn().mockResolvedValue(50), + getLatestBid: vi.fn().mockResolvedValue(49.90), + sell: vi.fn().mockResolvedValue(49.90), }); - const strategy = new MomentumStrategy(1.0, fastConfig); - const promise = strategy.execute(alpaca); + const strategy = new MomentumStrategy(fastConfig); + const promise = strategy.execute(alpaca, 5000); await vi.advanceTimersByTimeAsync(2000); - const signals = await promise; + await promise; - expect(signals[1]).toEqual({ symbol: 'TQQQ', direction: 'sell', allocation: 1.0 }); expect(console.log).toHaveBeenCalledWith('[momentum] exit TQQQ — reason: timeout'); + expect(alpaca.sell).toHaveBeenCalledWith('TQQQ', 5000); }); - it('returns both buy and sell signals', async () => { + it('uses actual fill price for target calculation', async () => { const alpaca = mockAlpaca({ getLatestAsk: vi.fn() .mockResolvedValueOnce(100) - .mockResolvedValueOnce(101) - .mockResolvedValueOnce(50), + .mockResolvedValueOnce(101), + // fill price is 50, so 1% target = 50.50 + buy: vi.fn().mockResolvedValue(50), getLatestBid: vi.fn() + // 50.49 is below target + .mockResolvedValueOnce(50.49) + // 50.50 hits target .mockResolvedValueOnce(50.50), + sell: vi.fn().mockResolvedValue(50.50), }); - const strategy = new MomentumStrategy(1.0, fastConfig); - const promise = strategy.execute(alpaca); + const strategy = new MomentumStrategy(fastConfig); + const promise = strategy.execute(alpaca, 5000); await vi.advanceTimersByTimeAsync(1000); - const signals = await promise; + await promise; - expect(signals).toHaveLength(2); - expect(signals[0].direction).toBe('buy'); - expect(signals[1].direction).toBe('sell'); + expect(console.log).toHaveBeenCalledWith('[momentum] exit TQQQ — reason: target'); }); }); diff --git a/src/momentum-strategy.ts b/src/momentum-strategy.ts index 532e318..d1bc846 100644 --- a/src/momentum-strategy.ts +++ b/src/momentum-strategy.ts @@ -1,5 +1,5 @@ import { Alpaca } from "./alpaca"; -import { Strategy, Signal } from "./strategy"; +import { Strategy } from "./strategy"; import { MomentumIndicator, MomentumIndicatorConfig } from "./momentum-indicator"; import { wait } from "./trading"; @@ -18,23 +18,19 @@ const defaultConfig: MomentumStrategyConfig = { export class MomentumStrategy implements Strategy { name = 'momentum'; - capitalAllocation: number; private config: MomentumStrategyConfig; private indicator: MomentumIndicator; - constructor(capitalAllocation: number, config: Partial = {}) { - this.capitalAllocation = capitalAllocation; + constructor(config: Partial = {}) { this.config = { ...defaultConfig, ...config }; this.indicator = new MomentumIndicator(this.config.indicatorConfig); } - async execute(alpaca: Alpaca): Promise { + async execute(alpaca: Alpaca, capitalAmount: number): Promise { const result = await this.indicator.evaluate(alpaca); const symbol = result.direction === 'up' ? 'TQQQ' : 'SQQQ'; - const entryPrice = await alpaca.getLatestAsk(symbol); - - const buy: Signal = { symbol, direction: 'buy', allocation: 1.0 }; + const entryPrice = await alpaca.buy(symbol, capitalAmount); const targetPrice = entryPrice * (1 + this.config.targetGain); const deadline = Date.now() + this.config.holdTime; @@ -53,8 +49,6 @@ export class MomentumStrategy implements Strategy { console.log(`[${this.name}] exit ${symbol} — reason: ${reason}`); - const sell: Signal = { symbol, direction: 'sell', allocation: 1.0 }; - - return [buy, sell]; + await alpaca.sell(symbol, capitalAmount); } } diff --git a/src/strategy.ts b/src/strategy.ts index c8ea4af..3316c9f 100644 --- a/src/strategy.ts +++ b/src/strategy.ts @@ -1,15 +1,6 @@ import { Alpaca } from "./alpaca"; -export type SignalDirection = 'buy' | 'sell'; - -export interface Signal { - symbol: string; - direction: SignalDirection; - allocation: number; // fraction of this strategy's capital to use (0-1) -} - export interface Strategy { name: string; - capitalAllocation: number; // fraction of total account capital (0-1) - execute(alpaca: Alpaca): Promise; + execute(alpaca: Alpaca, capitalAmount: number): Promise; } diff --git a/src/trading.test.ts b/src/trading.test.ts index 75eeaff..d19b3ef 100644 --- a/src/trading.test.ts +++ b/src/trading.test.ts @@ -12,6 +12,8 @@ function mockAlpaca(overrides: Partial = {}): Alpaca { getLatestBid: vi.fn(), getLatestSpread: vi.fn(), getLatestTrades: vi.fn(), + buy: vi.fn(), + sell: vi.fn(), ...overrides, } as unknown as Alpaca; }