Add TopstepX platform support, drain withdrawal stages, and perfect-path calendar projection
- TopstepXClient (ProjectX Gateway): API-key auth, SignalR user hub, Trade/search daily P&L, emulated OCO exits, books-invariant withdrawal detection for $0-start accounts - Platform-aware client pool, credentials (api_key column), and credential form; $0-size account types match by prefix alone - Drain stages: min-day ($5-$15 randomized) MNQ legs for the configured days, then wait for the 50% withdrawal; stage pills show 'Stage N – Drain' - Account calendar projects the perfect path: future days book their planned targets, simulated 50% withdrawals, payout marker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1bca441247
commit
50a5eb4713
@@ -16,6 +16,7 @@ import {
|
||||
} from 'recharts';
|
||||
import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types';
|
||||
import { stagePillClasses } from '@/lib/stage-colors';
|
||||
import { projectPerfectPath } from '@/lib/trading-logic';
|
||||
|
||||
/** 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. */
|
||||
@@ -203,7 +204,18 @@ const MONTH_NAMES = [
|
||||
];
|
||||
const DOW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: number; month: number; pnlMap: Map<string, number>; fundMap: Map<string, number>; stageMap: Map<string, number> }) {
|
||||
function CalendarMonth({ year, month, pnlMap, fundMap, stageMap, projPnlMap, projFundMap, projStageMap, payoutDate }: {
|
||||
year: number;
|
||||
month: number;
|
||||
pnlMap: Map<string, number>;
|
||||
fundMap: Map<string, number>;
|
||||
stageMap: Map<string, number>;
|
||||
/** Perfect-path projection — rendered faded and dashed on days with no real data. */
|
||||
projPnlMap?: Map<string, number>;
|
||||
projFundMap?: Map<string, number>;
|
||||
projStageMap?: Map<string, number>;
|
||||
payoutDate?: string;
|
||||
}) {
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
const firstDow = new Date(year, month - 1, 1).getDay(); // 0 = Sunday
|
||||
|
||||
@@ -250,9 +262,16 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: numbe
|
||||
const key = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const pnl = pnlMap.get(key);
|
||||
const fundAmt = fundMap.get(key);
|
||||
const stage = stageMap.get(key);
|
||||
const hasData = pnl !== undefined;
|
||||
const positive = hasData && pnl! >= 0;
|
||||
|
||||
// Projection only fills days with no real data.
|
||||
const projPnl = !hasData && fundAmt === undefined ? projPnlMap?.get(key) : undefined;
|
||||
const projFund = !hasData && fundAmt === undefined ? projFundMap?.get(key) : undefined;
|
||||
const isPayout = !hasData && key === payoutDate;
|
||||
const isProj = projPnl !== undefined || projFund !== undefined || isPayout;
|
||||
const stage = stageMap.get(key) ?? (isProj ? projStageMap?.get(key) : undefined);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
@@ -263,7 +282,13 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: numbe
|
||||
: 'bg-red-50 border border-red-100'
|
||||
: fundAmt !== undefined
|
||||
? 'bg-amber-50 border border-amber-100'
|
||||
: 'bg-slate-50 border border-transparent'
|
||||
: isPayout
|
||||
? 'bg-blue-50/60 border border-dashed border-blue-200'
|
||||
: projFund !== undefined
|
||||
? 'bg-amber-50/50 border border-dashed border-amber-200'
|
||||
: projPnl !== undefined
|
||||
? 'bg-green-50/50 border border-dashed border-green-200'
|
||||
: 'bg-slate-50 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
@@ -273,7 +298,7 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: numbe
|
||||
{day}
|
||||
</span>
|
||||
{stage !== undefined && (
|
||||
<span className="text-[8px] font-semibold text-slate-400 leading-none">
|
||||
<span className={`text-[8px] font-semibold leading-none ${isProj ? 'text-slate-300' : 'text-slate-400'}`}>
|
||||
#{stage}
|
||||
</span>
|
||||
)}
|
||||
@@ -290,6 +315,21 @@ function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: numbe
|
||||
W/D {fundAmt >= 0 ? '+' : '−'}${fmt(Math.abs(fundAmt))}
|
||||
</span>
|
||||
)}
|
||||
{projPnl !== undefined && (
|
||||
<span className="text-[11px] font-semibold tabular-nums leading-tight text-green-600/60">
|
||||
+${fmt(projPnl)}
|
||||
</span>
|
||||
)}
|
||||
{projFund !== undefined && (
|
||||
<span className="text-[10px] font-semibold tabular-nums leading-tight mt-0.5 text-amber-500/70">
|
||||
W/D −${fmt(Math.abs(projFund))}
|
||||
</span>
|
||||
)}
|
||||
{isPayout && (
|
||||
<span className="text-[10px] font-bold leading-tight mt-0.5 text-blue-600/80">
|
||||
Payout ✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -433,9 +473,30 @@ export default function AccountPage() {
|
||||
|
||||
const strokeGradId = 'equityStroke';
|
||||
|
||||
// Perfect-path projection: what the future looks like if every trading day
|
||||
// books exactly its plan, cycles complete, and a 50% withdrawal fires the
|
||||
// day a cycle finishes. Same planner the trade path runs, so the numbers
|
||||
// (bumps and drain amounts included) are what would actually be aimed for.
|
||||
const projection = (cfg && !isDead) ? projectPerfectPath({
|
||||
accountId: account.id,
|
||||
cfg,
|
||||
cycleDailyPnL: account.dailyPnL ?? [],
|
||||
fundTransactions,
|
||||
balance: account.amount,
|
||||
realizedPnL: account.realizedPnL,
|
||||
}) : [];
|
||||
const projPnlMap = new Map(projection.filter((p) => p.withdrawal === undefined && !p.payoutReady).map((p) => [p.date, p.pnl]));
|
||||
const projFundMap = new Map(projection.filter((p) => p.withdrawal !== undefined).map((p) => [p.date, p.withdrawal!]));
|
||||
const projStageMap = new Map(projection.map((p) => [p.date, p.stage]));
|
||||
const payoutDate = projection.find((p) => p.payoutReady)?.date;
|
||||
|
||||
// Build calendar data
|
||||
const pnlMap = new Map(displayPnL.map((d) => [d.date, d.pnl]));
|
||||
const allCalendarDates = [...displayPnL.map((d) => d.date), ...fundTransactions.map((f) => f.date)];
|
||||
const allCalendarDates = [
|
||||
...displayPnL.map((d) => d.date),
|
||||
...fundTransactions.map((f) => f.date),
|
||||
...projection.map((p) => p.date),
|
||||
];
|
||||
const calendarMonths = [...new Set(allCalendarDates.map((d) => d.slice(0, 7)))].sort();
|
||||
|
||||
// Build stage map: stage increments after each withdrawal (negative fund txn)
|
||||
@@ -511,7 +572,7 @@ export default function AccountPage() {
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
Profit Target
|
||||
<span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-semibold ${stagePillClasses(account.stage)}`}>
|
||||
Stage {account.stage}
|
||||
Stage {account.stage}{account.stageDrain ? ' – Drain' : ''}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
@@ -704,11 +765,25 @@ export default function AccountPage() {
|
||||
{/* Daily P&L Calendar */}
|
||||
{calendarMonths.length > 0 && (
|
||||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-slate-900 font-bold text-base mb-6">Daily P&L</h2>
|
||||
<div className="flex items-baseline justify-between mb-6">
|
||||
<h2 className="text-slate-900 font-bold text-base">Daily P&L</h2>
|
||||
{projection.length > 0 && (
|
||||
<span className="text-xs text-slate-400">
|
||||
Dashed days: the perfect path — planned targets booked daily, 50% withdrawals when a stage completes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-8" style={{ gridTemplateColumns: `repeat(${Math.min(calendarMonths.length, 3)}, minmax(0, 1fr))` }}>
|
||||
{calendarMonths.map((ym) => {
|
||||
const [y, m] = ym.split('-').map(Number);
|
||||
return <CalendarMonth key={ym} year={y} month={m} pnlMap={pnlMap} fundMap={fundMap} stageMap={stageMap} />;
|
||||
return (
|
||||
<CalendarMonth
|
||||
key={ym} year={y} month={m}
|
||||
pnlMap={pnlMap} fundMap={fundMap} stageMap={stageMap}
|
||||
projPnlMap={projPnlMap} projFundMap={projFundMap}
|
||||
projStageMap={projStageMap} payoutDate={payoutDate}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,9 +20,9 @@ function dayPlanDiag(client: any, acc: { id: number; name: string }, tradeDate:
|
||||
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
||||
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
||||
const combined = combineCmeSessions(cfg.combineCmeHolidays === true, client.dailyPnL[acc.id] ?? [], tradeDate, cash.realizedPnL);
|
||||
const effective = resolveEffectiveConfig(cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages, priorProfit, allFundTxns);
|
||||
const effective = resolveEffectiveConfig(cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages, priorProfit, allFundTxns, cfg.minDayPnL);
|
||||
const equityProfit = cash.amount - cfg.accountSize;
|
||||
const plan = computeDayPlan(acc.id, combined.tradeDate, effective.profitTarget, effective.consistency, combined.dailyPnL, cfg.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL, cfg.splitEnabled);
|
||||
const plan = computeDayPlan(acc.id, combined.tradeDate, effective.profitTarget, effective.consistency, combined.dailyPnL, effective.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL, cfg.splitEnabled, effective.drain);
|
||||
|
||||
const priorDaysSum = combined.dailyPnL.filter((d) => d.date !== combined.tradeDate).reduce((s, d) => s + d.pnl, 0);
|
||||
const dayStartEquity = Math.round((equityProfit - combined.realizedPnL) * 100) / 100;
|
||||
@@ -40,6 +40,7 @@ function dayPlanDiag(client: any, acc: { id: number; name: string }, tradeDate:
|
||||
dayTarget: plan?.dayTarget ?? null,
|
||||
remainingToday: plan?.remainingToday ?? null,
|
||||
path: plan?.path ?? null,
|
||||
drain: effective.drain,
|
||||
consistency: effective.consistency,
|
||||
hasTodayRow: combined.dailyPnL.some((d) => d.date === combined.tradeDate),
|
||||
combineCmeHolidays: cfg.combineCmeHolidays === true,
|
||||
|
||||
@@ -5,24 +5,35 @@ import { reconcileClients, removeClient } from '@/lib/clients';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
/** The Tradovate login for one firm. This is the only firm data this machine
|
||||
/** The platform login for one firm. This is the only firm data this machine
|
||||
* owns — everything else comes from the master catalog, and these credentials
|
||||
* never go the other way. */
|
||||
* never go the other way.
|
||||
*
|
||||
* Tradovate firms need username + password. TopstepX firms need the API key;
|
||||
* username is optional for now (the loginKey call still wants it, so the
|
||||
* client sits idle with a clear error until one is added) and password is
|
||||
* unused. */
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
const { id } = await params;
|
||||
|
||||
const firm = getCachedFirm(id);
|
||||
if (!firm) return NextResponse.json({ error: 'Firm not found' }, { status: 404 });
|
||||
|
||||
const body = await req.json() as { username?: string; password?: string };
|
||||
const body = await req.json() as { username?: string; password?: string; apiKey?: string };
|
||||
const username = body.username?.trim() ?? '';
|
||||
const password = body.password?.trim() ?? '';
|
||||
if (!username || !password) {
|
||||
const apiKey = body.apiKey?.trim() ?? '';
|
||||
|
||||
if (firm.platform === 'topstepx') {
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ error: 'apiKey is required for a TopstepX firm' }, { status: 400 });
|
||||
}
|
||||
} else if (!username || !password) {
|
||||
return NextResponse.json({ error: 'username and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
setCredentials(id, firm.name, username, password);
|
||||
setCredentials(id, firm.name, username, password, apiKey);
|
||||
// Picks up the new login immediately rather than at the next restart.
|
||||
reconcileClients();
|
||||
return NextResponse.json({ ok: true });
|
||||
|
||||
@@ -18,11 +18,15 @@ export async function GET() {
|
||||
try {
|
||||
const result: FirmConfig[] = getCachedCatalog().map((firm) => {
|
||||
const creds = getCredentials(firm.id);
|
||||
const hasCredentials = firm.platform === 'topstepx'
|
||||
? !!creds?.apiKey
|
||||
: !!(creds?.username && creds?.password);
|
||||
return {
|
||||
id: firm.id,
|
||||
firm: firm.name,
|
||||
platform: firm.platform,
|
||||
username: creds?.username ?? '',
|
||||
hasCredentials: !!creds,
|
||||
hasCredentials,
|
||||
bannedSymbols: firm.bannedSymbols,
|
||||
accounts: firm.accountTypes.map((a) => ({
|
||||
id: a.id,
|
||||
|
||||
+23
-18
@@ -44,17 +44,29 @@ export async function GET() {
|
||||
);
|
||||
const dailyPnL = combined.dailyPnL;
|
||||
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
|
||||
// Days traded counts only days that hit minDayPnL (when set) — that's what
|
||||
// the firm requires toward the min trading day rule.
|
||||
const daysTraded: number = cfg && cfg.minDayPnL > 0
|
||||
? dailyPnL.filter((d) => d.pnl >= cfg.minDayPnL).length
|
||||
: combined.daysTraded;
|
||||
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
|
||||
const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold;
|
||||
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
|
||||
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
|
||||
// Stage = 1 + number of withdrawals (negative fund transactions)
|
||||
const stage = 1 + allFundTxns.filter((f) => f.amount < 0).length;
|
||||
// Stage-resolved config: a drain stage swaps in its own min-day
|
||||
// (default $5) and no profit target.
|
||||
const effective = cfg ? resolveEffectiveConfig(
|
||||
cfg.profitTarget,
|
||||
cfg.consistency,
|
||||
cfg.minTradingDays,
|
||||
cfg.targetSameEquity,
|
||||
cfg.withdrawalStages,
|
||||
priorProfit,
|
||||
allFundTxns,
|
||||
cfg.minDayPnL
|
||||
) : null;
|
||||
// Days traded counts only days that hit minDayPnL (when set) — that's what
|
||||
// the firm requires toward the min trading day rule.
|
||||
const daysTraded: number = effective && effective.minDayPnL > 0
|
||||
? dailyPnL.filter((d) => d.pnl >= effective.minDayPnL).length
|
||||
: combined.daysTraded;
|
||||
// Hide initial funding (amount === accountSize) from display
|
||||
const fundingAmount = cfg?.accountSize ?? inferredAccountSize;
|
||||
const displayFundTxns = fundingAmount !== null
|
||||
@@ -70,21 +82,12 @@ export async function GET() {
|
||||
let remainingToday: number | null = null;
|
||||
let effectiveProfitTarget: number | null = null;
|
||||
let nextStop: number | null = null;
|
||||
if (cfg && !isDead) {
|
||||
const effective = resolveEffectiveConfig(
|
||||
cfg.profitTarget,
|
||||
cfg.consistency,
|
||||
cfg.minTradingDays,
|
||||
cfg.targetSameEquity,
|
||||
cfg.withdrawalStages,
|
||||
priorProfit,
|
||||
allFundTxns
|
||||
);
|
||||
if (cfg && effective && !isDead) {
|
||||
const equityProfit = cash.amount - cfg.accountSize;
|
||||
const plan = computeDayPlan(
|
||||
acc.id, combined.tradeDate, effective.profitTarget, effective.consistency,
|
||||
dailyPnL, cfg.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL,
|
||||
cfg.splitEnabled
|
||||
dailyPnL, effective.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL,
|
||||
cfg.splitEnabled, effective.drain
|
||||
);
|
||||
dailyTarget = plan ? { amount: plan.dayTarget, path: plan.path } : null;
|
||||
remainingToday = plan?.remainingToday ?? null;
|
||||
@@ -113,7 +116,7 @@ export async function GET() {
|
||||
// Effective profit target = max(stage target, consistency realTarget).
|
||||
// If a big day forces the consistency rule, the account must reach maxDay/consistency
|
||||
// in total trading profit for the stage, not just profitTarget.
|
||||
const qualifying = cfg.minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= cfg.minDayPnL);
|
||||
const qualifying = effective.minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= effective.minDayPnL);
|
||||
const maxDay = qualifying.length > 0 ? Math.max(...qualifying.map((d) => d.pnl)) : 0;
|
||||
const consistencyReal = (effective.consistency > 0 && effective.consistency < 1 && maxDay > 0)
|
||||
? maxDay / effective.consistency : 0;
|
||||
@@ -192,6 +195,8 @@ export async function GET() {
|
||||
totalProfit,
|
||||
targetHit,
|
||||
stage,
|
||||
// Current stage is a drain stage — dashboards label it "Stage N – Drain".
|
||||
stageDrain: effective?.drain ?? false,
|
||||
dailyTarget,
|
||||
nextStop,
|
||||
remainingToday,
|
||||
|
||||
+49
-16
@@ -186,26 +186,35 @@ function AccountTypeCell({ account, firm, onAssigned }: {
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline credential entry, shown to the right of a firm that has none. */
|
||||
/** Inline credential entry, shown to the right of a firm that has none.
|
||||
* Tradovate wants username + password; TopstepX wants the API key (username
|
||||
* optional for now, though login needs it eventually — password unused). */
|
||||
function CredentialForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => void }) {
|
||||
const isTopstepX = firm.platform === 'topstepx';
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const canSave = isTopstepX
|
||||
? apiKey.trim().length > 0
|
||||
: username.trim().length > 0 && password.trim().length > 0;
|
||||
|
||||
async function save() {
|
||||
if (!username.trim() || !password.trim()) return;
|
||||
if (!canSave) return;
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/firms/${firm.id}/credentials`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
body: JSON.stringify(isTopstepX ? { username, apiKey } : { username, password }),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'Failed');
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setApiKey('');
|
||||
onSaved();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed');
|
||||
@@ -220,21 +229,32 @@ function CredentialForm({ firm, onSaved }: { firm: FirmConfig; onSaved: () => vo
|
||||
<div className="flex items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
className={cls}
|
||||
placeholder="Tradovate username"
|
||||
placeholder={isTopstepX ? 'Username (optional)' : 'Tradovate username'}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className={cls}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void save(); }}
|
||||
/>
|
||||
{isTopstepX ? (
|
||||
<input
|
||||
className={`${cls} w-52`}
|
||||
type="password"
|
||||
placeholder="TopstepX API key"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void save(); }}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className={cls}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void save(); }}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={saving || !username.trim() || !password.trim()}
|
||||
disabled={saving || !canSave}
|
||||
className="px-2.5 py-1 bg-blue-100 hover:bg-blue-200 disabled:opacity-50 text-blue-700 text-xs font-semibold rounded-md transition-colors"
|
||||
>
|
||||
{saving ? '…' : 'Save'}
|
||||
@@ -346,7 +366,7 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span>${(account.effectiveProfitTarget ?? cfg?.profitTarget ?? 0).toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
|
||||
<span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-semibold ${stagePillClasses(account.stage)}`}>
|
||||
Stage {account.stage}
|
||||
Stage {account.stage}{account.stageDrain ? ' – Drain' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{account.dailyTarget && (
|
||||
@@ -449,6 +469,19 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
|
||||
{tradeSymbol} BANNED
|
||||
</span>
|
||||
)}
|
||||
{firm.platform === 'topstepx' && (
|
||||
<span className="text-xs bg-indigo-100 text-indigo-700 px-1.5 py-0.5 rounded font-semibold">
|
||||
TopstepX
|
||||
</span>
|
||||
)}
|
||||
{firm.platform === 'topstepx' && hasCredentials && !firm.username && (
|
||||
<span
|
||||
className="text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-semibold"
|
||||
title="TopstepX login needs the platform username the API key was generated for — remove and re-save the credentials with it"
|
||||
>
|
||||
Username missing
|
||||
</span>
|
||||
)}
|
||||
{hasCredentials && !state.connected && (
|
||||
<span className="text-xs bg-slate-200 text-slate-600 px-1.5 py-0.5 rounded font-semibold">
|
||||
Connecting…
|
||||
@@ -469,7 +502,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
|
||||
<td colSpan={5} className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<span className="text-xs text-slate-500">
|
||||
Remove the saved Tradovate login for {firm.firm}? Its accounts disappear until you enter one again.
|
||||
Remove the saved {firm.platform === 'topstepx' ? 'TopstepX API key' : 'Tradovate login'} for {firm.firm}? Its accounts disappear until you enter one again.
|
||||
</span>
|
||||
<button
|
||||
onClick={removeCredentials}
|
||||
@@ -495,7 +528,7 @@ function FirmRows({ state, firm, hideDead, privacy, sortKey, sortDir, tradeSymbo
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setConfirmCreds(true); }}
|
||||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1 rounded-md transition-colors inline-flex"
|
||||
title="Remove saved Tradovate login"
|
||||
title={firm.platform === 'topstepx' ? 'Remove saved TopstepX API key' : 'Remove saved Tradovate login'}
|
||||
>
|
||||
<KeyOffIcon />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user