computeDailyTarget now requires equityProfit (amount - accountSize) and returns null when it's undefined/null/NaN. 0 is still a valid value. - Removed totalProfit parameter (was only used as fallback) - Callers handle null by skipping the account (eligibility) or throwing (execution paths) - State API sets dailyTarget to null when no valid balance, avoids incorrect targetHit computation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
/**
|
|
* 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 equityProfit = cash.amount - cfg.account_size;
|
|
const target = computeDailyTarget(cfg.profit_target, cfg.consistency, dailyPnL, cfg.min_day_pnl, cfg.min_trading_days, equityProfit);
|
|
const targetHit = !target ? false :
|
|
((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');
|
|
}
|