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:
Brandon Li
2026-09-26 16:04:15 -05:00
co-authored by Claude Opus 5.5
parent 49c4725e0c
commit 28f465508d
18 changed files with 3962 additions and 534 deletions
+88 -18
View File
@@ -17,7 +17,10 @@
* the two are not linked yet.)
*/
import type { CatalogAccountType, CatalogFirm, FirmCatalogPayload } from './master-protocol';
import { isFirmPlatform } from './master-protocol';
import type {
CatalogAccountType, CatalogFirm, FirmCatalogPayload, FirmPlatform, SymbolMap, SymbolMapCandidate,
} from './master-protocol';
import {
getSetting,
setSetting,
@@ -99,26 +102,70 @@ export function getCatalogMeta(): {
};
}
/** Whether stored credentials are what the firm's platform logs in with — a
* TopstepX API key, or a username + password (Tradovate, DXTrade). */
export function hasPlatformCredentials(
platform: FirmPlatform,
creds: { username: string; password: string; apiKey: string } | null | undefined,
): boolean {
if (!creds) return false;
switch (platform) {
case 'topstepx': return !!creds.apiKey;
case 'tradovate':
case 'dxtrade': return !!creds.username && !!creds.password;
}
}
/** Firms this machine can actually trade: in the catalog AND holding the
* credentials their platform needs — a Tradovate username + password, or a
* TopstepX API key. Everything that starts a platform client goes through
* here. */
* credentials their platform needs. Everything that starts a platform client
* goes through here. */
export function getTradableFirms(): (CatalogFirm & { username: string; password: string; apiKey: string })[] {
const result: (CatalogFirm & { username: string; password: string; apiKey: string })[] = [];
for (const firm of getCachedCatalog()) {
const creds = getCredentials(firm.id);
if (!creds) continue;
if (firm.platform === 'topstepx') {
if (creds.apiKey) result.push({ ...firm, ...creds });
} else {
if (creds.username && creds.password) result.push({ ...firm, ...creds });
}
if (creds && hasPlatformCredentials(firm.platform, creds)) result.push({ ...firm, ...creds });
}
return result;
}
// ── Sync ─────────────────────────────────────────────────────────────────────
const g2 = global as typeof globalThis & { __catalogUnknownPlatformWarned?: Set<string> };
/** Once per firm per process — the catalog is re-validated every 60s. */
function warnUnknownPlatform(id: string, name: string, platform: unknown): void {
const warned = (g2.__catalogUnknownPlatformWarned ??= new Set());
const key = `${id}|${String(platform)}`;
if (warned.has(key)) return;
warned.add(key);
console.warn(`[catalog] skipping "${name}": platform ${JSON.stringify(platform)} is unknown to this build — update this instance`);
}
function nonNegative(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
}
/** Drops anything malformed rather than failing the sync: a bad symbol-map
* entry only means that candidate isn't traded. */
function sanitizeSymbolMap(raw: unknown): SymbolMap {
const map: SymbolMap = {};
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return map;
for (const [root, list] of Object.entries(raw as Record<string, unknown>)) {
if (!Array.isArray(list)) continue;
const candidates = list.flatMap((c): SymbolMapCandidate[] => {
if (typeof c !== 'object' || c === null) return [];
const cand = c as Record<string, unknown>;
if (typeof cand.symbol !== 'string' || !cand.symbol || cand.symbol.includes('|')) return [];
const notional = nonNegative(cand.notionalPerContract);
const pct = typeof cand.commissionPct === 'number' && Number.isFinite(cand.commissionPct) && cand.commissionPct >= 0
? cand.commissionPct : undefined;
return [{ symbol: cand.symbol, notionalPerContract: notional, ...(pct !== undefined ? { commissionPct: pct } : {}) }];
});
if (candidates.length > 0) map[root.toUpperCase()] = candidates;
}
return map;
}
function isStages(value: unknown): value is { profit: number; consistency: number; minTradingDays: number }[] {
return Array.isArray(value) && value.every((s) =>
typeof s === 'object' && s !== null
@@ -136,12 +183,24 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
if (typeof body.updatedAt !== 'string') throw new Error('missing updatedAt');
if (!Array.isArray(body.firms)) throw new Error('missing firms');
const firms: CatalogFirm[] = body.firms.map((f: unknown, i: number) => {
const firms: CatalogFirm[] = [];
body.firms.forEach((f: unknown, i: number) => {
if (typeof f !== 'object' || f === null) throw new Error(`firm ${i} is not an object`);
const firm = f as Record<string, unknown>;
if (typeof firm.id !== 'string' || typeof firm.name !== 'string') {
throw new Error(`firm ${i} is missing id or name`);
}
// An old master sends no platform: that is Tradovate. A platform this
// build doesn't know is NOT — coercing it would start the wrong client
// with another platform's credentials. Skip the firm (before its
// account types can fail the whole sync); it comes back once this
// build knows the platform.
const platform = firm.platform === undefined ? 'tradovate' : firm.platform;
if (!isFirmPlatform(platform)) {
warnUnknownPlatform(firm.id, firm.name, platform);
return;
}
const tradesEveryDay = firm.tradesEveryDay === true;
if (!Array.isArray(firm.accountTypes)) throw new Error(`firm ${firm.name} is missing accountTypes`);
const accountTypes: CatalogAccountType[] = firm.accountTypes.map((t: unknown, j: number) => {
@@ -163,26 +222,37 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
return {
...type,
splitEnabled: type.splitEnabled !== false,
combineCmeHolidays: type.combineCmeHolidays === true,
// An every-day firm has no CME calendar to fold holidays by.
combineCmeHolidays: type.combineCmeHolidays === true && !tradesEveryDay,
// Lenient: a master from before the min-legs field means "no minimum".
minLegs: typeof type.minLegs === 'number' ? type.minLegs : 0,
// Lenient: the obfuscation stops are opt-in, so absent means off.
firstDayStop: type.firstDayStop === true,
minDayStop: type.minDayStop === true,
// Lenient: no hard daily limit, no notional cap.
dailyLossLimit: nonNegative(type.dailyLossLimit),
maxNotional: nonNegative(type.maxNotional),
// The firm's calendar, stamped on every type so the cfg-based
// paths (projections, completion, the dashboard) all see it.
tradesEveryDay,
} as unknown as CatalogAccountType;
});
return {
firms.push({
id: firm.id,
name: firm.name,
// Lenient on purpose: a master from before the platform field (or
// with a platform this build doesn't know) must not fail the whole
// sync, and 'tradovate' is what every firm was until then.
platform: firm.platform === 'topstepx' ? 'topstepx' : 'tradovate',
platform,
bannedSymbols: Array.isArray(firm.bannedSymbols) ? (firm.bannedSymbols as string[]) : [],
accountTypes,
serverUrl: typeof firm.serverUrl === 'string' ? firm.serverUrl : '',
loginDomain: typeof firm.loginDomain === 'string' ? firm.loginDomain : '',
commissionPct: nonNegative(firm.commissionPct),
tradesEveryDay,
symbolMap: sanitizeSymbolMap(firm.symbolMap),
targetHoldHours: nonNegative(firm.targetHoldHours),
financingPctPerDay: nonNegative(firm.financingPctPerDay),
updatedAt: typeof firm.updatedAt === 'string' ? firm.updatedAt : new Date(0).toISOString(),
};
});
});
return { version: body.version, updatedAt: body.updatedAt, firms };