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
+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