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:
125
src/trading.test.ts
Normal file
125
src/trading.test.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user