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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d945e0038f
commit
6d3e8b6065
@@ -86,6 +86,10 @@ export default function FirmSettingsPage() {
|
||||
const [rows, setRows] = useState<RowState[]>([]);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
|
||||
// Instruments (globally enabled only) and per-firm bans
|
||||
const [instruments, setInstruments] = useState<string[]>([]);
|
||||
const [bannedSymbols, setBannedSymbols] = useState<Set<string>>(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<RowState>) => {
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r.id === rowId ? { ...r, ...patch, dirty: true } : r))
|
||||
@@ -436,6 +466,56 @@ export default function FirmSettingsPage() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ── Instruments ── */}
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mt-10 mb-3">
|
||||
Instruments
|
||||
</h2>
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
{instruments.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center text-sm text-slate-400 italic">Loading…</div>
|
||||
) : (
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Symbol</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-slate-400">Banned</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{instruments.map((sym) => {
|
||||
const banned = bannedSymbols.has(sym);
|
||||
return (
|
||||
<tr key={sym} className="border-b border-slate-100 last:border-0">
|
||||
<td className="px-4 py-2.5 flex items-center gap-2">
|
||||
<span className="font-mono font-semibold text-slate-800">{sym}</span>
|
||||
{banned && (
|
||||
<span className="text-xs bg-red-100 text-red-700 px-1.5 py-0.5 rounded font-semibold">
|
||||
{sym} BANNED
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right">
|
||||
<button
|
||||
onClick={() => toggleBan(sym)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${
|
||||
banned ? 'bg-red-500' : 'bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
|
||||
banned ? 'translate-x-4' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user