Files
autofirmer-expanded/scripts/dxtrade-smoke.mjs
T

228 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// DXtrade smoke test — run against a real login before the scheduler trades a
// DXtrade firm. Confirms the API details lib/dxtrade-class.ts assumes.
//
// DX_BASE=https://dx.tradeify247.co/dxsca-web DX_USER=... DX_PASS=... \
// DX_DOMAIN=default DX_SYMBOLS=BTC/USD,ETH/USD,XYZ100 node scripts/dxtrade-smoke.mjs
//
// Read-only unless DX_CONFIRM=yes AND DX_ACCOUNT=<account code> are set, which
// adds a minimum-size round trip on that account (a real order: it pays two
// commissions). Credentials come from the environment only; the password and
// session token are never printed.
//
// Run it while the instance's client for the firm is disconnected (remove the
// credentials there, or stop the instance): two sessions on one login can
// collide, and logins are rate-limited per IP.
const BASE = (process.env.DX_BASE ?? '').replace(/\/+$/, '');
const USER = process.env.DX_USER ?? '';
const PASS = process.env.DX_PASS ?? '';
const DOMAIN = process.env.DX_DOMAIN || 'default';
const SYMBOLS = (process.env.DX_SYMBOLS ?? 'BTC/USD').split(',').map((s) => s.trim()).filter(Boolean);
const ORDER_ACCOUNT = process.env.DX_ACCOUNT ?? '';
const CONFIRM = process.env.DX_CONFIRM === 'yes';
if (!BASE.startsWith('https://') || !USER || !PASS) {
console.error('Set DX_BASE (https://…/dxsca-web), DX_USER and DX_PASS.');
process.exit(1);
}
let token = null;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const section = (title) => console.log(`\n── ${title} ${'─'.repeat(Math.max(0, 70 - title.length))}`);
const show = (label, value) => console.log(`${label}:`, typeof value === 'string' ? value : JSON.stringify(value, null, 2)?.slice(0, 4000));
async function call(method, path, { params, body, headers } = {}) {
const url = new URL(`${BASE}${path}`);
for (const [k, v] of Object.entries(params ?? {})) url.searchParams.set(k, String(v));
const res = await fetch(url, {
method,
headers: {
Accept: 'application/json',
...(token ? { Authorization: `DXAPI ${token}` } : {}),
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(30_000),
});
const text = await res.text();
let data = null;
try { data = text ? JSON.parse(text) : null; } catch { data = text.slice(0, 500); }
return { status: res.status, headers: Object.fromEntries(res.headers), data };
}
async function step(label, fn) {
try {
await fn();
} catch (err) {
console.log(`${label} FAILED: ${err instanceof Error ? err.message : String(err)}`);
}
}
const codeOf = (a) => (typeof a === 'string' ? a : a && typeof a === 'object' ? `${a.clearing}:${a.account}` : String(a));
section('login');
{
const res = await call('POST', '/login', { body: { username: USER, domain: DOMAIN, password: PASS } });
if (res.status !== 200 || !res.data?.sessionToken) {
show(`login refused (HTTP ${res.status})`, res.data);
console.log('Check DX_DOMAIN (try "default", then the broker name) — and stop after a couple of tries: repeated failures lock the account.');
process.exit(1);
}
token = res.data.sessionToken;
show('session timeout field (format check)', res.data.timeout);
}
let accounts = [];
section('accounts (/users)');
for (const path of [`${USER}@${DOMAIN}`, USER]) {
const res = await call('GET', `/users/${encodeURIComponent(path)}`);
console.log(`/users/${path} → HTTP ${res.status}`);
if (res.status === 200) {
accounts = (res.data?.userDetails ?? []).flatMap((u) => u.accounts ?? []);
for (const a of accounts) {
console.log(` ${codeOf(a.account)} status=${a.accountStatus} positionBased=${a.positionBased} currency=${a.baseCurrency} registered=${a.registrationTime}`);
}
break;
}
}
const codes = accounts.map((a) => codeOf(a.account));
const ref = codes[0];
if (!ref) {
console.log('No accounts found — nothing more to check.');
process.exit(0);
}
section('metrics (key casing: openPL vs openPl)');
await step('metrics', async () => {
const res = await call('GET', '/accounts/metrics', { params: { accounts: codes.join(',') } });
show(`HTTP ${res.status}`, res.data?.metrics?.[0] ?? res.data);
});
section('positions / open orders');
await step('positions', async () => show('positions', (await call('GET', '/accounts/positions', { params: { accounts: codes.join(',') } })).data));
await step('orders', async () => show('open orders', (await call('GET', '/accounts/orders', { params: { accounts: codes.join(',') } })).data));
section('transfers — types, signs, paging, the opening DEPOSIT, FINANCING');
await step('transfers', async () => {
const from = new Date(Date.now() - 180 * 86_400_000).toISOString();
const res = await call('GET', '/accounts/transfers', { params: { accounts: codes.join(','), 'time-from': from, limit: 50 } });
const txns = (res.data?.cashTransfers ?? []).flatMap((t) => (t.cashTransactions ?? []).map((x) => ({ ...x, transferTime: t.transactionTime })));
const byType = {};
for (const t of txns) {
const e = (byType[t.type] ??= { count: 0, min: Infinity, max: -Infinity });
e.count++; e.min = Math.min(e.min, t.value); e.max = Math.max(e.max, t.value);
}
show('by type (value range → sign convention)', byType);
show('first / last transaction (ordering)', [txns[0], txns[txns.length - 1]]);
show('nextPageTransactionTime', res.data?.nextPageTransactionTime ?? '(none)');
});
section('instruments');
await step('instruments', async () => {
const res = await call('GET', '/instruments/query', { params: { symbols: SYMBOLS.join(',') } });
for (const i of res.data?.instruments ?? []) {
console.log(` ${i.symbol}: type=${i.type} priceIncrement=${i.priceIncrement} multiplier=${i.multiplier} lotSize=${i.lotSize} quantityIncrement=${i.quantityIncrement} currency=${i.currency} sessions=${(i.tradingHours ?? []).length}`);
}
show('first instrument, raw', res.data?.instruments?.[0]);
});
await step('account instrument details', async () => {
const res = await call('GET', `/accounts/${encodeURIComponent(ref)}/instruments/query`, { params: { symbols: SYMBOLS.join(',') } });
show('details', res.data?.instrumentDetails ?? res.data);
});
section('quotes and hourly candles (σ)');
await step('quotes', async () => {
const res = await call('POST', '/marketdata', { body: { account: ref, symbols: SYMBOLS, eventTypes: [{ type: 'Quote', format: 'COMPACT' }] } });
show(`Quote events (HTTP ${res.status})`, res.data?.events ?? res.data);
});
await step('candles', async () => {
const now = Date.now();
const res = await call('POST', '/marketdata', {
body: {
account: ref, symbols: SYMBOLS,
eventTypes: [{ type: 'Candle', format: 'COMPACT', candleType: '1h', fromTime: new Date(now - 49 * 3_600_000).toISOString(), toTime: new Date(now).toISOString() }],
},
});
const events = res.data?.events ?? [];
console.log(`Candle events: HTTP ${res.status}, ${events.length} event(s); first: ${JSON.stringify(events[0])}`);
for (const symbol of SYMBOLS) {
const closes = events.filter((e) => e.symbol === symbol && typeof e.close === 'number').map((e) => e.close);
const rets = closes.slice(1).map((c, i) => Math.log(c / closes[i]));
const mean = rets.reduce((s, r) => s + r, 0) / Math.max(1, rets.length);
const sd = Math.sqrt(rets.reduce((s, r) => s + (r - mean) ** 2, 0) / Math.max(1, rets.length - 1));
console.log(` ${symbol}: ${closes.length} closes, σ ≈ ${(sd * 100).toFixed(3)}%/h ≈ ${(sd * Math.sqrt(24) * 100).toFixed(2)}%/day`);
}
});
section('end-of-day metrics vs the 22:00 UTC reset');
await step('eodmetrics', async () => {
const d = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10);
const res = await call('GET', `/accounts/eodmetrics/${d}`, { params: { accounts: ref } });
show(`eodmetrics ${d} (HTTP ${res.status}) — compare balance with the instance's dayStart debug value`, res.data);
});
section('rate limits (two history calls back to back)');
await step('rate', async () => {
const a = await call('GET', '/accounts/transfers', { params: { accounts: ref, limit: 1 } });
const b = await call('GET', '/accounts/transfers', { params: { accounts: ref, limit: 1 } });
console.log(`HTTP ${a.status}, ${b.status}${b.status === 429 ? ' — history calls are limited to 1/s' : ''}`);
});
section('ping');
console.log(`POST /ping → HTTP ${(await call('POST', '/ping')).status}`);
if (CONFIRM && ORDER_ACCOUNT) {
section(`LIVE ORDERS on ${ORDER_ACCOUNT}`);
const acct = encodeURIComponent(ORDER_ACCOUNT);
const symbol = SYMBOLS[0];
const inst = (await call('GET', '/instruments/query', { params: { symbols: symbol } })).data?.instruments?.[0] ?? {};
const det = (await call('GET', `/accounts/${acct}/instruments/query`, { params: { symbols: symbol } })).data?.instrumentDetails?.[0] ?? {};
const qty = Math.max(det.minOrderSize ?? 0, inst.quantityIncrement ?? 0, det.minOrderSizeIncrement ?? 0) || 1;
const code = `af-smoke-${Date.now().toString(36)}`;
const open = { account: ORDER_ACCOUNT, orderCode: code, type: 'MARKET', instrument: symbol, quantity: qty, positionEffect: 'OPEN', side: 'BUY', tif: 'GTC' };
const placed = await call('POST', `/accounts/${acct}/orders`, { body: open });
show(`MARKET BUY ${qty} ${symbol} (HTTP ${placed.status})`, placed.data);
show('duplicate orderCode (expect 409 / code 100)', (await call('POST', `/accounts/${acct}/orders`, { body: open })));
await sleep(3_000);
const pos = ((await call('GET', `/accounts/${acct}/positions`)).data?.positions ?? []).find((p) => p.symbol === symbol);
show('position (positionCode should equal the orderId on position-based accounts)', pos);
show('order history row (commission, averagePrice)', (await call('GET', `/accounts/${acct}/orders/history`, { params: { 'with-client-id': code, limit: 5 } })).data);
if (pos) {
const px = pos.openPrice;
const tick = inst.priceIncrement || 0.01;
const far = (p) => Math.round(p / tick) * tick;
// Exactly what lib/dxtrade-class.ts sends: a closing STOP/LIMIT names
// the position, never a quantity (code 33 otherwise).
const leg = (c, type, price) => ({
account: ORDER_ACCOUNT, orderCode: c, type, instrument: symbol, positionEffect: 'CLOSE',
positionCode: pos.positionCode, side: 'SELL', tif: 'GTC', ...(type === 'LIMIT' ? { limitPrice: price } : { stopPrice: price }),
});
const group = await call('POST', `/accounts/${acct}/orders`, {
body: { orders: [leg(`${code}-tp`, 'LIMIT', far(px * 1.05)), leg(`${code}-sl`, 'STOP', far(px * 0.95))], contingencyType: 'OCO' },
});
show(`native OCO group of CLOSE legs (HTTP ${group.status})`, group.data);
if (group.status >= 400) {
// The bot's fallback: the stop on its own first, then the take-profit.
show('fallback: lone STOP CLOSE', (await call('POST', `/accounts/${acct}/orders`, { body: leg(`${code}-sl1`, 'STOP', far(px * 0.95)) })));
show('fallback: lone LIMIT CLOSE', (await call('POST', `/accounts/${acct}/orders`, { body: leg(`${code}-tp1`, 'LIMIT', far(px * 1.05)) })));
}
show('open orders now (the exit pair should be here)', (await call('GET', `/accounts/${acct}/orders`)).data);
const del = await call('DELETE', `/accounts/${acct}/orders/${encodeURIComponent(`${code}-tp`)}`);
show(`cancel without If-Match (HTTP ${del.status}; 403/412/428 means it wants one)`, del.data);
// Tradeify: every trade must be held at least 20 seconds.
console.log('holding 22 s before closing (minimum hold)…');
await sleep(22_000);
const flatten = await call('POST', `/accounts/${acct}/close`, { body: { account: ORDER_ACCOUNT, closePositions: true, cancelOrders: true } });
show(`flatten via /close (HTTP ${flatten.status})`, flatten.data);
}
await sleep(3_000);
const recent = await call('GET', `/accounts/${acct}/transfers`, { params: { 'time-from': new Date(Date.now() - 10 * 60_000).toISOString() } });
show('transactions of the last 10 minutes (commission = 0.04% × notional per side?)', recent.data);
} else {
console.log('\n(read-only: set DX_CONFIRM=yes and DX_ACCOUNT=<code> to also test a live minimum-size round trip)');
}
await call('POST', '/logout');