From d0d3d7254a32f4e098417c5ba6672b62f53a7bc7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 30 Jan 2026 14:06:46 -0700 Subject: [PATCH] Add executor with logging placeholder and tests The Executor receives strategy signals and calculates dollar amounts from capital allocations. Currently logs intended trades rather than placing real orders. Co-Authored-By: Claude Opus 4.5 --- src/executor.test.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++ src/executor.ts | 23 +++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/executor.test.ts create mode 100644 src/executor.ts diff --git a/src/executor.test.ts b/src/executor.test.ts new file mode 100644 index 0000000..5c852c6 --- /dev/null +++ b/src/executor.test.ts @@ -0,0 +1,81 @@ +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(), + getLatestQuote: 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 new file mode 100644 index 0000000..16b9429 --- /dev/null +++ b/src/executor.ts @@ -0,0 +1,23 @@ +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)}` + ); + } + } +}