DXtrade platform for Tradeify247: percent fees, hard daily-loss stops, volatility sizing, every-day lanes
- DXtrade client (lib/dxtrade-class.ts): REST session login with lockout-safe backoff, 60s sync of accounts/metrics/positions/orders/transfers, history and fund transactions from cash transactions, day-start balance at the 22:00 UTC reset, CLOSE-by-positionCode exits (native OCO group, else a stop-first emulated pair), a cross-account hedge guard, stray-order reaper. - Instrument selector: the firm's symbol map lists candidate symbols per CME root; the cheapest tradable one per unit of recent hourly volatility wins (spread + 2x commission [+ financing] / sigma). - Fees: a firm's commission % of notional prices every fee off the fill; the take-profit is solved in closed form so the leg nets its target. - Hard daily loss limit per account type: every leg carries a stop keeping the firm's day inside the limit less a 10% reserve and both fees (rounded in); a green day never widens it; no room or a refused bracket flattens. - Notional sizing where the platform prices positions: target-hold volatility sizing or the symbol map's fixed notional, under a per-type notional cap. - Every-day firms trade weekends/holidays; the scheduler runs lanes (futures, one per DXtrade firm) with their own hours, flat gate, mutex and batches. - The catalog sync skips platforms this build doesn't know instead of treating them as Tradovate. One shared fireLeg replaces the duplicated batch/copy code. - npm run check: fee/stop/sizing/hours/selector checks; scripts/dxtrade-smoke.mjs confirms the API against a real login before trading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
49c4725e0c
commit
28f465508d
+45
-16
@@ -1,5 +1,7 @@
|
||||
import { TradovateClient } from './tradovate-class';
|
||||
import { TopstepXClient } from './topstepx-class';
|
||||
import { DXTradeClient } from './dxtrade-class';
|
||||
import type { FirmPlatform } from './master-protocol';
|
||||
import { errorMessage, isClientReady, type PlatformClient } from './platform-client';
|
||||
import { getInstruments } from './db';
|
||||
import { getTradableFirms, startCatalogSync } from './firm-catalog';
|
||||
@@ -43,15 +45,18 @@ function credsMap(): Map<string, string> {
|
||||
return g.__tradovateClientCreds;
|
||||
}
|
||||
|
||||
function credsSignature(firm: { platform: string; username: string; password: string; apiKey: string }): string {
|
||||
return `${firm.platform} ${firm.username} ${firm.password} ${firm.apiKey}`;
|
||||
/** What a client was built from — a change to any of it rebuilds the client.
|
||||
* DXTrade's server URL and login domain are the firm's (master), not the
|
||||
* machine's, but they decide the login just as much. */
|
||||
function credsSignature(firm: { platform: string; username: string; password: string; apiKey: string; serverUrl?: string; loginDomain?: string }): string {
|
||||
return `${firm.platform} ${firm.username} ${firm.password} ${firm.apiKey} ${firm.serverUrl ?? ''} ${firm.loginDomain ?? ''}`;
|
||||
}
|
||||
|
||||
export function initClient(
|
||||
id: string,
|
||||
firmName: string,
|
||||
platform: 'tradovate' | 'topstepx',
|
||||
creds: { username: string; password: string; apiKey: string },
|
||||
platform: FirmPlatform,
|
||||
creds: { username: string; password: string; apiKey: string; serverUrl?: string; loginDomain?: string },
|
||||
): PlatformClient {
|
||||
const map = ensureMap();
|
||||
const onSyncComplete = async () => {
|
||||
@@ -73,9 +78,29 @@ export function initClient(
|
||||
}
|
||||
};
|
||||
|
||||
const client: PlatformClient = platform === 'topstepx'
|
||||
? new TopstepXClient(creds.username, creds.apiKey, onSyncComplete)
|
||||
: new TradovateClient(creds.username, creds.password, onSyncComplete);
|
||||
let client: PlatformClient;
|
||||
switch (platform) {
|
||||
case 'topstepx':
|
||||
client = new TopstepXClient(creds.username, creds.apiKey, onSyncComplete);
|
||||
break;
|
||||
case 'dxtrade':
|
||||
client = new DXTradeClient({
|
||||
firmId: id,
|
||||
firmName,
|
||||
serverUrl: creds.serverUrl ?? '',
|
||||
loginDomain: creds.loginDomain ?? '',
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
}, onSyncComplete);
|
||||
break;
|
||||
case 'tradovate':
|
||||
client = new TradovateClient(creds.username, creds.password, onSyncComplete);
|
||||
break;
|
||||
default: {
|
||||
const unknown: never = platform;
|
||||
throw new Error(`no client for platform ${String(unknown)}`);
|
||||
}
|
||||
}
|
||||
map.set(id, client);
|
||||
credsMap().set(id, credsSignature({ platform, ...creds }));
|
||||
return client;
|
||||
@@ -316,11 +341,16 @@ const SUPERVISOR_INTERVAL_MS = 60_000;
|
||||
const REBUILD_AFTER_UNREADY_MS = 20 * 60_000;
|
||||
const REBUILD_COOLDOWN_MS = 20 * 60_000;
|
||||
|
||||
/** The message TopstepXClient records when a firm has an API key but no
|
||||
* username. That client never even tries to log in, so a rebuild changes
|
||||
* nothing — adding the username changes the credentials, and
|
||||
* reconcileClients rebuilds it then. */
|
||||
const TOPSTEPX_USERNAME_MISSING = /username missing/i;
|
||||
/** Clients that can't log in until someone acts — a rebuild changes nothing,
|
||||
* and fixing the credentials (or the firm's settings) rebuilds them through
|
||||
* reconcileClients anyway. TopstepX with an API key but no username never
|
||||
* tries to log in; DXTrade with no server URL can't, and after refused
|
||||
* credentials it stops trying so as not to lock the account. */
|
||||
function needsAttention(platform: FirmPlatform, client: PlatformClient): boolean {
|
||||
if (platform === 'topstepx') return /username missing/i.test(client.lastFetchErrors?.['login'] ?? '');
|
||||
if (platform === 'dxtrade') return (client as DXTradeClient).needsAttention === true;
|
||||
return false;
|
||||
}
|
||||
|
||||
interface SupervisionEntry {
|
||||
/** The instance being judged. A different one (rebuilt here, or replaced
|
||||
@@ -383,8 +413,8 @@ function rebuildHold(platform: string, client: PlatformClient, now: number): str
|
||||
+ ` (${health.consecutiveLoginFailures} failure(s) in a row)`;
|
||||
}
|
||||
} catch { /* no health: judge it on the rest */ }
|
||||
if (platform === 'topstepx' && Object.keys(client.positions ?? {}).length > 0) {
|
||||
return 'it holds an open position (TopstepX exit brackets are tracked only inside the client)';
|
||||
if ((platform === 'topstepx' || platform === 'dxtrade') && Object.keys(client.positions ?? {}).length > 0) {
|
||||
return 'it holds an open position (its exit pairs are tracked only inside the client)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -429,8 +459,7 @@ function superviseClients(): void {
|
||||
firms ??= getTradableFirms();
|
||||
const firm = firms.find((f) => f.id === id);
|
||||
if (!firm) continue; // no longer tradable: reconcileClients drops it
|
||||
if (firm.platform === 'topstepx'
|
||||
&& TOPSTEPX_USERNAME_MISSING.test(client.lastFetchErrors?.['login'] ?? '')) continue;
|
||||
if (needsAttention(firm.platform, client)) continue;
|
||||
|
||||
const minutes = Math.round((now - entry.unhealthySince) / 60_000);
|
||||
const hold = rebuildHold(firm.platform, client, now);
|
||||
|
||||
Reference in New Issue
Block a user