Add master dashboard reporter and settings UI

- New reporter module that pushes firm stats (total accounts, accounts
  traded, in trade) to a configurable master dashboard every 30 seconds
- Add Instance Name and Dashboard URL fields to the settings page
- Register master_dashboard_url and instance_name in settings API
- Seed default (empty) values for new settings in db

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Senofy
2026-03-13 03:01:48 -05:00
co-authored by Claude Opus 4.6
parent 15ef2d7adc
commit f112118090
6 changed files with 159 additions and 3 deletions
+4
View File
@@ -1,6 +1,7 @@
import { TradovateClient } from './tradovate-class';
import { getFirms, getInstruments } from './db';
import { resolveContracts } from './contract-resolver';
import { startReporter } from './reporter';
// Use global to persist the client pool across HMR reloads in dev mode
const g = global as typeof globalThis & {
@@ -59,6 +60,9 @@ export function getClients(): Map<number, TradovateClient> {
// Schedule daily resolve at midnight
scheduleDailyResolve();
// Start master dashboard reporter
startReporter();
} catch (err) {
console.error('[clients] Failed to initialize clients', err);
}
+2
View File
@@ -162,6 +162,8 @@ db.exec(`
const seedSetting = db.prepare(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`);
seedSetting.run('max_concurrent_accounts', '5');
seedSetting.run('tick_interval_seconds', '60');
seedSetting.run('master_dashboard_url', '');
seedSetting.run('instance_name', '');
export function getSetting(key: string): string | null {
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
+103
View File
@@ -0,0 +1,103 @@
/**
* Reporter module — pushes aggregated state to the master dashboard.
*
* Every 30 seconds, collects firm-level stats + scheduler state and POSTs
* to the configured master_dashboard_url. Silently skips if not configured.
*/
import { getSetting } from './db';
import { getFirms } from './db';
import { getClients } from './clients';
import { getSchedulerStatus } from './auto-trade';
import { computeDailyTarget } from './trading-logic';
import type { AccountConfigRow } from './db';
function getAccountConfig(name: string, accounts: AccountConfigRow[]): AccountConfigRow | undefined {
return [...accounts]
.sort((a, b) => b.prefix.length - a.prefix.length)
.find((a) => name.startsWith(a.prefix));
}
function collectFirmStats() {
const firms = getFirms();
const clients = getClients();
return firms.map((f) => {
const client = clients.get(f.id);
if (!client || client.accountList.length === 0) {
return { firm: f.name, totalAccounts: 0, accountsTraded: 0, inTrade: 0 };
}
let totalAccounts = 0;
let accountsTraded = 0;
let inTrade = 0;
for (const acc of client.accountList) {
const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold;
if (isDead) continue;
totalAccounts++;
if (client.positions[acc.id]) {
inTrade++;
accountsTraded++;
continue;
}
// Check if target was hit today
const cfg = getAccountConfig(acc.name, f.accounts);
if (cfg) {
const dailyPnL = client.dailyPnL[acc.id] ?? [];
const totalProfit = dailyPnL.reduce((sum: number, d: { pnl: number }) => sum + d.pnl, 0);
const target = computeDailyTarget(cfg.profit_target, cfg.consistency, totalProfit, dailyPnL, cfg.min_day_pnl, cfg.min_trading_days);
const targetHit =
(target.amount === 0 && Math.abs(cash.realizedPnL) > 0 && (client.daysTraded[acc.id] ?? 0) <= cfg.min_trading_days) ||
(cash.realizedPnL >= target.amount);
if (targetHit || cash.realizedPnL !== 0) {
accountsTraded++;
}
}
}
return { firm: f.name, totalAccounts, accountsTraded, inTrade };
});
}
// HMR-safe global to avoid duplicate intervals
const _g = globalThis as typeof globalThis & { __reporterInterval?: ReturnType<typeof setInterval> };
export function startReporter() {
if (_g.__reporterInterval) {
clearInterval(_g.__reporterInterval);
}
const report = async () => {
const url = getSetting('master_dashboard_url');
const instanceName = getSetting('instance_name');
if (!url || !instanceName) return;
try {
const firms = collectFirmStats();
const scheduler = getSchedulerStatus();
await fetch(`${url}/api/report`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instance: instanceName, firms, scheduler }),
});
} catch {
// Master might be down — silently continue
}
};
_g.__reporterInterval = setInterval(report, 30_000);
// Fire first report after a short delay to let clients sync
setTimeout(report, 10_000);
console.log('[reporter] started — reporting every 30s');
}