- Auto-trade scheduler fires every 60s; uses Promise.allSettled batch so no new trades fire while any position from the current batch is open
- Commission gross-up: read entryCommission from cash.realizedPnL after fill (fallback 2.5×contracts), grossTarget = target + 2×entryCommission
- Sync gate: TradovateClient.syncComplete flag; scheduler skips tick until every client finishes initial position/balance sync
- Contracts formula changed to Math.ceil so $1500 target = 2 contracts
- Removed all fee caching (perContractFees, recentFills, fillFee handler) from tradovate-class.ts
- Removed firm_fees table, getFirmFees, upsertFirmFee from db.ts
- Deleted instrument-configs API routes; removed Fees UI from firm settings page
- /api/instruments returns full {symbol, enabled}[] objects; dashboard filters to enabled-only for trade selector
- Added auto-trade, debug, orders, settings, and trade API routes
- Instrument selector on dashboard now driven by enabled instruments from DB
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
6.9 KiB
TypeScript
155 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
|
|
interface Instrument {
|
|
symbol: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
interface AppSettings {
|
|
max_concurrent_accounts: string | null;
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
const [instruments, setInstruments] = useState<Instrument[]>([]);
|
|
const [maxConcurrent, setMaxConcurrent] = useState<string>('5');
|
|
const [saving, setSaving] = useState(false);
|
|
const [saved, setSaved] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/instruments')
|
|
.then((r) => r.json())
|
|
.then(setInstruments);
|
|
|
|
fetch('/api/settings')
|
|
.then((r) => r.json())
|
|
.then((s: AppSettings) => {
|
|
if (s.max_concurrent_accounts != null) {
|
|
setMaxConcurrent(s.max_concurrent_accounts);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
async function toggle(symbol: string, enabled: boolean) {
|
|
setInstruments((prev) =>
|
|
prev.map((i) => (i.symbol === symbol ? { ...i, enabled } : i))
|
|
);
|
|
await fetch(`/api/instruments/${symbol}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
}
|
|
|
|
async function saveSettings() {
|
|
setSaving(true);
|
|
setSaved(false);
|
|
await fetch('/api/settings', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ max_concurrent_accounts: maxConcurrent }),
|
|
});
|
|
setSaving(false);
|
|
setSaved(true);
|
|
setTimeout(() => setSaved(false), 2000);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 p-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<Link
|
|
href="/"
|
|
className="text-slate-400 hover:text-slate-600 text-sm transition-colors"
|
|
>
|
|
← Back
|
|
</Link>
|
|
<h1 className="text-2xl font-bold text-slate-900">Settings</h1>
|
|
</div>
|
|
|
|
{/* ── Trading ── */}
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-3">
|
|
Trading
|
|
</h2>
|
|
<div className="bg-white border border-slate-200 rounded-xl shadow-sm mb-8">
|
|
<div className="flex items-center justify-between px-4 py-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-800">Max concurrent accounts</p>
|
|
<p className="text-xs text-slate-400 mt-0.5">
|
|
How many accounts trade in parallel per /api/trade call
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
max={500}
|
|
value={maxConcurrent}
|
|
onChange={(e) => setMaxConcurrent(e.target.value)}
|
|
className="w-20 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm text-right font-mono text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
<button
|
|
onClick={saveSettings}
|
|
disabled={saving}
|
|
className={`text-sm px-3 py-1.5 rounded-lg font-medium transition-colors ${
|
|
saved
|
|
? 'bg-green-500 text-white'
|
|
: 'bg-blue-500 hover:bg-blue-600 text-white disabled:opacity-50'
|
|
}`}
|
|
>
|
|
{saved ? 'Saved ✓' : saving ? 'Saving…' : 'Save'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Instruments ── */}
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 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">Enabled</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{instruments.map((instr) => (
|
|
<tr key={instr.symbol} className="border-b border-slate-100 last:border-0">
|
|
<td className="px-4 py-2.5 font-mono font-semibold text-slate-800">{instr.symbol}</td>
|
|
<td className="px-4 py-2.5 text-right">
|
|
<button
|
|
onClick={() => toggle(instr.symbol, !instr.enabled)}
|
|
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${
|
|
instr.enabled ? 'bg-blue-500' : 'bg-slate-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
|
|
instr.enabled ? 'translate-x-4' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|