| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- 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<number> {
- 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
- })
|