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 ); CREATE TABLE IF NOT EXISTS instruments ( symbol TEXT PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 1 ); `); // Migration: add account_size column if it doesn't exist yet try { db.exec('ALTER TABLE account_configs ADD COLUMN account_size REAL NOT NULL DEFAULT 50000'); } catch { // Column already exists } // Migration: add max_loss column (0 = no max loss limit) try { db.exec('ALTER TABLE account_configs ADD COLUMN max_loss REAL NOT NULL DEFAULT 0'); } catch { // Column already exists } // Migration: add max_position_size column (0 = no limit) try { db.exec('ALTER TABLE account_configs ADD COLUMN max_position_size INTEGER NOT NULL DEFAULT 0'); } catch { // Column already exists } // ── Interfaces ───────────────────────────────────────────────────────────── export interface AccountConfigRow { id: number; firm_id: number; prefix: string; profit_target: number; consistency: number; min_day_pnl: number; min_trading_days: number; account_size: number; max_loss: number; max_position_size: number; } export interface FirmRow { id: number; name: string; username: string; password: string; } export interface FirmWithAccounts extends FirmRow { accounts: AccountConfigRow[]; } // ── Firms ─────────────────────────────────────────────────────────────────── 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 getFirmById(id: number): FirmWithAccounts | undefined { const firm = db.prepare('SELECT * FROM firms WHERE id = ?').get(id) as FirmRow | undefined; if (!firm) return undefined; const accounts = db.prepare('SELECT * FROM account_configs WHERE firm_id = ? ORDER BY id').all(id) as AccountConfigRow[]; return { ...firm, accounts }; } 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; } // ── Account Configs ───────────────────────────────────────────────────────── export function createAccountConfig(firmId: number, data: { prefix: string; profitTarget: number; consistency: number; minDayPnL: number; minTradingDays: number; accountSize: number; maxLoss: number; maxPositionSize: number; }): AccountConfigRow { const stmt = db.prepare( 'INSERT INTO account_configs (firm_id, prefix, profit_target, consistency, min_day_pnl, min_trading_days, account_size, max_loss, max_position_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' ); const result = stmt.run(firmId, data.prefix, data.profitTarget, data.consistency, data.minDayPnL, data.minTradingDays, data.accountSize, data.maxLoss, data.maxPositionSize); return db.prepare('SELECT * FROM account_configs WHERE id = ?').get(result.lastInsertRowid) as AccountConfigRow; } export function deleteAccountConfig(id: number): boolean { const result = db.prepare('DELETE FROM account_configs WHERE id = ?').run(id); return result.changes > 0; } export function updateAccountConfig(id: number, data: { prefix: string; profitTarget: number; consistency: number; minDayPnL: number; minTradingDays: number; accountSize: number; maxLoss: number; maxPositionSize: number; }): boolean { const result = db.prepare(` UPDATE account_configs SET prefix = ?, profit_target = ?, consistency = ?, min_day_pnl = ?, min_trading_days = ?, account_size = ?, max_loss = ?, max_position_size = ? WHERE id = ? `).run(data.prefix, data.profitTarget, data.consistency, data.minDayPnL, data.minTradingDays, data.accountSize, data.maxLoss, data.maxPositionSize, id); return result.changes > 0; } // ── Settings ───────────────────────────────────────────────────────────────── db.exec(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); `); // Seed defaults if missing const seedSetting = db.prepare(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`); seedSetting.run('max_concurrent_accounts', '5'); seedSetting.run('tick_interval_seconds', '60'); export function getSetting(key: string): string | null { const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined; return row?.value ?? null; } export function setSetting(key: string, value: string): void { db.prepare(`INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(key, value); } // ── Instruments ────────────────────────────────────────────────────────────── const SYMBOLS = ['NQ','MNQ','ES','MES','YM','MYM','RTY','M2K','GC','MGC','SI','CL','MCL','NG','ZB','ZN','ZF','6E','6J','6B']; // Seed instruments table if empty const instrCount = (db.prepare('SELECT COUNT(*) as count FROM instruments').get() as { count: number }).count; if (instrCount === 0) { const ins = db.prepare('INSERT INTO instruments (symbol, enabled) VALUES (?, 1)'); for (const s of SYMBOLS) ins.run(s); } export interface InstrumentRow { symbol: string; enabled: boolean; } export function getInstruments(): InstrumentRow[] { return (db.prepare('SELECT symbol, enabled FROM instruments ORDER BY symbol').all() as { symbol: string; enabled: number }[]) .map((r) => ({ symbol: r.symbol, enabled: r.enabled === 1 })); } export function setInstrumentEnabled(symbol: string, enabled: boolean): boolean { const result = db.prepare('UPDATE instruments SET enabled = ? WHERE symbol = ?').run(enabled ? 1 : 0, symbol); return result.changes > 0; } // ── Firm banned symbols ─────────────────────────────────────────────────────── db.exec(` CREATE TABLE IF NOT EXISTS firm_banned_symbols ( firm_id INTEGER NOT NULL REFERENCES firms(id) ON DELETE CASCADE, symbol TEXT NOT NULL, PRIMARY KEY (firm_id, symbol) ); `); export function getBannedSymbols(firmId: number): string[] { return (db.prepare('SELECT symbol FROM firm_banned_symbols WHERE firm_id = ? ORDER BY symbol').all(firmId) as { symbol: string }[]) .map((r) => r.symbol); } export function isSymbolBanned(firmId: number, symbol: string): boolean { return !!db.prepare('SELECT 1 FROM firm_banned_symbols WHERE firm_id = ? AND symbol = ?').get(firmId, symbol); } export function setBannedSymbol(firmId: number, symbol: string, banned: boolean): void { if (banned) { db.prepare('INSERT OR IGNORE INTO firm_banned_symbols (firm_id, symbol) VALUES (?, ?)').run(firmId, symbol); } else { db.prepare('DELETE FROM firm_banned_symbols WHERE firm_id = ? AND symbol = ?').run(firmId, symbol); } }