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 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-01-30 14:06:46 -07:00
parent 345ff933ec
commit d0d3d7254a
2 changed files with 104 additions and 0 deletions

81
src/executor.test.ts Normal file
View File

@@ -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> = {}): 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');
});
});

23
src/executor.ts Normal file
View File

@@ -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<void> {
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)}`
);
}
}
}