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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
dist/
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
29
CLAUDE.md
Normal file
29
CLAUDE.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A TypeScript trading bot that monitors the TQQQ ETF via the Alpaca paper trading API. It waits for market open, samples two price quotes one second apart, and logs whether the price moved up or down. It runs in a continuous hourly loop.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- **Build:** `npm run build` (compiles TypeScript from `src/` to `dist/`)
|
||||||
|
- **Start:** `npm start` (runs compiled `dist/index.js`)
|
||||||
|
- **Dev:** `npm run dev` (runs with ts-node + nodemon for auto-reload)
|
||||||
|
- **Docker:** Standard `docker build` / `docker run` using the included Dockerfile (Node 22 base)
|
||||||
|
|
||||||
|
There are no tests or linting configured.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Two source files with a simple layered design:
|
||||||
|
|
||||||
|
- `src/index.ts` — Entry point. Contains the main loop (`main()` → `runDay()`) that waits for market open via the Alpaca clock API, fetches two consecutive TQQQ quotes, compares them, and logs the direction. Utility functions for account balance, asset info, and delays live here too.
|
||||||
|
- `src/alpaca.ts` — Wrapper class around `@alpacahq/alpaca-trade-api`. Exposes methods for account info, asset lookup, market clock, and price quotes. Uses CommonJS `require` for the Alpaca SDK (not ES import).
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
- **TypeScript target:** ES2016, CommonJS modules, strict mode enabled
|
||||||
|
- **Source/output dirs:** `src/` → `dist/`
|
||||||
|
- `esModuleInterop` is on — mixed import styles are allowed
|
||||||
20
eslint.config.js
Normal file
20
eslint.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const tseslint = require('@typescript-eslint/eslint-plugin');
|
||||||
|
const tsparser = require('@typescript-eslint/parser');
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
files: ['src/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsparser,
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...tseslint.configs.recommended.rules,
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
3507
package-lock.json
generated
3507
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,18 +1,24 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.1",
|
||||||
"@types/ws": "^8.5.14",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build tsconfig.json",
|
"build": "tsc --build tsconfig.json",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "nodemon --exec ts-node src/index.ts"
|
"dev": "nodemon --exec ts-node src/index.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "eslint src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-fetch": "^3.3.2",
|
"@alpacahq/alpaca-trade-api": "^3.1.3",
|
||||||
"ws": "^8.18.0"
|
"dotenv": "^17.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/alpaca.test.ts
Normal file
65
src/alpaca.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Alpaca, AlpacaClient } from './alpaca';
|
||||||
|
|
||||||
|
function mockClient(overrides: Partial<AlpacaClient> = {}): AlpacaClient {
|
||||||
|
return {
|
||||||
|
getAccount: vi.fn().mockResolvedValue({ cash: '10000.00' }),
|
||||||
|
getAssets: vi.fn().mockResolvedValue([]),
|
||||||
|
getAsset: vi.fn().mockResolvedValue({ symbol: 'TQQQ', fractionable: true, status: 'active', asset_class: 'us_equity' }),
|
||||||
|
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({ ap: 50.00, bp: 49.90 }),
|
||||||
|
getLatestTrades: vi.fn().mockResolvedValue(new Map()),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Alpaca', () => {
|
||||||
|
it('throws when live is true', () => {
|
||||||
|
expect(() => new Alpaca(true)).toThrow('not doing live yet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates getAccount to the underlying client', async () => {
|
||||||
|
const client = mockClient();
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
const account = await alpaca.getAccount();
|
||||||
|
expect(account.cash).toBe('10000.00');
|
||||||
|
expect(client.getAccount).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates getAsset with the symbol string', async () => {
|
||||||
|
const client = mockClient();
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
await alpaca.getAsset('TQQQ');
|
||||||
|
expect(client.getAsset).toHaveBeenCalledWith('TQQQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates getClock to the underlying client', async () => {
|
||||||
|
const client = mockClient();
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
const clock = await alpaca.getClock();
|
||||||
|
expect(clock.is_open).toBe(false);
|
||||||
|
expect(client.getClock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates getLatestQuote to the underlying client', async () => {
|
||||||
|
const client = mockClient();
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
const quote = await alpaca.getLatestQuote('TQQQ');
|
||||||
|
expect(quote.ap).toBe(50.00);
|
||||||
|
expect(client.getLatestQuote).toHaveBeenCalledWith('TQQQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates getAssets with active us_equity filter', async () => {
|
||||||
|
const client = mockClient();
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
await alpaca.getAssets();
|
||||||
|
expect(client.getAssets).toHaveBeenCalledWith({ status: 'active', asset_class: 'us_equity' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates getLatestTrades to the underlying client', async () => {
|
||||||
|
const client = mockClient();
|
||||||
|
const alpaca = new Alpaca(false, client);
|
||||||
|
await alpaca.getLatestTrades(['TQQQ', 'SPY']);
|
||||||
|
expect(client.getLatestTrades).toHaveBeenCalledWith(['TQQQ', 'SPY']);
|
||||||
|
});
|
||||||
|
});
|
||||||
169
src/alpaca.ts
169
src/alpaca.ts
@@ -1,130 +1,93 @@
|
|||||||
import ws from 'ws';
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const AlpacaJS = require('@alpacahq/alpaca-trade-api')
|
||||||
|
|
||||||
|
if (!process.env.ALPACA_KEY_ID || !process.env.ALPACA_SECRET_KEY) {
|
||||||
|
throw new Error('Missing ALPACA_KEY_ID or ALPACA_SECRET_KEY in environment');
|
||||||
|
}
|
||||||
|
|
||||||
const paper_credentials = {
|
const paper_credentials = {
|
||||||
key : 'PKCSK7Z6WGNF6QDEKEG3',
|
keyId : process.env.ALPACA_KEY_ID,
|
||||||
secret : '89JxFVaJ14BVa1xVzDL6gIZETsFHYl1cnseEVJog'
|
secretKey : process.env.ALPACA_SECRET_KEY,
|
||||||
}
|
paper : true
|
||||||
const live_livecredentials = {
|
|
||||||
key : '', //not yet
|
|
||||||
secret : ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AlpacaAccount {
|
||||||
|
cash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlpacaAsset {
|
||||||
|
symbol: string;
|
||||||
|
fractionable: boolean;
|
||||||
|
status: string;
|
||||||
|
asset_class: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlpacaClock {
|
||||||
|
is_open: boolean;
|
||||||
|
next_open: string;
|
||||||
|
next_close: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlpacaQuote {
|
||||||
|
ap: number; // ask price
|
||||||
|
bp: number; // bid price
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlpacaTrade {
|
||||||
|
p: number; // price
|
||||||
|
s: number; // size
|
||||||
|
t: string; // timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlpacaClient {
|
||||||
|
getAccount(): Promise<AlpacaAccount>;
|
||||||
|
getAssets(params: { status: string; asset_class: string }): Promise<AlpacaAsset[]>;
|
||||||
|
getAsset(symbol: string): Promise<AlpacaAsset>;
|
||||||
|
getClock(): Promise<AlpacaClock>;
|
||||||
|
getLatestQuote(symbol: string): Promise<AlpacaQuote>;
|
||||||
|
getLatestTrades(symbols: string[]): Promise<Map<string, AlpacaTrade>>;
|
||||||
|
}
|
||||||
|
|
||||||
export class Alpaca {
|
export class Alpaca {
|
||||||
private credentials: {key: string, secret: string};
|
private alpaca: AlpacaClient;
|
||||||
private api_url : string;
|
constructor(live = false, client?: AlpacaClient) {
|
||||||
constructor(live = false) {
|
if (client) {
|
||||||
if (live) {
|
this.alpaca = client;
|
||||||
this.credentials = live_livecredentials;
|
}
|
||||||
this.api_url = ''//
|
else if (live) {
|
||||||
throw new Error("not doing live yet");
|
throw new Error("not doing live yet");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.credentials = paper_credentials;
|
this.alpaca = new AlpacaJS(paper_credentials);
|
||||||
this.api_url = 'https://paper-api.alpaca.markets';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get(url: string) {
|
|
||||||
//probably options
|
|
||||||
return fetch(this.api_url + url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'APCA-API-KEY-ID': this.credentials.key,
|
|
||||||
'APCA-API-SECRET-KEY': this.credentials.secret
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
private data(url: string) {
|
|
||||||
return fetch( url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'APCA-API-KEY-ID': this.credentials.key,
|
|
||||||
'APCA-API-SECRET-KEY': this.credentials.secret
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAccount() {
|
public async getAccount() {
|
||||||
const response = await this.get('/v2/account');
|
return this.alpaca.getAccount();
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
public async getPositions() {
|
|
||||||
const response = await this.get('/v2/positions');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
public async getOrders() {
|
|
||||||
const response = await this.get('/v2/orders');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAssets() {
|
public async getAssets() {
|
||||||
const response = await this.get('/v2/assets');
|
|
||||||
return await response.json();
|
return this.alpaca.getAssets({
|
||||||
|
status : 'active',
|
||||||
|
asset_class : 'us_equity'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public async getAsset(symbol: string) {
|
public async getAsset(symbol: string) {
|
||||||
const response = await this.get('/v2/assets/' + symbol);
|
return this.alpaca.getAsset(symbol);
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getWatchlists() {
|
|
||||||
const response = await this.get('/v2/watchlists');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getCalendar() {
|
|
||||||
const response = await this.get('/v2/calendar');
|
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClock() {
|
public async getClock() {
|
||||||
const response = await this.get('/v2/clock');
|
return this.alpaca.getClock();
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAccountConfigurations() {
|
public async getLatestQuote(symbol: string) {
|
||||||
const response = await this.get('/v2/account/configurations');
|
return this.alpaca.getLatestQuote(symbol);
|
||||||
return await response.json();
|
|
||||||
}
|
}
|
||||||
public async getAccountActivities() {
|
|
||||||
const response = await this.get('/v2/account/activities');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getPortfolioHistory() {
|
|
||||||
const response = await this.get('/v2/account/portfolio/history');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getOrdersByPath() {
|
|
||||||
const response = await this.get('/v2/orders');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getOrderByPath() {
|
|
||||||
const response = await this.get('/v2/orders');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getOrderByClientOrderId() {
|
|
||||||
const response = await this.get('/v2/orders');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getOrderByID() {
|
|
||||||
const response = await this.get('/v2/orders');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
public async getLatestQuote(symbol: string) {{
|
|
||||||
const response = await this.data(`https://data.alpaca.markets/v2/stocks/${symbol}/quotes/latest`);
|
|
||||||
return response.json();
|
|
||||||
|
|
||||||
}}
|
|
||||||
public async getLatestTrades(symbols: string[]) {
|
public async getLatestTrades(symbols: string[]) {
|
||||||
console.log(symbols.length)
|
return this.alpaca.getLatestTrades(symbols);
|
||||||
console.log(symbols.join(','))
|
|
||||||
const response = await this.data('https://data.alpaca.markets/v2/stocks/trades/latest?symbols=' + symbols.join(','));
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
52
src/index.ts
52
src/index.ts
@@ -1,61 +1,15 @@
|
|||||||
import { Alpaca } from "./alpaca";
|
import { Alpaca } from "./alpaca";
|
||||||
|
import { runDay, wait } from "./trading";
|
||||||
|
|
||||||
const alpaca = new Alpaca(false);
|
const alpaca = new Alpaca(false);
|
||||||
|
|
||||||
async function printAsset(symbol: string) {
|
|
||||||
const asset = await alpaca.getAsset(symbol);
|
|
||||||
if (asset && asset.fractionable)
|
|
||||||
console.log(symbol + ' is fractional')
|
|
||||||
else
|
|
||||||
console.log(symbol + ' is not fractional')
|
|
||||||
}
|
|
||||||
async function accountBalance() {
|
|
||||||
const account = await alpaca.getAccount();
|
|
||||||
return account.cash;
|
|
||||||
|
|
||||||
}
|
|
||||||
async function waitForNextOpen() {
|
|
||||||
const clock = await alpaca.getClock();
|
|
||||||
return wait(new Date(clock.next_open).valueOf() - new Date().valueOf());
|
|
||||||
}
|
|
||||||
function wait(ms :number) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
//console.log('would wait: ' + ms + 'ms');
|
|
||||||
//resolve('')
|
|
||||||
setTimeout(resolve, ms);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
async function runDay() {
|
|
||||||
console.log('waiting for open');
|
|
||||||
await waitForNextOpen();
|
|
||||||
|
|
||||||
await wait(1000); //wait a miniute
|
|
||||||
|
|
||||||
const q = await alpaca.getLatestQuote('TQQQ');
|
|
||||||
await wait(1000);
|
|
||||||
|
|
||||||
const q2 = await alpaca.getLatestQuote('TQQQ');
|
|
||||||
|
|
||||||
if (q2.ap - q.ap > 0) {
|
|
||||||
//up day
|
|
||||||
console.log('up day: ', new Date())
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//down day
|
|
||||||
console.log('down day', new Date());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function main() {
|
async function main() {
|
||||||
while(true) {
|
while(true) {
|
||||||
await runDay();
|
await runDay(alpaca);
|
||||||
await wait(1000 * 60 * 60);//wait an hour before going and getting the next open
|
await wait(1000 * 60 * 60);//wait an hour before going and getting the next open
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//run main
|
//run main
|
||||||
main().then(
|
main().then(
|
||||||
() => {
|
() => {
|
||||||
@@ -65,4 +19,4 @@ main().then(
|
|||||||
(e) => console.log('Error: ', e)
|
(e) => console.log('Error: ', e)
|
||||||
).finally(
|
).finally(
|
||||||
() => process.exit(0)
|
() => process.exit(0)
|
||||||
);
|
);
|
||||||
|
|||||||
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
46
src/trading.ts
Normal file
46
src/trading.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Alpaca } from "./alpaca";
|
||||||
|
|
||||||
|
export function wait(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function printAsset(alpaca: Alpaca, symbol: string) {
|
||||||
|
const asset = await alpaca.getAsset(symbol);
|
||||||
|
if (asset && asset.fractionable)
|
||||||
|
console.log(symbol + ' is fractional')
|
||||||
|
else
|
||||||
|
console.log(symbol + ' is not fractional')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function accountBalance(alpaca: Alpaca) {
|
||||||
|
const account = await alpaca.getAccount();
|
||||||
|
return account.cash;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForNextOpen(alpaca: Alpaca) {
|
||||||
|
const clock = await alpaca.getClock();
|
||||||
|
return wait(new Date(clock.next_open).valueOf() - new Date().valueOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runDay(alpaca: Alpaca) {
|
||||||
|
console.log('waiting for open');
|
||||||
|
await waitForNextOpen(alpaca);
|
||||||
|
|
||||||
|
await wait(60000); //wait a minute
|
||||||
|
|
||||||
|
const q = await alpaca.getLatestQuote('TQQQ');
|
||||||
|
await wait(1000);
|
||||||
|
|
||||||
|
const q2 = await alpaca.getLatestQuote('TQQQ');
|
||||||
|
|
||||||
|
if (q2.ap - q.ap > 0) {
|
||||||
|
//up day
|
||||||
|
console.log('up day: ', new Date())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//down day
|
||||||
|
console.log('down day', new Date());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,5 +107,6 @@
|
|||||||
/* Completeness */
|
/* Completeness */
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
}
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
7
vitest.config.ts
Normal file
7
vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user