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:
Brandon Li
2026-09-12 16:25:52 -05:00
co-authored by Claude Fable 5
parent 1bca441247
commit 50a5eb4713
21 changed files with 1854 additions and 142 deletions
+20 -8
View File
@@ -49,6 +49,15 @@ db.exec(`
// Tradovate credentials, and a silent DROP would be unrecoverable if this
// change had to be rolled back. Safe to drop by hand once it has stuck.
// Migration: TopstepX firms authenticate with an API key instead of a
// password. Kept in the same table so re-keying and reconciliation treat both
// platforms alike; a Tradovate row simply leaves it blank.
try {
db.exec("ALTER TABLE firm_credentials ADD COLUMN api_key TEXT NOT NULL DEFAULT ''");
} catch {
// Column already exists
}
// ── Firm credentials ─────────────────────────────────────────────────────────
export interface FirmCredentialRow {
@@ -56,13 +65,15 @@ export interface FirmCredentialRow {
firm_name: string;
username: string;
password: string;
api_key: string;
updated_at: string;
}
export function getCredentials(firmId: string): { username: string; password: string } | undefined {
const row = db.prepare('SELECT username, password FROM firm_credentials WHERE firm_id = ?').get(firmId) as
{ username: string; password: string } | undefined;
return row;
export function getCredentials(firmId: string): { username: string; password: string; apiKey: string } | undefined {
const row = db.prepare('SELECT username, password, api_key FROM firm_credentials WHERE firm_id = ?').get(firmId) as
{ username: string; password: string; api_key: string } | undefined;
if (!row) return undefined;
return { username: row.username, password: row.password, apiKey: row.api_key };
}
export function listCredentials(): FirmCredentialRow[] {
@@ -72,16 +83,17 @@ export function listCredentials(): FirmCredentialRow[] {
/** firm_name is stored alongside the id so credentials can be re-keyed by name
* if the master's database is ever rebuilt and the firms come back with new
* ids. See rekeyCredentialsByName in lib/firm-catalog.ts. */
export function setCredentials(firmId: string, firmName: string, username: string, password: string): void {
export function setCredentials(firmId: string, firmName: string, username: string, password: string, apiKey = ''): void {
db.prepare(`
INSERT INTO firm_credentials (firm_id, firm_name, username, password, updated_at)
VALUES (?, ?, ?, ?, ?)
INSERT INTO firm_credentials (firm_id, firm_name, username, password, api_key, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(firm_id) DO UPDATE SET
firm_name = excluded.firm_name,
username = excluded.username,
password = excluded.password,
api_key = excluded.api_key,
updated_at = excluded.updated_at
`).run(firmId, firmName, username, password, new Date().toISOString());
`).run(firmId, firmName, username, password, apiKey, new Date().toISOString());
}
export function clearCredentials(firmId: string): boolean {