import 'dotenv/config' import { PacificaClient } from '../src/exchanges/pacifica/PacificaClient' import { PacificaAdapter } from '../src/exchanges/pacifica/PacificaAdapter' import { PacificaAccountAdapter } from '../src/exchanges/pacifica/AccountAdapter' async function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) } async function main() { const symbol = 'BTC' const client = new PacificaClient({ baseUrl: process.env.PACIFICA_BASE_URL || 'https://api.pacifica.fi', wsUrl: process.env.PACIFICA_WS_URL || 'wss://ws.pacifica.fi', apiKey: process.env.PACIFICA_API_KEY, privateKey: process.env.PACIFICA_ACCOUNT_PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY, account: process.env.PACIFICA_ACCOUNT, ...(process.env.PACIFICA_USE_AGENT === '1' && process.env.PACIFICA_AGENT_PRIVATE_KEY ? { agentWallet: process.env.PACIFICA_AGENT_WALLET, agentPrivateKey: process.env.PACIFICA_AGENT_PRIVATE_KEY, } : {}), }) const ex = new PacificaAdapter(client) const account = new PacificaAccountAdapter(client) const notionalUsd = process.env.PACIFICA_TOGGLE_NOTIONAL_USD ? Number(process.env.PACIFICA_TOGGLE_NOTIONAL_USD) : undefined const explicitQty = process.env.PACIFICA_TEST_QTY const intervalMs = Number(process.env.PACIFICA_TOGGLE_INTERVAL_MS || 2000) const loops = Number(process.env.PACIFICA_TOGGLE_LOOPS || 10) const riskPct = Number(process.env.PACIFICA_TOGGLE_RISK_PCT || 0.2) // 每次下单不超过账户总值的比例 console.log( `Pacifica BTC toggle test: notionalUsd=${notionalUsd ?? 'N/A'} qty=${ explicitQty ?? 'auto' } intervalMs=${intervalMs} loops=${loops}`, ) async function fetchSymbolMeta(): Promise<{ lot: number; minNotional: number }> { try { const info: any = await (client.getPublic as any)(client.endpoints.symbols) const data = info?.data ?? info const row = Array.isArray(data) ? data.find((x: any) => String(x.symbol).toUpperCase() === symbol) : undefined const lot = row?.lot_size ? Number(row.lot_size) : 0.00001 const minNotional = row?.min_order_size ? Number(row.min_order_size) : 10 return { lot: Number.isFinite(lot) && lot > 0 ? lot : 0.00001, minNotional: Number.isFinite(minNotional) && minNotional > 0 ? minNotional : 10, } } catch { return { lot: 0.00001, minNotional: 10 } } } function floorToStep(value: number, step: number): number { const mul = Math.round(1 / step) return Math.floor(value * mul) / mul } function ceilToStep(value: number, step: number): number { const mul = Math.round(1 / step) return Math.ceil(value * mul) / mul } async function getAccountValue(): Promise { try { const info: any = await account.info() // 兼容字段名 const candidates = [info?.account_value, info?.equity, info?.total_equity, info?.accountValue, info?.equityValue] const val = candidates.find((v: any) => typeof v === 'number') return typeof val === 'number' && Number.isFinite(val) ? val : 0 } catch { return 0 } } async function computeQty(): Promise<{ qty: string usedNotional?: number ok: boolean reason?: string meta?: { lot: number; minNotional: number } mid?: number }> { const meta = await fetchSymbolMeta() if (notionalUsd) { const d = await ex.depth(symbol, 10) const bid = d.bids?.[0]?.price ? Number(d.bids[0].price) : NaN const ask = d.asks?.[0]?.price ? Number(d.asks[0].price) : NaN const mid = Number.isFinite(bid) && Number.isFinite(ask) ? (bid + ask) / 2 : Number.isFinite(bid) ? bid : ask const accVal = await getAccountValue() if (accVal > 0 && accVal < meta.minNotional) { return { qty: '0', usedNotional: 0, ok: false, reason: `equity ${accVal.toFixed(2)} < minNotional ${meta.minNotional}`, meta, mid, } } let target = notionalUsd if (accVal > 0 && riskPct > 0) target = Math.min(target, accVal * riskPct) target = Math.max(target, meta.minNotional) const rawQty = target / (mid || 1) let q = floorToStep(rawQty, meta.lot) // 保证最小 10u(或交易对要求)时不被步进向下截断为 0:必要时向上取整到最小名义额 if (q * (mid || 1) < meta.minNotional) { q = ceilToStep(meta.minNotional / (mid || 1), meta.lot) } // 再次保护:至少一档最小数量 if (q <= 0) q = meta.lot const qtyStr = q.toFixed(Math.max(0, String(meta.lot).split('.')[1]?.length || 0)) return { qty: qtyStr, usedNotional: q * (mid || 1), ok: true, meta, mid } } if (explicitQty) return { qty: explicitQty, ok: true } return { qty: '0.001', ok: true } } for (let i = 0; i < loops; i++) { const { qty, usedNotional, ok, reason, meta, mid } = await computeQty() if (!ok) { console.log(`[${i}] skip: ${reason}`) await sleep(intervalMs * 2) continue } try { const buy = await ex.placeOrder({ symbol, side: 'BUY', type: 'MARKET', quantity: qty, tif: 'IOC' }) console.log( `[${i}] market BUY ok id=${buy.id} qty=${qty}${usedNotional ? ` notional~${usedNotional.toFixed(2)}` : ''}`, ) } catch (e: any) { const msg = String(e?.message || '') if (msg.includes('Insufficient balance') && meta && (mid || mid === 0)) { // BUY 降额重试 const accVal = await getAccountValue() let tryNotional = (usedNotional || Number(qty) * (mid || 1)) * 0.5 if (notionalUsd && accVal > 0) tryNotional = Math.min(tryNotional, accVal * riskPct) // 对齐最小 10u,如果降额后仍低于,直接向上取到 10u,再按步进对齐;若余额仍不足,会由接口拒绝 if (tryNotional < meta.minNotional) tryNotional = meta.minNotional const q2n = tryNotional / (mid || 1) let q2 = floorToStep(q2n, meta.lot) if (q2 * (mid || 1) < meta.minNotional) q2 = ceilToStep(meta.minNotional / (mid || 1), meta.lot) if (q2 <= 0) q2 = meta.lot const qty2 = q2.toFixed(Math.max(0, String(meta.lot).split('.')[1]?.length || 0)) try { const buy2 = await ex.placeOrder({ symbol, side: 'BUY', type: 'MARKET', quantity: qty2, tif: 'IOC' }) console.log(`[${i}] market BUY retry ok id=${buy2.id} qty=${qty2}`) } catch (e2: any) { console.error(`[${i}] market BUY retry failed qty=${qty2}`, e2?.message || e2) } } else { console.error(`[${i}] market BUY failed qty=${qty}`, msg) } } await sleep(intervalMs) try { const sell = await ex.placeOrder({ symbol, side: 'SELL', type: 'MARKET', quantity: qty, tif: 'IOC' }) console.log( `[${i}] market SELL ok id=${sell.id} qty=${qty}${usedNotional ? ` notional~${usedNotional.toFixed(2)}` : ''}`, ) } catch (e: any) { const msg = String(e?.message || '') if (msg.includes('Insufficient balance') && meta && (mid || mid === 0)) { // 尝试降一半再重试一次 const accVal = await getAccountValue() let tryNotional = (usedNotional || Number(qty) * (mid || 1)) * 0.5 if (notionalUsd && accVal > 0) tryNotional = Math.min(tryNotional, accVal * riskPct) if (tryNotional < meta.minNotional) tryNotional = meta.minNotional const q2n = tryNotional / (mid || 1) let q2 = floorToStep(q2n, meta.lot) if (q2 * (mid || 1) < meta.minNotional) q2 = ceilToStep(meta.minNotional / (mid || 1), meta.lot) if (q2 <= 0) q2 = meta.lot const qty2 = q2.toFixed(Math.max(0, String(meta.lot).split('.')[1]?.length || 0)) try { const sell2 = await ex.placeOrder({ symbol, side: 'SELL', type: 'MARKET', quantity: qty2, tif: 'IOC' }) console.log(`[${i}] market SELL retry ok id=${sell2.id} qty=${qty2}`) } catch (e2: any) { console.error(`[${i}] market SELL retry failed qty=${qty2}`, e2?.message || e2) } } else { console.error(`[${i}] market SELL failed qty=${qty}`, msg) } } await sleep(intervalMs) } console.log('Toggle test done.') } main().catch(e => { console.error(e) process.exitCode = 1 })