Broker sessions rest while nothing needs them
A firm's client used to stay logged in around the clock, renewing on a fixed timer overnight and at weekends. Now, once a firm has nothing open, nothing pending and nothing left to trade before its next session for a random 15–45 minutes, its client disconnects and the dashboard shows it Resting. It logs back in a random 20–50 minutes before the next session opens, at a check-in every 3–7 Central daytime hours, or at once when trading starts, a manual leg, copy trade, manual order or AutoBuyer run needs it. Token renewals fall at random points (Tradovate 15–45 min before expiry, TopstepX every 45–150 min). Settings → Trading → Rest idle broker sessions (session_rest) turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6dd2528fe2
commit
5eb0ea7760
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getCachedFirm } from '@/lib/firm-catalog';
|
||||
import { firmInstruments, isFuturesFirm } from '@/lib/instruments';
|
||||
import { getClients } from '@/lib/clients';
|
||||
import { getClients, isWaking, wakeFirm } from '@/lib/clients';
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
@@ -27,6 +27,11 @@ export async function POST(
|
||||
const clients = getClients();
|
||||
const client = clients.get(firmId);
|
||||
if (!client) return NextResponse.json({ error: 'Client not found for firm' }, { status: 404 });
|
||||
// A resting session (lib/clients.ts) holds no login: start one, and
|
||||
// let the caller come back once it's up.
|
||||
if (wakeFirm(firmId, 'manual order') || isWaking(firmId)) {
|
||||
return NextResponse.json({ error: "the firm's session is logging back in — try again in a minute" }, { status: 503 });
|
||||
}
|
||||
|
||||
// A futures firm trades only what its instrument list (master Firms
|
||||
// page) names.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getClients } from '@/lib/clients';
|
||||
import { getClients, sessionRestEnabled, sessionRestInfo } from '@/lib/clients';
|
||||
import { getTradableFirms } from '@/lib/firm-catalog';
|
||||
import { getSchedulerHealth, getSchedulerStatus } from '@/lib/auto-trade';
|
||||
import { getBuildVersion } from '@/lib/version';
|
||||
@@ -41,20 +41,24 @@ export async function GET() {
|
||||
const entries = getTradableFirms().map((firm) => {
|
||||
const base = { firmId: firm.id, firm: firm.name, platform: firm.platform };
|
||||
const client = clients.get(firm.id);
|
||||
if (!client) return { ...base, hasClient: false, ready: false, accounts: 0 };
|
||||
if (!client) return { ...base, hasClient: false, ready: false, resting: null, accounts: 0 };
|
||||
|
||||
const ready = isClientReady(client, now);
|
||||
const accounts = client.accountList.length;
|
||||
// Logged out on purpose while nothing needs it (lib/clients.ts):
|
||||
// not ready, and nothing wrong.
|
||||
const resting = sessionRestInfo(firm.id);
|
||||
try {
|
||||
return { ...base, hasClient: true, ready, accounts, ...client.health() };
|
||||
return { ...base, hasClient: true, ready, resting, accounts, ...client.health() };
|
||||
} catch (err) {
|
||||
// One client's broken health() must not hide the rest.
|
||||
return { ...base, hasClient: true, ready, accounts, healthError: errorMessage(err) };
|
||||
return { ...base, hasClient: true, ready, resting, accounts, healthError: errorMessage(err) };
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: entries.every((e) => !e.hasClient || e.ready),
|
||||
ok: entries.every((e) => !e.hasClient || e.ready || !!e.resting),
|
||||
sessionRest: sessionRestEnabled(),
|
||||
now,
|
||||
uptimeSec: Math.round(process.uptime()),
|
||||
version: getBuildVersion(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getSetting, setSetting } from '@/lib/db';
|
||||
import { scheduleBackupPush } from '@/lib/backup-sync';
|
||||
import { getRequestUserAgent } from '@/lib/request-user-agent';
|
||||
|
||||
const VALID_KEYS = ['max_concurrent_accounts', 'tick_interval_seconds', 'master_dashboard_url', 'instance_name', 'trading_hours', 'twocaptcha_api_key', 'twocaptcha_proxy', 'master_backup_token', 'proxy_url'] as const;
|
||||
const VALID_KEYS = ['max_concurrent_accounts', 'tick_interval_seconds', 'master_dashboard_url', 'instance_name', 'trading_hours', 'twocaptcha_api_key', 'twocaptcha_proxy', 'master_backup_token', 'proxy_url', 'session_rest'] as const;
|
||||
type SettingKey = typeof VALID_KEYS[number];
|
||||
|
||||
export async function GET() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { listAssignments, loadDailyPnL } from '@/lib/db';
|
||||
import { getClients } from '@/lib/clients';
|
||||
import { getClients, restingWakeAt, sessionRestInfo } from '@/lib/clients';
|
||||
import { getAccountTypeById, getCachedCatalog } from '@/lib/firm-catalog';
|
||||
import { ruleDiffs } from '@/lib/kept-rules';
|
||||
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
|
||||
@@ -27,8 +27,10 @@ export async function GET() {
|
||||
// A firm with no accounts is a real, settled state and must not be
|
||||
// reported the same way as one that is still authenticating.
|
||||
const connected = !!client && client.syncComplete;
|
||||
const rest = sessionRestInfo(f.id);
|
||||
const resting = rest ? { resting: { since: rest.since, wakeAt: restingWakeAt(f.id) } } : {};
|
||||
if (!client || client.accountList.length === 0) {
|
||||
return { firmId: f.id, firm: f.name, connected, accounts: [] };
|
||||
return { firmId: f.id, firm: f.name, connected, ...resting, accounts: [] };
|
||||
}
|
||||
// Accounts whose rules date was set by hand — one read per firm
|
||||
// rather than one per account.
|
||||
@@ -314,7 +316,7 @@ export async function GET() {
|
||||
keptRules,
|
||||
};
|
||||
});
|
||||
return { firmId: f.id, firm: f.name, connected, accounts };
|
||||
return { firmId: f.id, firm: f.name, connected, ...resting, accounts };
|
||||
});
|
||||
|
||||
return NextResponse.json(state);
|
||||
|
||||
+12
-2
@@ -526,7 +526,17 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
|
||||
Username missing
|
||||
</span>
|
||||
)}
|
||||
{hasCredentials && !state.connected && (
|
||||
{hasCredentials && state.resting && (
|
||||
<span
|
||||
className="text-xs bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded font-semibold"
|
||||
title="Logged out while nothing can trade here; the accounts show their last sync"
|
||||
>
|
||||
{state.resting.wakeAt !== null
|
||||
? `Resting · back ${new Date(state.resting.wakeAt).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' })}`
|
||||
: 'Resting until trading starts'}
|
||||
</span>
|
||||
)}
|
||||
{hasCredentials && !state.connected && !state.resting && (
|
||||
<span className="text-xs bg-slate-200 text-slate-600 px-1.5 py-0.5 rounded font-semibold">
|
||||
Connecting…
|
||||
</span>
|
||||
@@ -594,7 +604,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
|
||||
state.accounts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-5 pl-12 text-sm text-slate-400 italic">
|
||||
{state.connected ? 'No accounts at this firm' : 'Connecting…'}
|
||||
{state.connected || state.resting ? 'No accounts at this firm' : 'Connecting…'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
|
||||
+21
-1
@@ -45,6 +45,7 @@ interface AppSettings {
|
||||
instance_name: string | null;
|
||||
instance_id: string | null;
|
||||
trading_hours: string | null;
|
||||
session_rest: string | null;
|
||||
master_backup_token: string | null;
|
||||
proxy_url: string | null;
|
||||
user_agent: string | null;
|
||||
@@ -64,6 +65,7 @@ export default function SettingsPage() {
|
||||
const [contracts, setContracts] = useState<Record<string, ResolvedContract | null>>({});
|
||||
const [maxConcurrent, setMaxConcurrent] = useState<string>('5');
|
||||
const [tradingHours, setTradingHours] = useState<string>('full_cme');
|
||||
const [sessionRest, setSessionRest] = useState<boolean>(true);
|
||||
const [masterUrl, setMasterUrl] = useState<string>('');
|
||||
const [catalog, setCatalog] = useState<{
|
||||
version: number;
|
||||
@@ -114,6 +116,7 @@ export default function SettingsPage() {
|
||||
if (s.max_concurrent_accounts != null) {
|
||||
setMaxConcurrent(s.max_concurrent_accounts);
|
||||
}
|
||||
setSessionRest(s.session_rest !== '0');
|
||||
if (s.trading_hours != null) {
|
||||
setTradingHours(s.trading_hours);
|
||||
}
|
||||
@@ -161,7 +164,7 @@ export default function SettingsPage() {
|
||||
await fetch('/api/settings', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ max_concurrent_accounts: maxConcurrent, trading_hours: tradingHours, master_dashboard_url: masterUrl, instance_name: instanceName, master_backup_token: backupToken, proxy_url: proxyUrl }),
|
||||
body: JSON.stringify({ max_concurrent_accounts: maxConcurrent, trading_hours: tradingHours, session_rest: sessionRest ? '1' : '0', master_dashboard_url: masterUrl, instance_name: instanceName, master_backup_token: backupToken, proxy_url: proxyUrl }),
|
||||
});
|
||||
setSaving(false);
|
||||
setSaved(true);
|
||||
@@ -280,6 +283,23 @@ export default function SettingsPage() {
|
||||
<option value="equity_hours">Equity Hours (8:30 AM – 3:00 PM CT)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 border-t border-slate-100 px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">Rest idle broker sessions</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
Log a firm out once nothing is open and nothing is left to trade before its next
|
||||
session (weekends, the daily break, a finished day), and back in 20–50 min before
|
||||
that session opens, with a check-in every few daytime hours. Off keeps every
|
||||
session logged in around the clock.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sessionRest}
|
||||
onChange={(e) => setSessionRest(e.target.checked)}
|
||||
className="h-4 w-4 shrink-0 accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Master Dashboard ── */}
|
||||
|
||||
Reference in New Issue
Block a user