DXtrade: Hyperliquid prices when /marketdata is denied; closing stop/limit without quantity
Tradeify247's API user may not read DXtrade market data (code 34), which skipped every batch at the instrument pick. Quotes and hourly candles then come from Hyperliquid's public API (BTC, ETH on the main dex; XYZ100, SP500 and tokenized stocks on trade[XYZ]) — for the pick and sizing only; orders, fills and exit prices stay DXtrade's. Closing STOP/LIMIT orders close the whole position by positionCode and must not carry a quantity (code 33) — the exit bracket was refused, and the daily-loss guard flattened both accounts. A close's fill price also falls back to its executions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d01f03b293
commit
9fc1ed6908
+79
-18
@@ -46,6 +46,7 @@ import {
|
||||
lastUtcReset, ctTradeDayStart, timeOf, parseDurationMs, ciNum, isFundType, signedFundValue, newOrderCode,
|
||||
inTradingHours, realizedVolPerHour, scoreCandidate, candidateRejection,
|
||||
} from './dxtrade-helpers';
|
||||
import { hyperliquidCoins, hyperliquidCoinFor, hyperliquidQuote, hyperliquidHourlyCloses } from './hyperliquid-feed';
|
||||
|
||||
// ── timings ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -176,7 +177,10 @@ function parseOrder(raw: unknown): DxOrder | null {
|
||||
instrument: String(o.instrument ?? leg.instrument ?? ''),
|
||||
positionCode: typeof leg.positionCode === 'string' ? leg.positionCode : undefined,
|
||||
filledQuantity: ciNum(leg, 'filledQuantity') ?? 0,
|
||||
averagePrice: ciNum(leg, 'averagePrice'),
|
||||
// The leg's average, else the last execution's (a filled close may
|
||||
// carry it only there).
|
||||
averagePrice: ciNum(leg, 'averagePrice')
|
||||
?? [...executions].reverse().map((e) => ciNum(e, 'averagePrice')).find((p) => p !== undefined && p > 0),
|
||||
issueTime: timeOf(o.issueTime),
|
||||
rejectReason: rejected ? String(rejected.rejectReason) : undefined,
|
||||
};
|
||||
@@ -1067,12 +1071,49 @@ export class DXTradeClient implements PlatformClient {
|
||||
return data?.events ?? [];
|
||||
}
|
||||
|
||||
/** Whether DXtrade's own /marketdata is off limits to this login (it
|
||||
* answers code 34 "No proper permission is configured"), in which case
|
||||
* quotes and candles come from Hyperliquid instead (hyperliquid-feed). */
|
||||
private dxMarketDataDenied = false;
|
||||
|
||||
private isPermissionDenied(err: unknown): boolean {
|
||||
return err instanceof DxApiError && (err.code === 34 || err.status === 403);
|
||||
}
|
||||
|
||||
private noteMarketDataDenied(err: unknown): void {
|
||||
if (this.dxMarketDataDenied) return;
|
||||
this.dxMarketDataDenied = true;
|
||||
console.warn(`${this.tag} DXtrade market data not permitted (${errorMessage(err)}) — using Hyperliquid's public prices for the instrument pick`);
|
||||
}
|
||||
|
||||
/** DXtrade symbol → Hyperliquid coin for the symbols given (unmapped left out). */
|
||||
private async hyperliquidCoins(symbols: string[]): Promise<Map<string, string>> {
|
||||
const coins = await hyperliquidCoins();
|
||||
const out = new Map<string, string>();
|
||||
for (const s of symbols) {
|
||||
const coin = hyperliquidCoinFor(s, coins);
|
||||
if (coin) out.set(s, coin);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private async quotes(account: string, symbols: string[]): Promise<Map<string, { bid?: number; ask?: number; time: number }>> {
|
||||
const events = await this.marketData(account, symbols, { type: 'Quote', format: 'COMPACT' });
|
||||
const out = new Map<string, { bid?: number; ask?: number; time: number }>();
|
||||
for (const e of events) {
|
||||
if (typeof e.symbol !== 'string') continue;
|
||||
out.set(e.symbol, { bid: ciNum(e, 'bid'), ask: ciNum(e, 'ask'), time: timeOf(e.time ?? e.timestamp) });
|
||||
if (!this.dxMarketDataDenied) {
|
||||
try {
|
||||
const events = await this.marketData(account, symbols, { type: 'Quote', format: 'COMPACT' });
|
||||
for (const e of events) {
|
||||
if (typeof e.symbol !== 'string') continue;
|
||||
out.set(e.symbol, { bid: ciNum(e, 'bid'), ask: ciNum(e, 'ask'), time: timeOf(e.time ?? e.timestamp) });
|
||||
}
|
||||
return out;
|
||||
} catch (err) {
|
||||
if (!this.isPermissionDenied(err)) throw err;
|
||||
this.noteMarketDataDenied(err);
|
||||
}
|
||||
}
|
||||
for (const [symbol, coin] of await this.hyperliquidCoins(symbols)) {
|
||||
out.set(symbol, await hyperliquidQuote(coin));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1081,20 +1122,36 @@ export class DXTradeClient implements PlatformClient {
|
||||
const now = Date.now();
|
||||
const missing = symbols.filter((s) => !(now - (this.candleCache.get(s)?.at ?? 0) < CANDLE_TTL_MS));
|
||||
if (missing.length > 0) {
|
||||
const events = await this.marketData(account, missing, {
|
||||
type: 'Candle', format: 'COMPACT', candleType: '1h',
|
||||
fromTime: new Date(now - (CANDLE_HOURS + 1) * 3_600_000).toISOString(),
|
||||
toTime: new Date(now).toISOString(),
|
||||
});
|
||||
const bySymbol = new Map<string, { time: number; close: number }[]>();
|
||||
for (const e of events) {
|
||||
const close = ciNum(e, 'close');
|
||||
if (typeof e.symbol !== 'string' || close === undefined) continue;
|
||||
bySymbol.set(e.symbol, [...(bySymbol.get(e.symbol) ?? []), { time: timeOf(e.time ?? e.timestamp), close }]);
|
||||
const closes = new Map<string, number[]>();
|
||||
let fromDx = false;
|
||||
if (!this.dxMarketDataDenied) {
|
||||
try {
|
||||
const events = await this.marketData(account, missing, {
|
||||
type: 'Candle', format: 'COMPACT', candleType: '1h',
|
||||
fromTime: new Date(now - (CANDLE_HOURS + 1) * 3_600_000).toISOString(),
|
||||
toTime: new Date(now).toISOString(),
|
||||
});
|
||||
const bySymbol = new Map<string, { time: number; close: number }[]>();
|
||||
for (const e of events) {
|
||||
const close = ciNum(e, 'close');
|
||||
if (typeof e.symbol !== 'string' || close === undefined) continue;
|
||||
bySymbol.set(e.symbol, [...(bySymbol.get(e.symbol) ?? []), { time: timeOf(e.time ?? e.timestamp), close }]);
|
||||
}
|
||||
for (const [s, list] of bySymbol) closes.set(s, list.sort((a, b) => a.time - b.time).map((c) => c.close));
|
||||
fromDx = true;
|
||||
} catch (err) {
|
||||
if (!this.isPermissionDenied(err)) throw err;
|
||||
this.noteMarketDataDenied(err);
|
||||
}
|
||||
}
|
||||
if (!fromDx) {
|
||||
for (const [symbol, coin] of await this.hyperliquidCoins(missing)) {
|
||||
closes.set(symbol, await hyperliquidHourlyCloses(coin, CANDLE_HOURS));
|
||||
}
|
||||
}
|
||||
for (const s of missing) {
|
||||
const candles = (bySymbol.get(s) ?? []).sort((a, b) => a.time - b.time);
|
||||
this.candleCache.set(s, { vol: realizedVolPerHour(candles.map((c) => c.close)), candles: candles.length, at: now });
|
||||
const list = closes.get(s) ?? [];
|
||||
this.candleCache.set(s, { vol: realizedVolPerHour(list), candles: list.length, at: now });
|
||||
}
|
||||
}
|
||||
return new Map(symbols.map((s) => [s, this.candleCache.get(s) ?? { vol: null, candles: 0 }]));
|
||||
@@ -1286,7 +1343,11 @@ export class DXTradeClient implements PlatformClient {
|
||||
orderCode: o.code,
|
||||
type: o.type,
|
||||
instrument: symbol,
|
||||
quantity: o.quantity,
|
||||
// A closing STOP or LIMIT closes the whole position by its code and
|
||||
// must not name a quantity (DXtrade: "Closing STOP/LIMIT orders
|
||||
// should not have specified quantity", code 33). A MARKET close
|
||||
// may, and the flatten path relies on it.
|
||||
...(o.positionEffect === 'CLOSE' && o.type !== 'MARKET' ? {} : { quantity: o.quantity }),
|
||||
positionEffect: o.positionEffect,
|
||||
...(o.positionCode ? { positionCode: o.positionCode } : {}),
|
||||
side: o.side,
|
||||
|
||||
Reference in New Issue
Block a user