- lib/instrument-specs.ts (pure, tested): the CME specs — point value, tick size, one-tick spread, Yahoo ticker — replacing POINT_VALUES; the draw's maths; the 12-hour hold cap; the micro for drain and extra-day legs; reading a firm's list (the master's, or the old rules against an older master: this machine's enabled set less bans, the symbol map). - lib/instruments.ts: the one module the trading paths ask. Measures every future's daily move from Yahoo's hourly candles once per trade date, and shows entry cost (a tick of spread plus Tradovate's fee parameters or TopstepX's last measured fill) without ever choosing by it. - Futures Auto draws one root per batch from the batch firm's list: movement per dollar of spread early in the session with a 10% floor each, sliding to the fastest movers by the last entry; nothing expected to hold 12h or more; micros only if nothing else is listed. Every draw logs its odds. - The batch is chosen before its instrument, and a group that can't trade anything yields to the next. Copy trade matches the held contract against the listed roots and won't copy an instrument it can't identify. Manual legs, manual orders and drain/extra-day legs follow the lists. - DXtrade firms rank their own list whatever root was drawn. - Capabilities, the dashboard's trade bar and contract resolves follow the lists; the trade bar and scheduler default to Auto. The master link reconnects when master_dashboard_url changes, and the catalog drops ?since after a switch so two masters' versions can't collide. - Reports carry each futures firm's instrumentStats for the master's Firms page; GET /api/instruments shows them (?simulate=N runs the draw, no orders); the Settings page shows the lists read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
7.9 KiB
TypeScript
165 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter, useParams } from 'next/navigation';
|
|
import type { FirmConfig } from '@/types';
|
|
|
|
/**
|
|
* Read-only view of one firm's account types and banned symbols.
|
|
*
|
|
* Both are owned by the master dashboard now. This page renders from the local
|
|
* catalog cache rather than linking straight out to the master, because the
|
|
* moment you most want to know what an account type says is the moment the
|
|
* master is unreachable.
|
|
*/
|
|
|
|
function fmtSize(n: number) {
|
|
return n >= 1000 ? `${n / 1000}k` : String(n);
|
|
}
|
|
|
|
function fmtMoney(n: number) {
|
|
return `$${n.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
|
|
}
|
|
|
|
export default function FirmSettingsPage() {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const id = params.id as string;
|
|
|
|
const [firm, setFirm] = useState<FirmConfig | null>(null);
|
|
const [masterUrl, setMasterUrl] = useState<string | null>(null);
|
|
const [loadError, setLoadError] = useState('');
|
|
|
|
useEffect(() => {
|
|
fetch(`/api/firms/${id}`)
|
|
.then((r) => {
|
|
if (!r.ok) throw new Error('Not found');
|
|
return r.json() as Promise<FirmConfig>;
|
|
})
|
|
.then(setFirm)
|
|
.catch(() => setLoadError('Failed to load firm'));
|
|
|
|
fetch('/api/firm-catalog')
|
|
.then((r) => r.json())
|
|
.then((meta: { masterUrl?: string | null }) => setMasterUrl(meta.masterUrl ?? null))
|
|
.catch(() => {});
|
|
}, [id]);
|
|
|
|
if (loadError) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 p-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
<p className="text-sm text-red-600">{loadError}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const firmsPageUrl = masterUrl ? new URL('/firms', masterUrl).toString() : null;
|
|
|
|
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">
|
|
<button
|
|
onClick={() => router.push('/')}
|
|
className="text-slate-400 hover:text-slate-600 text-sm transition-colors"
|
|
>
|
|
← Back
|
|
</button>
|
|
<h1 className="text-2xl font-bold text-slate-900">{firm?.firm ?? '…'}</h1>
|
|
<span className="text-slate-300 text-2xl font-light">/</span>
|
|
<span className="text-2xl font-bold text-slate-400">Account Types</span>
|
|
</div>
|
|
|
|
<div className="mb-4 rounded-lg border border-slate-200 bg-white px-4 py-3 text-sm text-slate-600 shadow-sm">
|
|
Account types and banned symbols are managed on the master dashboard and pulled from it — this is a
|
|
read-only copy of the last successful sync.
|
|
{firmsPageUrl && (
|
|
<>
|
|
{' '}
|
|
<a
|
|
href={firmsPageUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="text-blue-600 hover:text-blue-700 font-medium"
|
|
>
|
|
Edit on master →
|
|
</a>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
|
<table className="w-full text-sm border-collapse">
|
|
<thead>
|
|
<tr className="border-b border-slate-200 bg-slate-50">
|
|
{['Label', 'Prefix', 'Account Size', 'Profit Target', 'Consistency', 'Min Day P&L', 'Min Trading Days', 'Max Contracts', 'After First W/D'].map((h) => (
|
|
<th key={h} className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{firm && firm.accounts.length === 0 && (
|
|
<tr>
|
|
<td colSpan={9} className="px-4 py-6 text-center text-sm text-slate-400 italic">
|
|
No account types — add them on the master dashboard
|
|
</td>
|
|
</tr>
|
|
)}
|
|
|
|
{firm?.accounts.map((acc) => (
|
|
<tr key={acc.id} className="border-b border-slate-100 last:border-0">
|
|
<td className="px-4 py-2.5 font-medium text-slate-800">{acc.label}</td>
|
|
<td className="px-4 py-2.5 font-mono text-slate-700">{acc.prefix}</td>
|
|
<td className="px-4 py-2.5 text-slate-600 tabular-nums">{fmtSize(acc.accountSize)}</td>
|
|
<td className="px-4 py-2.5 text-slate-600 tabular-nums">{fmtMoney(acc.profitTarget)}</td>
|
|
<td className="px-4 py-2.5 text-slate-600 tabular-nums">{Math.round(acc.consistency * 100)}%</td>
|
|
<td className="px-4 py-2.5 text-slate-600 tabular-nums">
|
|
{acc.minDayPnL <= -999 ? '—' : fmtMoney(acc.minDayPnL)}
|
|
</td>
|
|
<td className="px-4 py-2.5 text-slate-600 tabular-nums">{acc.minTradingDays}</td>
|
|
<td className="px-4 py-2.5 text-slate-600 tabular-nums">
|
|
{acc.maxPositionSize === 0 ? 'No limit' : acc.maxPositionSize}
|
|
</td>
|
|
<td className="px-4 py-2.5 text-slate-600">
|
|
{acc.targetSameEquity
|
|
? 'Same equity'
|
|
: acc.withdrawalStages.length === 0
|
|
? 'Base values'
|
|
: `${acc.withdrawalStages.length} stage${acc.withdrawalStages.length === 1 ? '' : 's'}`}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mt-10 mb-3">
|
|
Instruments
|
|
</h2>
|
|
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm">
|
|
{!firm || firm.instruments.length === 0 ? (
|
|
<span className="text-sm text-slate-400 italic">None — this firm won't trade</span>
|
|
) : (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{firm.instruments.map((sym) => (
|
|
<span key={sym} className="px-2 py-1 rounded-md text-xs font-mono font-semibold bg-slate-100 text-slate-700">
|
|
{sym}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
<p className="mt-2 text-xs text-slate-400">
|
|
{firm?.instrumentsSource === 'legacy'
|
|
? 'The master doesn\'t send instrument lists yet: this machine\'s enabled instruments, less the firm\'s banned symbols.'
|
|
: 'Edited on the master\'s Firms page.'}
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|