From 6d3e8b6065fe9bc07c7a3fb3369a0ca06c51c433 Mon Sep 17 00:00:00 2001 From: Senofy <63175905+Senofy@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:29:16 -0500 Subject: [PATCH] Add per-firm banned symbols feature - lib/db.ts: new firm_banned_symbols table with getBannedSymbols, isSymbolBanned, and setBannedSymbol helpers - app/api/firms/[id]/banned-symbols/route.ts: GET lists banned symbols, PATCH toggles a ban for a given symbol - app/api/firms/route.ts: include bannedSymbols[] in firm list response - app/firms/[id]/settings/page.tsx: Instruments section shows all globally-enabled symbols with a red toggle to ban/unban; banned symbols display a "SYMBOL BANNED" pill next to their name - app/page.tsx: FirmRows shows "ES BANNED" (or current symbol) pill next to the firm name when the selected trade symbol is banned for that firm - lib/auto-trade.ts: skip firms entirely when the trade symbol is banned - types.ts: add bannedSymbols field to FirmConfig Co-Authored-By: Claude Sonnet 4.6 --- app/api/firms/[id]/banned-symbols/route.ts | 25 +++++++ app/api/firms/route.ts | 3 +- app/firms/[id]/settings/page.tsx | 80 ++++++++++++++++++++++ app/page.tsx | 9 ++- lib/auto-trade.ts | 8 ++- lib/db.ts | 27 ++++++++ types.ts | 1 + 7 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 app/api/firms/[id]/banned-symbols/route.ts diff --git a/app/api/firms/[id]/banned-symbols/route.ts b/app/api/firms/[id]/banned-symbols/route.ts new file mode 100644 index 0000000..77f29a6 --- /dev/null +++ b/app/api/firms/[id]/banned-symbols/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getBannedSymbols, setBannedSymbol } from '@/lib/db'; + +type Params = { params: Promise<{ id: string }> }; + +/** GET /api/firms/[id]/banned-symbols — list banned symbols for a firm */ +export async function GET(_req: NextRequest, { params }: Params) { + const { id: idStr } = await params; + const firmId = parseInt(idStr, 10); + if (isNaN(firmId)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 }); + return NextResponse.json(getBannedSymbols(firmId)); +} + +/** PATCH /api/firms/[id]/banned-symbols — { symbol, banned } to ban or unban */ +export async function PATCH(req: NextRequest, { params }: Params) { + const { id: idStr } = await params; + const firmId = parseInt(idStr, 10); + if (isNaN(firmId)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 }); + + const { symbol, banned } = await req.json() as { symbol: string; banned: boolean }; + if (!symbol) return NextResponse.json({ error: 'symbol required' }, { status: 400 }); + + setBannedSymbol(firmId, symbol, banned); + return NextResponse.json({ firmId, symbol, banned }); +} diff --git a/app/api/firms/route.ts b/app/api/firms/route.ts index ba09e29..cd04e98 100644 --- a/app/api/firms/route.ts +++ b/app/api/firms/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getFirms, createFirm } from '@/lib/db'; +import { getFirms, createFirm, getBannedSymbols } from '@/lib/db'; import { initClient } from '@/lib/clients'; export async function GET() { @@ -10,6 +10,7 @@ export async function GET() { firm: f.name, username: f.username, password: f.password, + bannedSymbols: getBannedSymbols(f.id), accounts: f.accounts.map((a) => ({ prefix: a.prefix, profitTarget: a.profit_target, diff --git a/app/firms/[id]/settings/page.tsx b/app/firms/[id]/settings/page.tsx index 3d49804..2c48594 100644 --- a/app/firms/[id]/settings/page.tsx +++ b/app/firms/[id]/settings/page.tsx @@ -86,6 +86,10 @@ export default function FirmSettingsPage() { const [rows, setRows] = useState([]); const [loadError, setLoadError] = useState(''); + // Instruments (globally enabled only) and per-firm bans + const [instruments, setInstruments] = useState([]); + const [bannedSymbols, setBannedSymbols] = useState>(new Set()); + useEffect(() => { fetch(`/api/firms/${id}`) .then((r) => { @@ -97,8 +101,34 @@ export default function FirmSettingsPage() { setRows(firm.accounts.map(initRow)); }) .catch(() => setLoadError('Failed to load firm')); + + // Load globally-enabled instruments + fetch('/api/instruments') + .then((r) => r.json()) + .then((data: { symbol: string; enabled: boolean }[]) => + setInstruments(data.filter((i) => i.enabled).map((i) => i.symbol)) + ); + + // Load this firm's banned symbols + fetch(`/api/firms/${id}/banned-symbols`) + .then((r) => r.json()) + .then((syms: string[]) => setBannedSymbols(new Set(syms))); }, [id]); + async function toggleBan(symbol: string) { + const nowBanned = !bannedSymbols.has(symbol); + setBannedSymbols((prev) => { + const next = new Set(prev); + if (nowBanned) next.add(symbol); else next.delete(symbol); + return next; + }); + await fetch(`/api/firms/${id}/banned-symbols`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ symbol, banned: nowBanned }), + }); + } + const updateRow = (rowId: number, patch: Partial) => { setRows((prev) => prev.map((r) => (r.id === rowId ? { ...r, ...patch, dirty: true } : r)) @@ -436,6 +466,56 @@ export default function FirmSettingsPage() { + {/* ── Instruments ── */} +

+ Instruments +

+
+ {instruments.length === 0 ? ( +
Loading…
+ ) : ( + + + + + + + + + {instruments.map((sym) => { + const banned = bannedSymbols.has(sym); + return ( + + + + + ); + })} + +
SymbolBanned
+ {sym} + {banned && ( + + {sym} BANNED + + )} + + +
+ )} +
+ ); diff --git a/app/page.tsx b/app/page.tsx index 9d9a3ef..c82cbf7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -161,7 +161,7 @@ function AccountRow({ account, firm, hideDead, privacy }: { account: AccountStat ); } -function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, privacy, sortKey, sortDir }: { +function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, privacy, sortKey, sortDir, tradeSymbol }: { state: FirmState; firm: FirmConfig; deleteMode: boolean; @@ -171,6 +171,7 @@ function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, priva privacy: boolean; sortKey: SortKey | null; sortDir: SortDir; + tradeSymbol: string; }) { const [open, setOpen] = useState(true); @@ -209,6 +210,11 @@ function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, priva /> )} {state.firm} + {firm.bannedSymbols?.includes(tradeSymbol) && ( + + {tradeSymbol} BANNED + + )} @@ -540,6 +546,7 @@ export default function Home() { privacy={privacy} sortKey={sortKey} sortDir={sortDir} + tradeSymbol={tradeSymbol} /> ); }) diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index e17ae96..7e577ee 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -7,7 +7,7 @@ * signal but have since exited and are now eligible. */ -import { getFirms } from './db'; +import { getFirms, isSymbolBanned } from './db'; import { getClients } from './clients'; import { computeDailyTarget, POINT_VALUES } from './trading-logic'; import { getSetting } from './db'; @@ -100,6 +100,12 @@ export async function runTrade(action: 'Buy' | 'Sell' | 'Random', symbol: string const client = clients.get(firm.id); if (!client || client.accountList.length === 0) return; + // Skip this firm entirely if the symbol is banned for it + if (isSymbolBanned(firm.id, symbol)) { + console.log(`[auto-trade] ${firm.name}: ${symbol} is banned — skipping firm`); + return; + } + const firmConfig = mapFirmConfig(firm); const contract = await client.findFrontMonthContract(symbol); diff --git a/lib/db.ts b/lib/db.ts index 12a7d6d..569d5ff 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -196,3 +196,30 @@ 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); + } +} diff --git a/types.ts b/types.ts index 3f3f60c..635fb64 100644 --- a/types.ts +++ b/types.ts @@ -14,6 +14,7 @@ export interface FirmConfig { firm: string; username: string; password: string; + bannedSymbols: string[]; accounts: AccountConfig[]; }