pacifica_toggle_btc.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import 'dotenv/config'
  2. import { PacificaClient } from '../src/exchanges/pacifica/PacificaClient'
  3. import { PacificaAdapter } from '../src/exchanges/pacifica/PacificaAdapter'
  4. import { PacificaAccountAdapter } from '../src/exchanges/pacifica/AccountAdapter'
  5. async function sleep(ms: number) {
  6. return new Promise(r => setTimeout(r, ms))
  7. }
  8. async function main() {
  9. const symbol = 'BTC'
  10. const client = new PacificaClient({
  11. baseUrl: process.env.PACIFICA_BASE_URL || 'https://api.pacifica.fi',
  12. wsUrl: process.env.PACIFICA_WS_URL || 'wss://ws.pacifica.fi',
  13. apiKey: process.env.PACIFICA_API_KEY,
  14. privateKey: process.env.PACIFICA_ACCOUNT_PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY,
  15. account: process.env.PACIFICA_ACCOUNT,
  16. ...(process.env.PACIFICA_USE_AGENT === '1' && process.env.PACIFICA_AGENT_PRIVATE_KEY
  17. ? {
  18. agentWallet: process.env.PACIFICA_AGENT_WALLET,
  19. agentPrivateKey: process.env.PACIFICA_AGENT_PRIVATE_KEY,
  20. }
  21. : {}),
  22. })
  23. const ex = new PacificaAdapter(client)
  24. const account = new PacificaAccountAdapter(client)
  25. const notionalUsd = process.env.PACIFICA_TOGGLE_NOTIONAL_USD
  26. ? Number(process.env.PACIFICA_TOGGLE_NOTIONAL_USD)
  27. : undefined
  28. const explicitQty = process.env.PACIFICA_TEST_QTY
  29. const intervalMs = Number(process.env.PACIFICA_TOGGLE_INTERVAL_MS || 2000)
  30. const loops = Number(process.env.PACIFICA_TOGGLE_LOOPS || 10)
  31. const riskPct = Number(process.env.PACIFICA_TOGGLE_RISK_PCT || 0.2) // 每次下单不超过账户总值的比例
  32. console.log(
  33. `Pacifica BTC toggle test: notionalUsd=${notionalUsd ?? 'N/A'} qty=${
  34. explicitQty ?? 'auto'
  35. } intervalMs=${intervalMs} loops=${loops}`,
  36. )
  37. async function fetchSymbolMeta(): Promise<{ lot: number; minNotional: number }> {
  38. try {
  39. const info: any = await (client.getPublic as any)(client.endpoints.symbols)
  40. const data = info?.data ?? info
  41. const row = Array.isArray(data) ? data.find((x: any) => String(x.symbol).toUpperCase() === symbol) : undefined
  42. const lot = row?.lot_size ? Number(row.lot_size) : 0.00001
  43. const minNotional = row?.min_order_size ? Number(row.min_order_size) : 10
  44. return {
  45. lot: Number.isFinite(lot) && lot > 0 ? lot : 0.00001,
  46. minNotional: Number.isFinite(minNotional) && minNotional > 0 ? minNotional : 10,
  47. }
  48. } catch {
  49. return { lot: 0.00001, minNotional: 10 }
  50. }
  51. }
  52. function floorToStep(value: number, step: number): number {
  53. const mul = Math.round(1 / step)
  54. return Math.floor(value * mul) / mul
  55. }
  56. function ceilToStep(value: number, step: number): number {
  57. const mul = Math.round(1 / step)
  58. return Math.ceil(value * mul) / mul
  59. }
  60. async function getAccountValue(): Promise<number> {
  61. try {
  62. const info: any = await account.info()
  63. // 兼容字段名
  64. const candidates = [info?.account_value, info?.equity, info?.total_equity, info?.accountValue, info?.equityValue]
  65. const val = candidates.find((v: any) => typeof v === 'number')
  66. return typeof val === 'number' && Number.isFinite(val) ? val : 0
  67. } catch {
  68. return 0
  69. }
  70. }
  71. async function computeQty(): Promise<{
  72. qty: string
  73. usedNotional?: number
  74. ok: boolean
  75. reason?: string
  76. meta?: { lot: number; minNotional: number }
  77. mid?: number
  78. }> {
  79. const meta = await fetchSymbolMeta()
  80. if (notionalUsd) {
  81. const d = await ex.depth(symbol, 10)
  82. const bid = d.bids?.[0]?.price ? Number(d.bids[0].price) : NaN
  83. const ask = d.asks?.[0]?.price ? Number(d.asks[0].price) : NaN
  84. const mid = Number.isFinite(bid) && Number.isFinite(ask) ? (bid + ask) / 2 : Number.isFinite(bid) ? bid : ask
  85. const accVal = await getAccountValue()
  86. if (accVal > 0 && accVal < meta.minNotional) {
  87. return {
  88. qty: '0',
  89. usedNotional: 0,
  90. ok: false,
  91. reason: `equity ${accVal.toFixed(2)} < minNotional ${meta.minNotional}`,
  92. meta,
  93. mid,
  94. }
  95. }
  96. let target = notionalUsd
  97. if (accVal > 0 && riskPct > 0) target = Math.min(target, accVal * riskPct)
  98. target = Math.max(target, meta.minNotional)
  99. const rawQty = target / (mid || 1)
  100. let q = floorToStep(rawQty, meta.lot)
  101. // 保证最小 10u(或交易对要求)时不被步进向下截断为 0:必要时向上取整到最小名义额
  102. if (q * (mid || 1) < meta.minNotional) {
  103. q = ceilToStep(meta.minNotional / (mid || 1), meta.lot)
  104. }
  105. // 再次保护:至少一档最小数量
  106. if (q <= 0) q = meta.lot
  107. const qtyStr = q.toFixed(Math.max(0, String(meta.lot).split('.')[1]?.length || 0))
  108. return { qty: qtyStr, usedNotional: q * (mid || 1), ok: true, meta, mid }
  109. }
  110. if (explicitQty) return { qty: explicitQty, ok: true }
  111. return { qty: '0.001', ok: true }
  112. }
  113. for (let i = 0; i < loops; i++) {
  114. const { qty, usedNotional, ok, reason, meta, mid } = await computeQty()
  115. if (!ok) {
  116. console.log(`[${i}] skip: ${reason}`)
  117. await sleep(intervalMs * 2)
  118. continue
  119. }
  120. try {
  121. const buy = await ex.placeOrder({ symbol, side: 'BUY', type: 'MARKET', quantity: qty, tif: 'IOC' })
  122. console.log(
  123. `[${i}] market BUY ok id=${buy.id} qty=${qty}${usedNotional ? ` notional~${usedNotional.toFixed(2)}` : ''}`,
  124. )
  125. } catch (e: any) {
  126. const msg = String(e?.message || '')
  127. if (msg.includes('Insufficient balance') && meta && (mid || mid === 0)) {
  128. // BUY 降额重试
  129. const accVal = await getAccountValue()
  130. let tryNotional = (usedNotional || Number(qty) * (mid || 1)) * 0.5
  131. if (notionalUsd && accVal > 0) tryNotional = Math.min(tryNotional, accVal * riskPct)
  132. // 对齐最小 10u,如果降额后仍低于,直接向上取到 10u,再按步进对齐;若余额仍不足,会由接口拒绝
  133. if (tryNotional < meta.minNotional) tryNotional = meta.minNotional
  134. const q2n = tryNotional / (mid || 1)
  135. let q2 = floorToStep(q2n, meta.lot)
  136. if (q2 * (mid || 1) < meta.minNotional) q2 = ceilToStep(meta.minNotional / (mid || 1), meta.lot)
  137. if (q2 <= 0) q2 = meta.lot
  138. const qty2 = q2.toFixed(Math.max(0, String(meta.lot).split('.')[1]?.length || 0))
  139. try {
  140. const buy2 = await ex.placeOrder({ symbol, side: 'BUY', type: 'MARKET', quantity: qty2, tif: 'IOC' })
  141. console.log(`[${i}] market BUY retry ok id=${buy2.id} qty=${qty2}`)
  142. } catch (e2: any) {
  143. console.error(`[${i}] market BUY retry failed qty=${qty2}`, e2?.message || e2)
  144. }
  145. } else {
  146. console.error(`[${i}] market BUY failed qty=${qty}`, msg)
  147. }
  148. }
  149. await sleep(intervalMs)
  150. try {
  151. const sell = await ex.placeOrder({ symbol, side: 'SELL', type: 'MARKET', quantity: qty, tif: 'IOC' })
  152. console.log(
  153. `[${i}] market SELL ok id=${sell.id} qty=${qty}${usedNotional ? ` notional~${usedNotional.toFixed(2)}` : ''}`,
  154. )
  155. } catch (e: any) {
  156. const msg = String(e?.message || '')
  157. if (msg.includes('Insufficient balance') && meta && (mid || mid === 0)) {
  158. // 尝试降一半再重试一次
  159. const accVal = await getAccountValue()
  160. let tryNotional = (usedNotional || Number(qty) * (mid || 1)) * 0.5
  161. if (notionalUsd && accVal > 0) tryNotional = Math.min(tryNotional, accVal * riskPct)
  162. if (tryNotional < meta.minNotional) tryNotional = meta.minNotional
  163. const q2n = tryNotional / (mid || 1)
  164. let q2 = floorToStep(q2n, meta.lot)
  165. if (q2 * (mid || 1) < meta.minNotional) q2 = ceilToStep(meta.minNotional / (mid || 1), meta.lot)
  166. if (q2 <= 0) q2 = meta.lot
  167. const qty2 = q2.toFixed(Math.max(0, String(meta.lot).split('.')[1]?.length || 0))
  168. try {
  169. const sell2 = await ex.placeOrder({ symbol, side: 'SELL', type: 'MARKET', quantity: qty2, tif: 'IOC' })
  170. console.log(`[${i}] market SELL retry ok id=${sell2.id} qty=${qty2}`)
  171. } catch (e2: any) {
  172. console.error(`[${i}] market SELL retry failed qty=${qty2}`, e2?.message || e2)
  173. }
  174. } else {
  175. console.error(`[${i}] market SELL failed qty=${qty}`, msg)
  176. }
  177. }
  178. await sleep(intervalMs)
  179. }
  180. console.log('Toggle test done.')
  181. }
  182. main().catch(e => {
  183. console.error(e)
  184. process.exitCode = 1
  185. })