Refactor for TDD workflow with Vitest and ESLint

- Move API keys from hardcoded values to .env via dotenv
- Extract business logic into src/trading.ts with dependency injection
- Add typed AlpacaClient interface, replace `any` on Alpaca class
- Add Vitest test suites for trading logic and Alpaca wrapper (14 tests)
- Set up ESLint with @typescript-eslint for linting
- Fix getAsset to pass symbol string directly to SDK
- Fix typo (alpacha -> alpaca), remove unused ws/node-fetch deps
- Update .gitignore for node_modules and .env

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-01-30 13:49:31 -07:00
parent 892305e349
commit 5e06c06987
12 changed files with 3781 additions and 262 deletions

125
src/trading.test.ts Normal file
View File

@@ -0,0 +1,125 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { printAsset, accountBalance, waitForNextOpen, runDay } from './trading';
import type { Alpaca } from './alpaca';
function mockAlpaca(overrides: Partial<Alpaca> = {}): Alpaca {
return {
getAccount: vi.fn(),
getAssets: vi.fn(),
getAsset: vi.fn(),
getClock: vi.fn(),
getLatestQuote: vi.fn(),
getLatestTrades: vi.fn(),
...overrides,
} as unknown as Alpaca;
}
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe('printAsset', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('logs fractional when asset is fractionable', async () => {
const alpaca = mockAlpaca({
getAsset: vi.fn().mockResolvedValue({ symbol: 'TQQQ', fractionable: true, status: 'active', asset_class: 'us_equity' }),
});
await printAsset(alpaca, 'TQQQ');
expect(console.log).toHaveBeenCalledWith('TQQQ is fractional');
});
it('logs not fractional when asset is not fractionable', async () => {
const alpaca = mockAlpaca({
getAsset: vi.fn().mockResolvedValue({ symbol: 'BRK.A', fractionable: false, status: 'active', asset_class: 'us_equity' }),
});
await printAsset(alpaca, 'BRK.A');
expect(console.log).toHaveBeenCalledWith('BRK.A is not fractional');
});
});
describe('accountBalance', () => {
it('returns the cash value from the account', async () => {
const alpaca = mockAlpaca({
getAccount: vi.fn().mockResolvedValue({ cash: '10000.00' }),
});
const result = await accountBalance(alpaca);
expect(result).toBe('10000.00');
});
});
describe('waitForNextOpen', () => {
it('calls getClock and waits until next_open', async () => {
const futureDate = new Date(Date.now() + 60000).toISOString();
const alpaca = mockAlpaca({
getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: futureDate, next_close: futureDate }),
});
const promise = waitForNextOpen(alpaca);
await vi.advanceTimersByTimeAsync(60000);
await promise;
expect(alpaca.getClock).toHaveBeenCalled();
});
});
describe('runDay', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('logs up day when second quote ask price is higher', async () => {
const alpaca = mockAlpaca({
getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: new Date().toISOString(), next_close: new Date().toISOString() }),
getLatestQuote: vi.fn()
.mockResolvedValueOnce({ ap: 50.00, bp: 49.90 })
.mockResolvedValueOnce({ ap: 50.50, bp: 50.40 }),
});
const promise = runDay(alpaca);
await vi.advanceTimersByTimeAsync(61000);
await promise;
expect(console.log).toHaveBeenCalledWith('up day: ', expect.any(Date));
});
it('logs down day when second quote ask price is lower', async () => {
const alpaca = mockAlpaca({
getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: new Date().toISOString(), next_close: new Date().toISOString() }),
getLatestQuote: vi.fn()
.mockResolvedValueOnce({ ap: 50.00, bp: 49.90 })
.mockResolvedValueOnce({ ap: 49.50, bp: 49.40 }),
});
const promise = runDay(alpaca);
await vi.advanceTimersByTimeAsync(61000);
await promise;
expect(console.log).toHaveBeenCalledWith('down day', expect.any(Date));
});
it('logs down day when prices are equal', async () => {
const alpaca = mockAlpaca({
getClock: vi.fn().mockResolvedValue({ is_open: false, next_open: new Date().toISOString(), next_close: new Date().toISOString() }),
getLatestQuote: vi.fn()
.mockResolvedValueOnce({ ap: 50.00, bp: 49.90 })
.mockResolvedValueOnce({ ap: 50.00, bp: 49.90 }),
});
const promise = runDay(alpaca);
await vi.advanceTimersByTimeAsync(61000);
await promise;
expect(console.log).toHaveBeenCalledWith('down day', expect.any(Date));
});
});