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; }