From a9b6acd4792e6c547317a1395c50be9290a75a09 Mon Sep 17 00:00:00 2001 From: Senofy <63175905+Senofy@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:07:02 -0600 Subject: [PATCH] Initial Commit --- .claude/launch.json | 12 + app/add/page.tsx | 83 ++++++ app/api/firms/[id]/route.ts | 27 ++ app/api/firms/route.ts | 54 ++++ app/api/state/route.ts | 35 +++ app/globals.css | 29 +- app/layout.tsx | 21 +- app/page.tsx | 324 +++++++++++++++++---- autotrader.sqlite | Bin 0 -> 20480 bytes lib/clients.ts | 45 +++ lib/db.ts | 87 ++++++ lib/tradovate-class.ts | 403 +++++++++++++++++++++++++ lib/tradovate-helpers.ts | 61 ++++ next.config.ts | 2 +- package-lock.json | 565 +++++++++++++++++++++++++++++++++++- package.json | 3 + types.ts | 31 ++ 17 files changed, 1665 insertions(+), 117 deletions(-) create mode 100644 .claude/launch.json create mode 100644 app/add/page.tsx create mode 100644 app/api/firms/[id]/route.ts create mode 100644 app/api/firms/route.ts create mode 100644 app/api/state/route.ts create mode 100644 autotrader.sqlite create mode 100644 lib/clients.ts create mode 100644 lib/db.ts create mode 100644 lib/tradovate-class.ts create mode 100644 lib/tradovate-helpers.ts create mode 100644 types.ts diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..8528960 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "autotrader-next", + "runtimeExecutable": "npx", + "runtimeArgs": ["next", "dev", "--port", "3000"], + "port": 3000, + "cwd": "D:\\Development\\market-dev\\autotrader-firms\\autotrader-next" + } + ] +} diff --git a/app/add/page.tsx b/app/add/page.tsx new file mode 100644 index 0000000..1f5931e --- /dev/null +++ b/app/add/page.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function AddPage() { + const router = useRouter(); + const [firmName, setFirmName] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [saving, setSaving] = useState(false); + + const inputCls = 'w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-400'; + const labelCls = 'block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1'; + + const handleSave = async () => { + if (!firmName.trim() || !username.trim() || !password.trim()) return; + setSaving(true); + setError(''); + try { + const res = await fetch('/api/firms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: firmName.trim(), username: username.trim(), password: password.trim() }), + }); + if (!res.ok) { + const data = await res.json() as { error?: string }; + setError(data.error ?? 'Failed to save firm'); + return; + } + router.push('/'); + } catch { + setError('Could not connect to server'); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+ +

Add New Firm

+
+
+ {error && ( +
+ {error} +
+ )} +
+ + setFirmName(e.target.value)} className={inputCls} placeholder="e.g. MyFirm" /> +
+
+ + setUsername(e.target.value)} className={inputCls} placeholder="Username" /> +
+
+ + setPassword(e.target.value)} className={inputCls} placeholder="Password" /> +
+
+ + +
+
+
+
+ ); +} diff --git a/app/api/firms/[id]/route.ts b/app/api/firms/[id]/route.ts new file mode 100644 index 0000000..f49ed90 --- /dev/null +++ b/app/api/firms/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { deleteFirm } from '@/lib/db'; +import { removeClient } from '@/lib/clients'; + +export async function DELETE( + _req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id: idStr } = await params; + const id = parseInt(idStr, 10); + + if (isNaN(id)) { + return NextResponse.json({ error: 'Invalid id' }, { status: 400 }); + } + + try { + const deleted = deleteFirm(id); + if (!deleted) { + return NextResponse.json({ error: 'Firm not found' }, { status: 404 }); + } + removeClient(id); + return NextResponse.json({ success: true }); + } catch (err) { + console.error('[DELETE /api/firms/:id]', err); + return NextResponse.json({ error: 'Failed to delete firm' }, { status: 500 }); + } +} diff --git a/app/api/firms/route.ts b/app/api/firms/route.ts new file mode 100644 index 0000000..c059d74 --- /dev/null +++ b/app/api/firms/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getFirms, createFirm } from '@/lib/db'; +import { initClient } from '@/lib/clients'; + +export async function GET() { + try { + const firms = getFirms(); + const result = firms.map((f) => ({ + id: f.id, + firm: f.name, + username: f.username, + password: f.password, + accounts: f.accounts.map((a) => ({ + prefix: a.prefix, + profitTarget: a.profit_target, + consistency: a.consistency, + minDayPnL: a.min_day_pnl, + minTradingDays: a.min_trading_days, + })), + })); + return NextResponse.json(result); + } catch (err) { + console.error('[GET /api/firms]', err); + return NextResponse.json({ error: 'Failed to fetch firms' }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + const body = await req.json() as { name?: string; username?: string; password?: string }; + const { name, username, password } = body; + + if (!name?.trim() || !username?.trim() || !password?.trim()) { + return NextResponse.json({ error: 'name, username, and password are required' }, { status: 400 }); + } + + try { + const firm = createFirm(name.trim(), username.trim(), password.trim()); + initClient(firm.id, firm.username, firm.password, firm.name); + return NextResponse.json({ + id: firm.id, + firm: firm.name, + username: firm.username, + password: firm.password, + accounts: [], + }, { status: 201 }); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes('UNIQUE')) { + return NextResponse.json({ error: 'A firm with that name already exists' }, { status: 409 }); + } + console.error('[POST /api/firms]', err); + return NextResponse.json({ error: 'Failed to create firm' }, { status: 500 }); + } +} diff --git a/app/api/state/route.ts b/app/api/state/route.ts new file mode 100644 index 0000000..ef6e4c2 --- /dev/null +++ b/app/api/state/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import { getFirms } from '@/lib/db'; +import { getClients } from '@/lib/clients'; + +export async function GET() { + try { + const firms = getFirms(); + const clients = getClients(); + + const state = firms.map((f) => { + const client = clients.get(f.id); + if (!client || client.accountList.length === 0) { + return { firm: f.name, connected: false, accounts: [] }; + } + const accounts = client.accountList.map((acc) => { + const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 }; + return { + id: acc.id, + name: acc.name, + active: acc.active, + amount: cash.amount, + realizedPnL: cash.realizedPnL, + daysTraded: client.daysTraded[acc.id] ?? 0, + hasPosition: !!client.positions[acc.id], + }; + }); + return { firm: f.name, connected: true, accounts }; + }); + + return NextResponse.json(state); + } catch (err) { + console.error('[GET /api/state]', err); + return NextResponse.json({ error: 'Failed to fetch state' }, { status: 500 }); + } +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..6ad04cd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,13 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { - background: var(--background); - color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +.chevron { + transition: transform 0.2s; +} + +.chevron.open { + transform: rotate(180deg); +} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..1d11acd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,20 +1,9 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "AutoTrader", + description: "AutoTrader Firm Dashboard", }; export default function RootLayout({ @@ -24,11 +13,7 @@ export default function RootLayout({ }>) { return ( - - {children} - + {children} ); } diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..e67407b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,267 @@ -import Image from "next/image"; +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types'; + +function getAccountConfig(name: string, firm: FirmConfig): AccountConfig | undefined { + return [...firm.accounts] + .sort((a, b) => b.prefix.length - a.prefix.length) + .find((a) => name.startsWith(a.prefix)); +} + +function fmt(value: number) { + return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function SettingsIcon() { + return ( + + + + + ); +} + +function ChevronIcon({ open }: { open: boolean }) { + return ( + + + + ); +} + +function AccountRow({ account, firm }: { account: AccountState; firm: FirmConfig }) { + const cfg = getAccountConfig(account.name, firm); + const pnlColor = account.realizedPnL > 0 ? 'text-green-600 font-medium' : account.realizedPnL < 0 ? 'text-red-600 font-medium' : 'text-slate-400'; + + return ( + + +
+ {account.name} +
+ + ${fmt(account.amount)} + + {account.realizedPnL >= 0 ? '+' : ''}${fmt(account.realizedPnL)} + + + {account.daysTraded} / {cfg?.minTradingDays ?? '—'} + + + ${cfg?.profitTarget?.toLocaleString() ?? '—'} + + + {!account.active + ? Inactive + : account.hasPosition + ? In Trade + : Flat} + + + ); +} + +function FirmRows({ state, firm, deleteMode, selected, onToggle }: { + state: FirmState; + firm: FirmConfig; + deleteMode: boolean; + selected: boolean; + onToggle: () => void; +}) { + const [open, setOpen] = useState(true); + + return ( + <> + setOpen((o) => !o)} + > + +
+ {deleteMode && ( + e.stopPropagation()} + className="w-4 h-4 accent-red-500 cursor-pointer flex-shrink-0" + /> + )} + {state.firm} +
+ + + +
+ + +
+ + + + {open && ( + state.accounts.length === 0 ? ( + + + Waiting for data... + + + ) : ( + state.accounts.map((acc) => ( + + )) + ) + )} + + ); +} export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+ const router = useRouter(); + const [config, setConfig] = useState([]); + const [firms, setFirms] = useState([]); + const [deleteMode, setDeleteMode] = useState(false); + const [selected, setSelected] = useState>(new Set()); + + const fetchConfig = async () => { + try { + const res = await fetch('/api/firms'); + if (res.ok) { + const data: FirmConfig[] = await res.json(); + setConfig(data); + } + } catch { + // server not running yet + } + }; + + useEffect(() => { + fetchConfig(); + + const fetchState = async () => { + try { + const res = await fetch('/api/state'); + if (res.ok) { + const data: FirmState[] = await res.json(); + setFirms(data); + } + } catch { + // server not running yet + } + }; + + fetchState(); + const interval = setInterval(fetchState, 5000); + return () => clearInterval(interval); + }, []); + + const handleConfirmDelete = async () => { + await Promise.all( + [...selected].map((id) => + fetch(`/api/firms/${id}`, { method: 'DELETE' }).catch(() => {}) + ) + ); + setSelected(new Set()); + setDeleteMode(false); + fetchConfig(); + }; + + const toggleSelected = (id: number) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + return ( +
+
+
+

AutoTrader

+
+ {deleteMode ? ( + <> + {selected.size > 0 && ( + + )} + + + ) : ( + <> + + + + )} +
+
+
+ + + + + + + + + + + + + {config.length === 0 ? ( + + + + ) : ( + config.map((cfg) => { + const state = firms.find((f) => f.firm === cfg.firm) ?? { firm: cfg.firm, connected: false, accounts: [] }; + return ( + toggleSelected(cfg.id)} + /> + ); + }) + )} + +
AccountBalanceDay P&LDays TradedTargetStatus
+ No firms yet — click + Add New to get started +
+
+
- -
-
- ); + ); } diff --git a/autotrader.sqlite b/autotrader.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..38d40b38ab3ba35ecc9851193fd71be479d19c82 GIT binary patch literal 20480 zcmeI(&u`Oa7zgmzc9J$Nori{Gp;34%q^VJ9WhZ2%hl;t)8x%D-iQ}-fm&k3L&LVM& z?E>9xLvZ891@8O}@CR_+afscvUAenaa2TJNgXx#Ss1CGR-GnGkN=sG|0b!KX(DIk zU%z<4D<}|v00bZa0SG_<0uX=z1pZ%v2f5UQvaldL%7*rK&#`xQ2K!#v-WhmZcQ+Vo zrq@lTTZ~%zT9whUAf2D0hsEvCiea%E%%pX*a#J@q=?2@RddsR+3_iw9W>{B_v%9X} zZ;x~_Y8EwG)habvnVHO3XH6RA1oLhuM{5Qxu`1)USl65DdWoG3wC6ir_dd1Q4}5^b zK}Ms4t{b*P+uwCUKDJ&xPU0h$^;XrQ1yxl~dT@lm4MNA;IXER>T`HcW_g$~uu@BmN zUhkCj#g&znWPa$|9oO59`US^kM{_@(Rx!59)ul|dRGaZi1$TPB^QOZj7p<5vU2-@?^Q;fEyuMC3Q} zbACdB00bZa0SG_<0uX=z1Rwwb2teRJ705|xGJC0D@of*s?H*XAO;s)C)qJzNyN#N)hTG*!m1{w$>N-5*|(j#A8p;lo%}UZH%c|5TG770T3pGOi$yKp%3sr#!@H~B z2x+2db9!&@M|<11y$;{NalAp#UCu9G`*v|Ff4kma_SSZ%_%%uLZ$y4C|HV%z5P$## zAOHafKmY;|fB*y_009VmE&^wyS>b43K{_Wb3gbHm;-sWx; + __tradovateClientsInitialized?: boolean; +}; + +function ensureMap(): Map { + if (!g.__tradovateClients) { + g.__tradovateClients = new Map(); + } + return g.__tradovateClients; +} + +export function initClient(id: number, username: string, password: string, firmName: string): TradovateClient { + const map = ensureMap(); + const client = new TradovateClient(username, password, async () => { + console.log(`[${firmName}] sync complete — ${client.accountList.length} account(s)`); + }); + map.set(id, client); + return client; +} + +export function removeClient(id: number): void { + ensureMap().delete(id); +} + +export function getClients(): Map { + const map = ensureMap(); + if (!g.__tradovateClientsInitialized) { + g.__tradovateClientsInitialized = true; + try { + const firms = getFirms(); + for (const firm of firms) { + initClient(firm.id, firm.username, firm.password, firm.name); + } + console.log(`[clients] Initialized ${firms.length} Tradovate client(s)`); + } catch (err) { + console.error('[clients] Failed to initialize clients', err); + } + } + return map; +} diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..ee42fc4 --- /dev/null +++ b/lib/db.ts @@ -0,0 +1,87 @@ +import Database from 'better-sqlite3'; +import path from 'path'; + +const db = new Database(path.join(process.cwd(), 'autotrader.sqlite')); + +db.pragma('foreign_keys = ON'); + +db.exec(` + CREATE TABLE IF NOT EXISTS firms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + username TEXT NOT NULL, + password TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS account_configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + firm_id INTEGER NOT NULL REFERENCES firms(id) ON DELETE CASCADE, + prefix TEXT NOT NULL, + profit_target REAL NOT NULL DEFAULT 3000, + consistency REAL NOT NULL DEFAULT 0.5, + min_day_pnl REAL NOT NULL DEFAULT -999, + min_trading_days INTEGER NOT NULL DEFAULT 5 + ); +`); + +// Seed default data if empty +const firmCount = (db.prepare('SELECT COUNT(*) as count FROM firms').get() as { count: number }).count; +if (firmCount === 0) { + const insertFirm = db.prepare('INSERT INTO firms (name, username, password) VALUES (?, ?, ?)'); + const insertAccount = db.prepare( + 'INSERT INTO account_configs (firm_id, prefix, profit_target, consistency, min_day_pnl, min_trading_days) VALUES (?, ?, ?, ?, ?, ?)' + ); + + const alpha = insertFirm.run('Alpha', 'brandonsenoli72786', '-Z2kPm7nBg'); + insertAccount.run(alpha.lastInsertRowid, 'AFSTDEV', 9000, 0.51, -999, 5); + insertAccount.run(alpha.lastInsertRowid, 'AFSTDQA', 4500, 0.40, -999, 7); + insertAccount.run(alpha.lastInsertRowid, 'AFZEROEV', 3000, 0.50, -999, 5); + insertAccount.run(alpha.lastInsertRowid, 'AFZEROQA', 3000, 0.50, -999, 5); + insertAccount.run(alpha.lastInsertRowid, 'AF', 3000, 0.50, -999, 5); + + const tpt = insertFirm.run('TakeProfitTrader', 'BRANDONLI1', 'W4592F5512U2817tv='); + insertAccount.run(tpt.lastInsertRowid, 'TAKEPROFIT', 9000, 0.50, -999, 5); + + console.log('[db] Seeded default firms.'); +} + +export interface AccountConfigRow { + id: number; + firm_id: number; + prefix: string; + profit_target: number; + consistency: number; + min_day_pnl: number; + min_trading_days: number; +} + +export interface FirmRow { + id: number; + name: string; + username: string; + password: string; +} + +export interface FirmWithAccounts extends FirmRow { + accounts: AccountConfigRow[]; +} + +export function getFirms(): FirmWithAccounts[] { + const firms = db.prepare('SELECT * FROM firms ORDER BY id').all() as FirmRow[]; + const getAccounts = db.prepare('SELECT * FROM account_configs WHERE firm_id = ? ORDER BY id'); + return firms.map((firm) => ({ + ...firm, + accounts: getAccounts.all(firm.id) as AccountConfigRow[], + })); +} + +export function createFirm(name: string, username: string, password: string): FirmRow { + const stmt = db.prepare('INSERT INTO firms (name, username, password) VALUES (?, ?, ?)'); + const result = stmt.run(name, username, password); + return db.prepare('SELECT * FROM firms WHERE id = ?').get(result.lastInsertRowid) as FirmRow; +} + +export function deleteFirm(id: number): boolean { + const result = db.prepare('DELETE FROM firms WHERE id = ?').run(id); + return result.changes > 0; +} diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts new file mode 100644 index 0000000..3049d70 --- /dev/null +++ b/lib/tradovate-class.ts @@ -0,0 +1,403 @@ +// Create a class with a constructor that takes a username and password and sets the credentials + +import axios from 'axios'; +import type { AccountItem, AuthLoginResponse, Contract } from './tradovate-helpers'; +import { computeSec, randomUUIDV4 } from './tradovate-helpers'; + +export class TradovateClient { + private name: string; + private password: string; + private accessInfo: AuthLoginResponse; + private deviceId = randomUUIDV4(); + + public accountList: AccountItem[] = []; + public accountCashBalances: { + [accountId: number]: { + amount: number; + realizedPnL: number; + }; + } = {}; + public positions: { + [accountId: number]: { + contractId: number; + netPos: number; + netPrice: number; + timestamp: Date; + }; + } = {}; + + public daysTraded: { [accountId: number]: number } = {}; + + private ws: WebSocket; + private callbackOnSyncRequest: () => Promise; + + // Events that we sent out, and tradovate gives us a response for the id we sent out + private directEventCallbacks: { + [id: number]: (response: any) => void; + } = {}; + private indirectEventCallbacks: { + entityType: + | 'order' + | 'orderVersion' + | 'auditUserAction' + | 'command' + | 'commandReport' + | 'fill' + | 'fillFee' + | 'executionReport' + | 'cashBalance'; + eventType: 'Created' | 'Updated'; + // Since the entity is not always the same, we need a validator to check if the response is the one we are looking for + validator: (response: any) => boolean; + callback: (response: any) => void; + }[] = []; + + constructor(name: string, password: string, callbackOnSyncRequest: () => Promise) { + this.name = name; + this.password = password; + this.callbackOnSyncRequest = callbackOnSyncRequest; + + this.login().then((res) => { + if (!res?.accessToken) { + console.log('Failed to login', res); + return; + } + + console.log('Logged in', res); + + // check every 2 minutes if the access token is expired + setInterval(() => { + // If we are within 15 minutes of the expiration time, renew the access token + if ( + new Date(res.expirationTime).getTime() < + new Date().getTime() + 15 * 60 * 1000 + ) { + this.renewAccessToken(); + } + }, 2 * 60 * 1000); + + this.accessInfo = res; + + const randomnumber = Math.random().toString(36).substring(2, 15); + this.ws = new WebSocket(`wss://demo.tradovateapi.com/v1/websocket?r=${randomnumber}`); + + this.ws.onopen = () => { + console.log('Connected to websocket'); + this.ws.send('authorize\n2\n\n' + this.accessInfo.accessToken); + this.directEventCallbacks[2] = (response: any) => { + // Once authorize, start syncing every 60 seconds + this.requestAccountUpdates(); + setInterval(() => this.requestAccountUpdates(), 60000); + + // Every 2.5 seconds send a heartbeat + setInterval(() => { + this.ws.send('[]'); + }, 2500); + }; + }; + + this.ws.onmessage = (event) => { + if (event.data[0] === 'a') { + const dataString = event.data.slice(1); + + const data: ( + | { + i?: number; // Id + s?: number; // Status + d?: any; // Data + } + | { + e?: string; + d?: { + entityType: + | 'order' + | 'orderVersion' + | 'auditUserAction' + | 'command' + | 'commandReport'; + eventType: 'Created' | 'Updated'; + entity: any; + }; + } + )[] = JSON.parse(dataString); + + for (const response of data) { + if ( + 'i' in response && + response.i && + this.directEventCallbacks[response.i] + ) { + this.directEventCallbacks[response.i](response.d); + } + + if ('e' in response && response.e) { + // Handle callbacks + for (const info of this.indirectEventCallbacks) { + if ( + response.d?.entityType === info?.entityType && + response.d?.eventType === info?.eventType && + info?.validator(response.d.entity) + ) { + info?.callback(response.d.entity); + } + } + + // console.log('No callback found', response.d); + } + } + } else if (event.data[0] === 'h') { + // Heartbeat response + } + }; + + this.ws.onerror = (event) => { + console.error('Error on websocket', event); + }; + }); + } + + private async requestAccountUpdates(): Promise { + this.directEventCallbacks[3] = (response: any) => { + + // Syncing DLL or MLL hit + const riskStatusById: { [id: number]: { liquidateOnly?: string } } = ( + response.accountRiskStatuses || [] + ).reduce((acc: any, item: any) => { + acc[item.id] = item; + return acc; + }, {}); + + this.accountList = (response.accounts as AccountItem[]).map((account) => { + if (riskStatusById[account.id]?.liquidateOnly) { + return { ...account, active: false }; + } + return account; + }); + + this.accountCashBalances = response.cashBalances.reduce( + ( + acc: { + [accountId: number]: { amount: number; realizedPnL: number }; + }, + item: { accountId: number; amount: number; realizedPnL: number } + ) => { + acc[item.accountId] = { + amount: item.amount, + realizedPnL: item.realizedPnL, + }; + return acc; + }, + {} as { [accountId: number]: { amount: number; realizedPnL: number } } + ); + this.positions = response.positions + .filter((item) => item.netPos !== 0) + .reduce( + ( + acc: { + [accountId: number]: { + contractId: number; + netPos: number; + netPrice: number; + timestamp: Date; + }; + }, + item: { + accountId: number; + contractId: number; + netPos: number; + netPrice: number; + timestamp: Date; + } + ) => { + acc[item.accountId] = { + contractId: item.contractId, + netPos: item.netPos, + netPrice: item.netPrice, + timestamp: new Date(item.timestamp), + }; + return acc; + }, + {} as { + [accountId: number]: { + contractId: number; + netPos: number; + netPrice: number; + timestamp: Date; + }; + } + ); + + this.callbackOnSyncRequest(); + this.fetchDaysTraded(); + }; + + this.ws.send('user/syncrequest\n3\n\n{"splitResponses":false}'); + } + + private async fetchDaysTraded(): Promise { + if (!this.accessInfo?.accessToken) return; + + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - 28); + + for (const account of this.accountList) { + try { + const res = await axios.get( + `https://demo.tradovateapi.com/v1/fill/ldeps?masterid=${account.id}`, + { headers: { Authorization: `Bearer ${this.accessInfo.accessToken}` } } + ); + const fills: { timestamp: string }[] = res.data ?? []; + const tradingDays = new Set( + fills + .filter((f) => new Date(f.timestamp) >= cutoff) + .map((f) => new Date(f.timestamp).toDateString()) + ); + this.daysTraded[account.id] = tradingDays.size; + } catch (err) { + console.error(`[fetchDaysTraded] account ${account.id}`, err); + this.daysTraded[account.id] = 0; + } + } + } + + private async login(): Promise { + const chl = '' + (Date.now() - 1581e9); + const sec = computeSec(this.name, this.password, this.deviceId, chl); + + const generateEncodedPassword = (e) => { + const { name: t, password: n } = e, + o = t.length % n.length, + r = (n.slice(o) + n.slice(0, o)).split('').reverse().join(''); + return btoa(r); + }; + + const res = await axios.post('https://live.tradovateapi.com/v1/auth/accesstokenrequest', { + name: this.name, + password: generateEncodedPassword({ name: this.name, password: this.password }), + environment: 'demo', + appId: 'tradovate_trader(web)', + appVersion: '3.251205.0', + deviceId: this.deviceId, + cid: '1', + chl: chl, + sec: sec, + enc: true, + }); + + if (res.status !== 200) { + console.error('Failed to login', res.data); + } + + return res.data; + } + + private async renewAccessToken(): Promise { + const res = await axios.get('https://live.tradovateapi.com/v1/auth/renewAccessToken', { + headers: { + Authorization: `Bearer ${this.accessInfo.accessToken}`, + }, + }); + + if (res.status !== 200) { + console.error('Failed to renew access token', res.data); + } + + console.log('Renewed access token'); + this.accessInfo = res.data; + } + + async getAccountList(): Promise { + if (!this.accessInfo?.accessToken) { + console.log('Not authenticated'); + return []; + } + + const res = await axios.get('https://demo.tradovateapi.com/v1/account/list', { + headers: { + Authorization: `Bearer ${this.accessInfo.accessToken}`, + }, + }); + + return res.data; + } + + async requestContractsFromSocket(names: string[]): Promise<{ + [name: string]: Contract; + }> { + if (!this.ws) { + console.log('Websocket not connected'); + return {}; + } + + return new Promise((resolve) => { + this.directEventCallbacks[10] = (response: any) => { + resolve( + (response || []).reduce((acc: { [name: string]: Contract }, item: Contract) => { + return { ...acc, [item.name]: item }; + }, {} as { [name: string]: Contract }) + ); + }; + this.ws.send(`contract/finds\n10\nnames=${names.join(',')}`); + }); + } + + async sendOrder( + accountId: number, + contractId: number, + quantity: number, + action: 'Buy' | 'Sell', + orderType: 'Market' | 'Limit', + price?: number + ): Promise { + if (!this.ws) { + console.log('Websocket not connected'); + return; + } + + this.ws.send( + `user/registeraudituseraction\n25\n\n${JSON.stringify({ + accountId: accountId, + actionType: action + orderType, + details: `DOM MESZ5: Buy ${orderType}, Buy ${quantity} ${orderType}${ + price ? ` ${price}` : '' + }, TIF Day`, + })}` + ); + + return new Promise((resolve, reject) => { + this.directEventCallbacks[26] = (response: any) => { + // console.log('Order id', response?.orderId); + console.log('Order placed, order id: ', response); + + // TODO: Implement for limit orders and rejected market orders + + // If we don't get a response within 5 seconds, reject the promise + setTimeout(() => { + reject(new Error('No response from order placement')); + }, 5000); + + this.indirectEventCallbacks.push({ + entityType: 'fill', + eventType: 'Created', + validator: (item: any) => item?.orderId === response?.orderId, + callback: (response: any) => { + resolve(response); + }, + }); + }; + + this.ws.send( + `order/placeorder\n26\n\n${JSON.stringify({ + accountId: accountId, + action: action, + symbol: contractId, + orderQty: quantity, + orderType: orderType, + price: price, + timeInForce: 'Day', + text: 'DOM', + })}` + ); + }); + } +} diff --git a/lib/tradovate-helpers.ts b/lib/tradovate-helpers.ts new file mode 100644 index 0000000..fca055a --- /dev/null +++ b/lib/tradovate-helpers.ts @@ -0,0 +1,61 @@ +import crypto from 'crypto'; + +export interface AuthLoginResponse { + accessToken: string; + mdAccessToken: string; + expirationTime: string; + userStatus: string; + userId: number; + name: string; + hasLive: boolean; + hasSimPlus: boolean; + hasFunded: boolean; + hasMarketData: boolean; + requiredNonProCertification: boolean; + outdatedSentimentPolicy: boolean; + orgName: string; +} + +export interface AccountItem { + id: number; + name: string; + userId: number; + accountType: string; + restricted: boolean; + closed: boolean; + clearingHouseId: number; + riskCategoryId: number; + autoLiqProfileId: number; + marginAccountType: string; + legalStatus: string; + archived: boolean; + timestamp: string; + active: boolean; +} + +export interface Contract { + id: number; + name: string; + contractMaturityId: number; + timestamp: string; + status: string; + providerTickSize: number; +} + +export const randomUUIDV4 = () => { + return crypto.randomUUID(); +}; + +export const computeSec = (user: string, password: string, deviceId: string, chl: string) => { + const key = '035a1259-11e7-485a-aeae-9b6016579351'; + const Ct = ['chl', 'deviceId', 'name', 'password', 'appId']; + const body = { + chl: chl, + deviceId: deviceId, + name: user, + password: password, + appId: 'tradovate_trader(web)', + }; + const msg = Ct.map((k) => body[k] ?? '').join(''); + return crypto.createHmac('sha256', key).update(msg, 'utf8').digest('hex'); +}; diff --git a/next.config.ts b/next.config.ts index e9ffa30..40988d5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + serverExternalPackages: ['better-sqlite3'], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 27d2443..e571429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,15 @@ "name": "autotrader-next", "version": "0.1.0", "dependencies": { + "axios": "^1.13.6", + "better-sqlite3": "^12.6.2", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1525,6 +1528,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2393,6 +2406,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2419,6 +2438,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2436,6 +2466,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -2448,6 +2498,40 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2507,6 +2591,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2530,7 +2638,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2604,6 +2711,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2630,6 +2743,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2745,6 +2870,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2788,11 +2937,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2815,7 +2972,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2840,6 +2996,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", @@ -2927,7 +3092,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2937,7 +3101,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2975,7 +3138,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2988,7 +3150,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3246,6 +3407,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3485,6 +3647,15 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3559,6 +3730,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3610,6 +3787,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3626,11 +3823,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3691,7 +3909,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3716,7 +3933,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3757,6 +3973,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3804,7 +4026,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3876,7 +4097,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3889,7 +4109,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3905,7 +4124,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3931,6 +4149,26 @@ "hermes-estree": "0.25.1" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3968,6 +4206,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4888,7 +5138,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4918,6 +5167,39 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4935,12 +5217,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4966,6 +5253,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5070,6 +5363,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -5219,6 +5536,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5385,6 +5711,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5407,6 +5760,22 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5438,6 +5807,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -5468,6 +5861,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5608,6 +6015,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5865,6 +6292,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5895,6 +6367,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6101,6 +6582,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6208,6 +6717,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6440,6 +6961,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6555,6 +7082,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 395fcf5..87baae1 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,15 @@ "lint": "eslint" }, "dependencies": { + "axios": "^1.13.6", + "better-sqlite3": "^12.6.2", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..5a5e6b9 --- /dev/null +++ b/types.ts @@ -0,0 +1,31 @@ +export interface AccountConfig { + prefix: string; + profitTarget: number; + consistency: number; + minDayPnL: number; + minTradingDays: number; +} + +export interface FirmConfig { + id: number; + firm: string; + username: string; + password: string; + accounts: AccountConfig[]; +} + +export interface AccountState { + id: number; + name: string; + active: boolean; + amount: number; + realizedPnL: number; + daysTraded: number; + hasPosition: boolean; +} + +export interface FirmState { + firm: string; + connected: boolean; + accounts: AccountState[]; +}