Old rules for older accounts, a rules date per account, and a firm cap

When the master changes an account type with "New accounts only", it now
serves the type's old rules as kept rule sets (firm.keptAccountRules), each
for accounts dated inside [openedAt, closedAt). This build applies them:

- getAccountConfig, the one rules lookup trading reads, puts a kept set's
  rule fields over the live type when the account's rules date falls in
  its window. Id, label, prefix, size and the projection fields stay the
  live type's, so every report still names the live type.
- account_assignments gains rules_date and rules_date_manual. A new row is
  dated when the machine first sees the account, on the master's clock
  when this one runs behind and never before the catalog's last change.
  Auto dates come down to the broker's evidence: the end of the first fund
  day in Chicago, or a DXtrade registration instant. Evidence is applied
  before an account's first trade. Rows from before the migration get its
  time; unpinning keeps the row and its date. Backups carry both columns.
- An account on a kept set stays on its type while its prefix and cached
  size still match, so a tie-break flip can't move it off its old rules.
- Kept entries parse through the same normalizer as live types (now
  normalizeAccountType in lib/kept-rules.ts); a malformed one rejects the
  sync like a malformed type. The cached catalog records the parser that
  stored it, and a cache from an older parser is fetched in full rather
  than trusted with ?since= (this also brings in maxConcurrentAccounts).
- Reports add keptRuleIds per firm and live counts per kept set per type.
- The dashboard row and the account page show the rules date next to the
  account name, with an "old rules" chip saying what differs. Clicking
  the date edits it inline (PUT /api/accounts/:id/rules-date), previewing
  which rules the date lands on.

Also: the master's per-firm cap on accounts traded at once
(maxConcurrentAccounts), applied on top of the machine's own max
concurrent when a batch is drawn and in copy trading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-18 22:21:55 -05:00
co-authored by Claude Opus 5.5
parent 98016b3eb3
commit c0899620d4
24 changed files with 2127 additions and 153 deletions
+2
View File
@@ -17,6 +17,7 @@ import {
import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types';
import { stagePillClasses } from '@/lib/stage-colors';
import { projectPerfectPath } from '@/lib/trading-logic';
import { RulesDate } from '@/app/components/RulesDate';
/** Reassign this account to another of its firm's account types, or hand it
* back to the auto-resolver. Mirrors the control on the dashboard. */
@@ -578,6 +579,7 @@ export default function AccountPage() {
onAssigned={() => { void load(); }}
/>
)}
<RulesDate account={account} firm={firm} onSaved={() => { void load(); }} />
<span className="text-slate-300">/</span>
<h1 className="text-slate-900 font-bold text-sm">{account.name}</h1>
{isDead && (
+46
View File
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { setRulesDate } from '@/lib/db';
import { sortableIso } from '@/lib/kept-rules';
type Params = { params: Promise<{ id: string }> };
/** A rules date may run this far ahead of the server's clock — room for a
* browser whose clock or time zone is ahead, not for dating accounts later. */
const MAX_FUTURE_MS = 24 * 60 * 60 * 1000;
function parseAccountId(raw: string): number | null {
const id = parseInt(raw, 10);
return Number.isNaN(id) ? null : id;
}
/** Set an account's rules date by hand. The date decides which of its type's
* rule sets it trades on — one dated before a firm's rule change keeps the
* old rules — and a date set here is never moved by broker evidence again.
* The trade path reads it per tick, so nothing needs re-resolving. */
export async function PUT(req: NextRequest, { params }: Params) {
const { id: idStr } = await params;
const accountId = parseAccountId(idStr);
if (accountId === null) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
const body = await req.json().catch(() => null) as { rulesDate?: unknown } | null;
const raw = body?.rulesDate;
const ms = typeof raw === 'string' ? Date.parse(raw) : NaN;
if (!Number.isFinite(ms)) {
return NextResponse.json({ error: 'rulesDate must be an ISO date' }, { status: 400 });
}
if (ms > Date.now() + MAX_FUTURE_MS) {
return NextResponse.json({ error: 'A rules date cannot be more than a day ahead' }, { status: 400 });
}
// Stored as a plain four-digit-year toISOString(), the one format every
// rules-date writer uses — lowerAutoRulesDate compares them as strings,
// so nothing before 2000 (or that doesn't print as one) gets in.
const rulesDate = sortableIso(ms);
if (!rulesDate) {
return NextResponse.json({ error: 'A rules date cannot be before 2000' }, { status: 400 });
}
if (!setRulesDate(accountId, rulesDate, true)) {
return NextResponse.json({ error: 'Unknown account' }, { status: 404 });
}
return NextResponse.json({ ok: true, rulesDate });
}
+6 -2
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { getCachedFirm } from '@/lib/firm-catalog';
import { getCachedFirm, getKeptRulesFor } from '@/lib/firm-catalog';
import { firmInstruments, instrumentsSource } from '@/lib/instruments';
import { getCredentials } from '@/lib/db';
@@ -25,7 +25,11 @@ export async function GET(
hasCredentials: !!creds,
instruments: firmInstruments(firm),
instrumentsSource: instrumentsSource(firm),
accounts: firm.accountTypes,
// Same shape as GET /api/firms: each type with its old rule sets' windows.
accounts: firm.accountTypes.map((a) => ({
...a,
kept: getKeptRulesFor(a.id).map((k) => ({ id: k.id, openedAt: k.openedAt, closedAt: k.closedAt })),
})),
catalogUpdatedAt: firm.updatedAt,
});
}
+11 -2
View File
@@ -1,8 +1,13 @@
import { NextResponse } from 'next/server';
import { firmInstruments, instrumentsSource } from '@/lib/instruments';
import { getCachedCatalog, hasPlatformCredentials } from '@/lib/firm-catalog';
import { getCachedCatalog, getKeptRulesFor, hasPlatformCredentials } from '@/lib/firm-catalog';
import { getCredentials } from '@/lib/db';
import type { FirmConfig } from '@/types';
import type { FirmConfig, KeptRulesWindow } from '@/types';
/** The windows of a live account type's old rule sets, by closedAt. */
function keptWindows(accountTypeId: string): KeptRulesWindow[] {
return getKeptRulesFor(accountTypeId).map((k) => ({ id: k.id, openedAt: k.openedAt, closedAt: k.closedAt }));
}
/**
* The firms this machine knows about — the master's catalog, annotated with
@@ -14,6 +19,9 @@ import type { FirmConfig } from '@/types';
*
* The stored password is deliberately not returned. It used to be, and nothing
* ever read it.
*
* Each account type carries the windows of the old rule sets it keeps (`kept`)
* so the dashboard's rules-date control can say which rules a date lands on.
*/
export async function GET() {
try {
@@ -48,6 +56,7 @@ export async function GET() {
withdrawalStages: a.withdrawalStages,
dailyLossLimit: a.dailyLossLimit ?? 0,
tradesEveryDay: a.tradesEveryDay === true,
kept: keptWindows(a.id),
})),
catalogUpdatedAt: firm.updatedAt,
};
+25 -2
View File
@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import { loadDailyPnL } from '@/lib/db';
import { listAssignments, loadDailyPnL } from '@/lib/db';
import { getClients } from '@/lib/clients';
import { getCachedCatalog } from '@/lib/firm-catalog';
import { getAccountTypeById, getCachedCatalog } from '@/lib/firm-catalog';
import { ruleDiffs } from '@/lib/kept-rules';
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
import { historyFresh } from '@/lib/platform-client';
import { rankNextLeg } from '@/lib/leg-estimate';
@@ -29,6 +30,11 @@ export async function GET() {
if (!client || client.accountList.length === 0) {
return { firmId: f.id, firm: f.name, connected, accounts: [] };
}
// Accounts whose rules date was set by hand — one read per firm
// rather than one per account.
const rulesDateByHand = new Set(listAssignments(f.id)
.filter((row) => row.rules_date_manual === 1)
.map((row) => row.account_id));
const accounts = client.accountList.map((acc) => {
const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
@@ -84,6 +90,20 @@ export async function GET() {
const ambiguous = cfg
? matchAccountType(acc.name, inferredAccountSize, f.accountTypes).ambiguous
: false;
// An account on one of its type's old rule sets: cfg already
// carries those rules; say which set, and how it differs from
// the type's current rules.
const live = resolved.keptRules && resolved.accountTypeId
? getAccountTypeById(resolved.accountTypeId)
: undefined;
const keptRules: AccountState['keptRules'] = resolved.keptRules && cfg && live
? {
id: resolved.keptRules.id,
openedAt: resolved.keptRules.openedAt,
closedAt: resolved.keptRules.closedAt,
differs: ruleDiffs(cfg, live),
}
: null;
let targetHit = false;
let dailyTarget: { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } | null = null;
let remainingToday: number | null = null;
@@ -289,6 +309,9 @@ export async function GET() {
accountType: cfg,
inferredAccountSize,
accountTypeAmbiguous: ambiguous,
rulesDate: resolved.rulesDate,
rulesDateManual: rulesDateByHand.has(acc.id),
keptRules,
};
});
return { firmId: f.id, firm: f.name, connected, accounts };
+317
View File
@@ -0,0 +1,317 @@
'use client';
import { useRef, useState, type MouseEvent } from 'react';
import type { AccountState, FirmAccountType, FirmConfig, KeptRulesWindow } from '@/types';
import { evidenceDay, pickedDayRulesDate, pickKeptRules, unchangedDaySaves } from '@/lib/kept-rules';
/**
* An account's rules date, shown next to its name, and the "old rules" chip.
*
* When a firm's rules change on the master with "New accounts only", accounts
* dated before the change keep the old rules. The date defaults to when the
* account started (or when this machine first saw it); clicking it edits it
* inline — a past day is stored as its start, today as now (the master's
* "today means from now") — with a preview of which rules that lands on.
* Used on the dashboard row and the account page.
*/
/** What the master's Firms page calls each rule field (its lib/kept-rules.ts
* ruleLabel), capitalized — so an "old rules" difference reads the same on
* both. */
const RULE_LABELS: Record<string, string> = {
profitTarget: 'Profit target',
consistency: 'Consistency',
minDayPnL: 'Min day P&L',
minTradingDays: 'Min trading days',
maxLoss: 'Max drawdown',
maxPositionSize: 'Max contracts',
targetSameEquity: 'Same equity after first w/d',
splitEnabled: 'Split legs',
minLegs: 'Min legs',
firstDayStop: 'Day 1 $1k stop',
minDayStop: 'Min-day 2× stop',
combineCmeHolidays: 'CME holiday combining',
withdrawalStages: 'Withdrawal stages',
payoutable: 'Payoutable',
trailingDrawdown: 'Trailing drawdown',
dailyLossLimit: 'Hard daily loss',
};
function money(n: number): string {
return `$${n.toLocaleString('en-US', { maximumFractionDigits: 2 })}`;
}
/** One rule value the way the Firms page shows it: money as $3,000, ratios as
* 50%, switches on/off, stages by count. */
function formatRule(field: string, value: unknown): string {
if (value === undefined || value === null) return 'unset';
if (typeof value === 'boolean') return value ? 'on' : 'off';
if (Array.isArray(value)) {
return value.length === 0 ? 'none' : `${value.length} stage${value.length === 1 ? '' : 's'}`;
}
if (typeof value !== 'number') return JSON.stringify(value);
switch (field) {
case 'consistency':
return value === 0 ? 'none' : `${Math.round(value * 100)}%`;
case 'minDayPnL':
return value <= -999 ? 'none' : money(value);
case 'maxPositionSize':
return value === 0 ? 'no limit' : String(value);
case 'minLegs':
return value === 0 ? 'auto' : String(value);
case 'trailingDrawdown':
return value === 0 ? 'unset' : money(value);
case 'maxLoss':
case 'dailyLossLimit':
return value === 0 ? 'none' : money(Math.abs(value));
case 'profitTarget':
return money(value);
default:
return String(value);
}
}
/** "Consistency 50% (type now 40%)". Two edits that summarize alike — three
* stages either way — just say the field changed. */
function describeDiff(d: { field: string; kept: unknown; now: unknown }): string {
const label = RULE_LABELS[d.field] ?? d.field;
const kept = formatRule(d.field, d.kept);
const now = formatRule(d.field, d.now);
return kept === now ? `${label} changed since` : `${label} ${kept} (type now ${now})`;
}
/** "Oct 18, 2026, 2:05 PM". */
function fullWhen(iso: string): string {
return new Date(iso).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit',
});
}
/** "Oct 12", with the year when it isn't this year's; `time` adds "2:05 PM". */
function shortWhen(iso: string, time = false): string {
const d = new Date(iso);
const opts: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
if (d.getFullYear() !== new Date().getFullYear()) opts.year = 'numeric';
if (time) {
opts.hour = 'numeric';
opts.minute = '2-digit';
}
return d.toLocaleString('en-US', opts);
}
/** Which rules dates a kept set covers, for the chip's tooltip. */
function windowText(k: KeptRulesWindow): string {
return k.openedAt
? `kept for accounts dated from ${fullWhen(k.openedAt)} to before ${fullWhen(k.closedAt)}`
: `kept for accounts dated before ${fullWhen(k.closedAt)}`;
}
/** The same, short, for the editor's preview line. */
function windowShort(k: KeptRulesWindow): string {
return k.openedAt
? `${shortWhen(k.openedAt, true)} – ${shortWhen(k.closedAt, true)}`
: `before ${shortWhen(k.closedAt, true)}`;
}
/** "Oct 12" for a calendar day (YYYY-MM-DD) itself, whatever the browser's
* zone, with the year when it isn't this year's; `year` always adds it. */
function dayLabel(day: string, year = false): string {
const d = new Date(`${day}T12:00:00.000Z`);
const opts: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', timeZone: 'UTC' };
if (year || d.getUTCFullYear() !== new Date().getFullYear()) opts.year = 'numeric';
return d.toLocaleString('en-US', opts);
}
/** YYYY-MM-DD of a moment in the browser's time zone — a date input's value. */
function localDay(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
/** A date input's value as that day's local midnight; null when unparsable. */
function localMidnight(day: string): Date | null {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
if (!m) return null;
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
return Number.isNaN(d.getTime()) ? null : d;
}
/** The rules date a picked day stores (lib/kept-rules.ts pickedDayRulesDate):
* today as now, any other day as its start. null when unparsable. */
function storedFor(day: string): string | null {
return pickedDayRulesDate(day, {
today: localDay(new Date()),
nowMs: Date.now(),
localMidnightMs: localMidnight(day)?.getTime() ?? NaN,
});
}
/** The amber chip on an account trading on one of its type's old rule sets. */
export function OldRulesChip({ keptRules }: { keptRules: NonNullable<AccountState['keptRules']> }) {
const lines = keptRules.differs.length > 0
? keptRules.differs.map(describeDiff)
: ['Same as the type\'s current rules'];
return (
<span
className="inline-block px-1.5 py-0.5 rounded text-[10px] font-semibold bg-amber-100 text-amber-700 shrink-0 font-sans whitespace-nowrap"
title={`Old rules — ${windowText(keptRules)}\n${lines.join('\n')}`}
>
old rules
</span>
);
}
/** The rules date and, when the account is on an old rule set, its chip.
* `firm` names the firm whose type's kept windows the editor's preview
* reads (none without it) — fetched fresh each time the editor opens, as a
* page's firm listing can predate a rule change on the master; `onSaved`
* should refresh /api/state. */
export function RulesDate({ account, firm, onSaved }: {
account: AccountState;
firm: FirmConfig | null;
onSaved: () => void;
}) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState('');
// The date and day the editor opened on: the 5s state poll can move the
// date underneath (evidence lowering it), and an untouched ✓ must still
// mean "leave it", not "store the day it showed when opened".
const [opened, setOpened] = useState<{ date: string; day: string } | null>(null);
// The type's kept windows as the server has them now; null while loading
// or when unknown.
const [kept, setKept] = useState<KeptRulesWindow[] | null>(null);
const openCount = useRef(0);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
// No assignment row yet (the resolver hasn't seen the account): nothing to
// show or set.
if (account.rulesDate === null) return null;
const rulesDate = account.rulesDate;
// Broker evidence is the end of a Chicago day: shown as that day in any
// zone, not as the next one east of Chicago.
const brokerDay = evidenceDay(rulesDate);
const currentDay = brokerDay ?? localDay(new Date(rulesDate));
// What ✓ would store, and the rules it lands on.
const chosen = editing ? storedFor(draft) : null;
const landsOn = kept && chosen ? pickKeptRules(kept, chosen) : null;
const preview = !kept || !chosen
? null
: landsOn ? `→ old rules (${windowShort(landsOn)})` : '→ current rules';
function open(e: MouseEvent) {
e.stopPropagation();
setDraft(currentDay);
setOpened({ date: rulesDate, day: currentDay });
setError('');
setKept(null);
setEditing(true);
const ticket = ++openCount.current;
const fallback = firm?.accounts.find((t) => t.id === account.accountTypeId)?.kept ?? null;
if (!firm || !account.accountTypeId) return;
void (async () => {
let windows: KeptRulesWindow[] | null = fallback;
try {
const res = await fetch(`/api/firms/${encodeURIComponent(firm.id)}`);
if (res.ok) {
const data = await res.json() as { accounts?: FirmAccountType[] };
windows = data.accounts?.find((t) => t.id === account.accountTypeId)?.kept ?? null;
}
} catch {
// Offline: the page's copy is the best there is.
}
if (ticket === openCount.current) setKept(windows);
})();
}
async function save() {
const value = storedFor(draft);
if (!value || !opened) {
setError('Pick a date');
return;
}
// An unchanged day keeps the exact date, unless storing it moves the
// account to other rules — what the preview then says ✓ does.
if (draft === opened.day && !unchangedDaySaves(kept, opened.date, value)) {
setEditing(false);
return;
}
setSaving(true);
setError('');
try {
const res = await fetch(`/api/accounts/${account.id}/rules-date`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rulesDate: value }),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'Failed');
setEditing(false);
onSaved();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed');
} finally {
setSaving(false);
}
}
if (editing) {
return (
<span className="inline-flex items-center gap-1 font-sans font-normal" onClick={(e) => e.stopPropagation()}>
<input
type="date"
value={draft}
max={localDay(new Date())}
autoFocus
disabled={saving}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') void save();
if (e.key === 'Escape') setEditing(false);
}}
className="border border-slate-200 rounded-md px-1 py-0.5 text-[11px] bg-white text-slate-700 focus:outline-none focus:ring-1 focus:ring-blue-400"
/>
<button
type="button"
onClick={() => void save()}
disabled={saving}
className="px-1.5 py-0.5 bg-blue-100 hover:bg-blue-200 disabled:opacity-50 text-blue-700 text-[11px] font-semibold rounded-md transition-colors"
title="Save the rules date"
>
{saving ? '…' : '✓'}
</button>
<button
type="button"
onClick={() => setEditing(false)}
disabled={saving}
className="px-1.5 py-0.5 bg-slate-100 hover:bg-slate-200 disabled:opacity-50 text-slate-600 text-[11px] font-semibold rounded-md transition-colors"
title="Cancel"
>
✕
</button>
{preview && <span className="text-[11px] text-slate-400 whitespace-nowrap">{preview}</span>}
{error && <span className="text-[11px] text-red-500 whitespace-nowrap">{error}</span>}
</span>
);
}
return (
<span className="inline-flex items-center gap-1.5 font-sans font-normal">
<button
type="button"
onClick={open}
className="inline-flex items-center gap-1 text-[11px] text-slate-400 hover:text-slate-600 tabular-nums whitespace-nowrap transition-colors"
title={'Rules date — accounts dated before a firm\'s rule change keep the old rules. Click to change.\n'
+ (brokerDay
? `End of ${dayLabel(brokerDay, true)} in Chicago${account.rulesDateManual ? '' : ' — when the broker shows it opened'}`
: fullWhen(rulesDate))
+ (account.rulesDateManual ? ' (set by hand)' : '')}
>
{brokerDay ? dayLabel(brokerDay) : shortWhen(rulesDate)}
{account.rulesDateManual && (
<span className="w-1 h-1 rounded-full bg-blue-400 shrink-0" title="Set by hand" />
)}
</button>
{account.keptRules && <OldRulesChip keptRules={account.keptRules} />}
</span>
);
}
+23 -17
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import type { FirmConfig, FirmState, AccountState } from '@/types';
import { stagePillClasses } from '@/lib/stage-colors';
import { RulesDate } from '@/app/components/RulesDate';
type SortKey = 'name' | 'balance' | 'dayPnL' | 'daysTraded' | 'target' | 'status';
type SortDir = 'asc' | 'desc';
@@ -342,12 +343,14 @@ function WipeAllButton() {
);
}
function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
function AccountRow({ account, firm, hideDead, privacy, onAssigned, onStateChanged }: {
account: AccountState;
firm: FirmConfig;
hideDead: boolean;
privacy: boolean;
onAssigned: () => void;
/** Refetches /api/state — after a rules date is saved. */
onStateChanged: () => void;
}) {
const cfg = account.accountType;
const dead = isAccountDead(account);
@@ -359,8 +362,9 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
return (
<tr className={`border-b border-slate-100 hover:bg-slate-50 transition-colors${!account.active || dead ? ' opacity-50' : ''}`}>
<td className="px-4 py-3">
<div className="flex items-center gap-2 font-medium text-slate-800 pl-4 font-mono">
{maskName(account.name, privacy)}
<div className="flex items-center gap-2 pl-4">
<span className="font-medium text-slate-800 font-mono">{maskName(account.name, privacy)}</span>
<RulesDate account={account} firm={firm} onSaved={onStateChanged} />
</div>
</td>
<td className="px-4 py-3 text-slate-600 tabular-nums">${fmt(account.amount)}</td>
@@ -440,7 +444,7 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
);
}
function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbol, onChanged }: {
function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbol, onChanged, onStateChanged }: {
state: FirmState;
firm: FirmConfig;
hideDead: boolean;
@@ -449,6 +453,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
sortDir: SortDir;
tradeSymbol: string;
onChanged: () => void;
onStateChanged: () => void;
}) {
const [open, setOpen] = useState(true);
const [confirmCreds, setConfirmCreds] = useState(false);
@@ -601,6 +606,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
hideDead={hideDead}
privacy={privacy}
onAssigned={onChanged}
onStateChanged={onStateChanged}
/>
))
)
@@ -673,6 +679,18 @@ export default function Home() {
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
}
};
const fetchScheduler = async () => {
try {
const res = await fetch('/api/auto-trade');
@@ -701,19 +719,6 @@ export default function Home() {
fetchConfig();
fetchCatalog();
fetchScheduler();
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 stateInterval = setInterval(fetchState, 5000);
const schedulerInterval = setInterval(fetchScheduler, 5000);
@@ -965,6 +970,7 @@ export default function Home() {
sortDir={sortDir}
tradeSymbol={tradeSymbol}
onChanged={fetchConfig}
onStateChanged={fetchState}
/>
);
})