index.ts 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969
  1. import 'dotenv/config';
  2. import { readFileSync, mkdirSync } from "node:fs";
  3. import { dirname, join, resolve } from "node:path";
  4. import { parse } from "yaml";
  5. import pino from "pino";
  6. import { ShadowBook } from "../../../packages/utils/src/shadowBook";
  7. import { MarketDataAdapter } from "../../../packages/utils/src/marketDataAdapter";
  8. import { AdapterRegistry } from "../../../packages/connectors/pacifica/src/adapterRegistry";
  9. import { OrderRouter } from "../../../packages/execution/src/orderRouter";
  10. import { GlobalOrderCoordinator } from "../../../packages/execution/src/globalOrderCoordinator";
  11. import { MarketMaker } from "../../../packages/strategies/src/marketMaker";
  12. import { MicroScalper } from "../../../packages/strategies/src/microScalper";
  13. import { GridMaker, type AdaptiveGridConfig } from "../../../packages/strategies/src/gridMaker";
  14. import { HedgeEngine } from "../../../packages/hedge/src/hedgeEngine";
  15. import { PositionManager } from "../../../packages/portfolio/src/positionManager";
  16. import { RiskEngine } from "../../../packages/risk/src/riskEngine";
  17. import { PacificaWebSocket } from "../../../packages/connectors/pacifica/src/wsClient";
  18. import { PacificaWsOrderGateway } from "../../../packages/connectors/pacifica/src/wsOrderGateway";
  19. import { RateLimiter } from "../../../packages/connectors/pacifica/src/rateLimiter";
  20. import type { Order, Fill } from "../../../packages/domain/src/types";
  21. import type { KillSwitchConfig } from "../../../packages/risk/src/riskEngine";
  22. let globalFileDestination: pino.DestinationStream | undefined;
  23. const flushLogsSafe = () => {
  24. if (!globalFileDestination) return;
  25. const stream: any = globalFileDestination;
  26. if (typeof stream.flushSync !== 'function') return;
  27. try {
  28. stream.flushSync();
  29. } catch {
  30. // 静默忽略 flush 失败(sonic-boom 未 ready 时正常)
  31. }
  32. };
  33. const logger = createRunnerLogger();
  34. const cfg = parse(readFileSync("config/config.yaml","utf8"));
  35. const baseUrl = cfg.api_base || process.env.PACIFICA_API_BASE || "https://api.pacifica.fi/api/v1";
  36. type AccountConfigEntry = {
  37. api_base?: string;
  38. address?: string;
  39. private_key?: string;
  40. api_key?: string;
  41. secret?: string;
  42. subaccount?: string;
  43. role?: string;
  44. };
  45. type MarketMakerRawConfig = {
  46. tick_sz?: number;
  47. clip_sz?: number;
  48. spread_bps?: number;
  49. reprice_ms?: number;
  50. };
  51. type ScalperRawConfig = {
  52. clip_sz?: number;
  53. tp_bps?: number;
  54. sl_bps?: number;
  55. on_book_interval_ms?: number;
  56. trigger?: {
  57. spread_bps?: number;
  58. min_cooldown_ms?: number;
  59. };
  60. };
  61. const adapterRegistry = new AdapterRegistry();
  62. const accountsConfig: Record<string, AccountConfigEntry> = cfg.accounts || {};
  63. const accountEntries = Object.entries(accountsConfig);
  64. const wsGatewayConfigs: Array<{ id: string; address?: string; privateKey?: string }> = [];
  65. const addressToAccountId = new Map<string, string>();
  66. const fillHandlers = new Map<string, Array<(fill: Fill) => Promise<void>>>();
  67. let killSwitchActive = false;
  68. let gridMakerInstance: GridMaker | undefined;
  69. const baseClipEquityPct = cfg.grid?.base_clip_equity_pct ?? 0;
  70. const baseClipLeverage = cfg.grid?.base_clip_leverage ?? 1;
  71. const baseClipUsdFloor = cfg.grid?.base_clip_usd ?? 0;
  72. const wsRateLimiterConfig = cfg.execution?.ws_rate_limiter;
  73. let marketMakerInstance: MarketMaker | undefined;
  74. let scalperInstance: MicroScalper | undefined;
  75. let gridStatusTimer: NodeJS.Timeout | undefined;
  76. let gridTickTimer: NodeJS.Timeout | undefined;
  77. let marketMakerTimer: NodeJS.Timeout | undefined;
  78. let scalperTimer: NodeJS.Timeout | undefined;
  79. function registerFillHandler(accountId: string | undefined, handler: (fill: Fill) => Promise<void>): void {
  80. if (!accountId || killSwitchActive) return;
  81. const existing = fillHandlers.get(accountId);
  82. if (existing) {
  83. existing.push(handler);
  84. return;
  85. }
  86. fillHandlers.set(accountId, [handler]);
  87. }
  88. const fallbackAddress =
  89. process.env.PACIFICA_ACCOUNT_ADDRESS ||
  90. process.env.PACIFICA_API_KEY;
  91. const fallbackPrivateKey =
  92. process.env.PACIFICA_ACCOUNT_PRIVATE_KEY ||
  93. process.env.PACIFICA_API_SECRET;
  94. const fallbackSubaccount = process.env.PACIFICA_SUBACCOUNT || "main";
  95. if (accountEntries.length === 0) {
  96. adapterRegistry.register("maker", {
  97. baseUrl,
  98. apiKey: fallbackAddress,
  99. secret: fallbackPrivateKey,
  100. subaccount: fallbackSubaccount
  101. }, "maker");
  102. if (fallbackAddress && fallbackPrivateKey) {
  103. wsGatewayConfigs.push({ id: "maker", address: fallbackAddress, privateKey: fallbackPrivateKey });
  104. addressToAccountId.set(fallbackAddress, "maker");
  105. } else {
  106. logger.warn("WS gateway disabled for maker account: missing address/private key");
  107. }
  108. } else {
  109. for (const [id, value] of accountEntries) {
  110. if (typeof value !== "object" || value === null) continue;
  111. const upper = id.toUpperCase();
  112. const address =
  113. value.address ||
  114. value.api_key ||
  115. process.env[`${upper}_ADDRESS`] ||
  116. process.env[`${upper}_API_KEY`] ||
  117. fallbackAddress;
  118. const privateKey =
  119. value.private_key ||
  120. value.secret ||
  121. process.env[`${upper}_PRIVATE_KEY`] ||
  122. process.env[`${upper}_API_SECRET`] ||
  123. fallbackPrivateKey;
  124. const subaccount =
  125. value.subaccount ||
  126. process.env[`${upper}_SUBACCOUNT`] ||
  127. fallbackSubaccount;
  128. adapterRegistry.register(
  129. id,
  130. {
  131. baseUrl: value.api_base || baseUrl,
  132. apiKey: address,
  133. secret: privateKey,
  134. subaccount
  135. },
  136. value.role || id
  137. );
  138. if (address && privateKey) {
  139. wsGatewayConfigs.push({ id, address, privateKey });
  140. addressToAccountId.set(address, id);
  141. } else {
  142. logger.warn({ accountId: id }, "WS gateway disabled: missing address/private_key");
  143. }
  144. }
  145. }
  146. const makerEntry = adapterRegistry.findEntryByRole("maker") ?? adapterRegistry.list()[0];
  147. if (!makerEntry) {
  148. throw new Error("No Pacifica adapters configured");
  149. }
  150. const makerAccountId = makerEntry.id;
  151. const makerAdapter = makerEntry.adapter;
  152. const hedgerAccountId = adapterRegistry.findEntryByRole("hedger")?.id ?? makerAccountId;
  153. const shadow = new ShadowBook();
  154. async function main(){
  155. const strategyMode = cfg.strategy_mode || 'scalper';
  156. const primarySymbol = Array.isArray(cfg.symbols) && cfg.symbols.length ? cfg.symbols[0] : "BTC";
  157. const defaultGridSymbol = cfg.grid?.symbol || primarySymbol;
  158. const marketMakerCfg: MarketMakerRawConfig = cfg.mm || {};
  159. const scalperCfg: ScalperRawConfig = cfg.scalper || {
  160. trigger: { spread_bps: 2, min_cooldown_ms: 500 },
  161. tp_bps: 3,
  162. sl_bps: 6,
  163. clip_sz: 0.001
  164. };
  165. logger.info({ symbol: primarySymbol, baseUrl, strategyMode }, "runner start");
  166. const marketData = new MarketDataAdapter({
  167. symbols: [primarySymbol],
  168. shadowBook: shadow,
  169. fetchSnapshot: (symbol: string) => makerAdapter.getOrderBook(symbol),
  170. pollIntervalMs: cfg.market_data?.poll_interval_ms ?? 1000
  171. });
  172. await marketData.start();
  173. marketData.on('error', payload => {
  174. logger.warn({ symbol: payload.symbol, error: payload.error }, 'market data error');
  175. });
  176. const positionManager = new PositionManager();
  177. const globalCoordinator = new GlobalOrderCoordinator();
  178. const riskEngine = new RiskEngine(
  179. {
  180. maxBaseAbs: cfg.risk?.max_base_abs ?? 0,
  181. maxNotionalAbs: cfg.risk?.max_notional_abs ?? 0,
  182. maxOrderSz: cfg.risk?.max_order_sz ?? 0
  183. },
  184. mapKillSwitch(cfg.risk?.kill_switch)
  185. );
  186. const triggerKillSwitch = async (source?: string) => {
  187. if (killSwitchActive) return;
  188. killSwitchActive = true;
  189. const status = riskEngine.getStatus();
  190. const gridStatusSnapshot = gridMakerInstance?.getStatus();
  191. logger.error({ status, gridStatus: gridStatusSnapshot, source }, 'Kill-switch activated; halting strategies');
  192. const hookUrl = process.env.KILL_SWITCH_WEBHOOK;
  193. if (hookUrl && typeof fetch === 'function') {
  194. try {
  195. await fetch(hookUrl, {
  196. method: 'POST',
  197. headers: { 'Content-Type': 'application/json' },
  198. body: JSON.stringify({
  199. timestamp: new Date().toISOString(),
  200. source,
  201. status,
  202. gridStatus: gridStatusSnapshot
  203. })
  204. });
  205. } catch (error) {
  206. logger.warn({ error }, 'Failed to notify kill-switch webhook');
  207. }
  208. }
  209. if (gridStatusTimer) {
  210. clearInterval(gridStatusTimer);
  211. gridStatusTimer = undefined;
  212. }
  213. if (gridTickTimer) {
  214. clearInterval(gridTickTimer);
  215. gridTickTimer = undefined;
  216. }
  217. if (marketMakerTimer) {
  218. clearInterval(marketMakerTimer);
  219. marketMakerTimer = undefined;
  220. }
  221. if (scalperTimer) {
  222. clearInterval(scalperTimer);
  223. scalperTimer = undefined;
  224. }
  225. if (gridMakerInstance) {
  226. await gridMakerInstance.shutdown().catch(error => {
  227. logger.error({ error }, 'Failed to shutdown GridMaker');
  228. });
  229. gridMakerInstance = undefined;
  230. }
  231. marketMakerInstance = undefined;
  232. scalperInstance = undefined;
  233. fillHandlers.clear();
  234. const outstanding = globalCoordinator.list();
  235. await Promise.allSettled(outstanding.map(async snapshot => {
  236. try {
  237. const adapter = adapterRegistry.get(snapshot.accountId);
  238. const symbol = snapshot.symbol ?? defaultGridSymbol;
  239. if (snapshot.clientOrderId) {
  240. await adapter.cancelByClientId(snapshot.clientOrderId, symbol);
  241. } else {
  242. await adapter.cancel(snapshot.orderId, symbol);
  243. }
  244. globalCoordinator.release(snapshot.orderId);
  245. } catch (error) {
  246. logger.error({ orderId: snapshot.orderId, clientOrderId: snapshot.clientOrderId, error }, 'Failed to cancel outstanding order');
  247. }
  248. }));
  249. };
  250. const maybeTriggerKillSwitch = async (source?: string) => {
  251. if (killSwitchActive) return;
  252. if (!riskEngine.shouldHalt()) return;
  253. await triggerKillSwitch(source);
  254. };
  255. const refreshRisk = async (symbol: string) => {
  256. try {
  257. const snapshots = await adapterRegistry.collectPositions(symbol);
  258. const aggregated = await positionManager.snapshot(symbol, snapshots);
  259. const mid = shadow.mid(symbol) ?? aggregated.accounts[0]?.entryPx ?? 0;
  260. riskEngine.updateDeltaAbs(aggregated.base);
  261. const equity = aggregated.quote + aggregated.base * mid;
  262. riskEngine.updateEquity(equity);
  263. if (gridMakerInstance && baseClipEquityPct > 0) {
  264. const effectiveEquity = equity * Math.max(1, baseClipLeverage);
  265. const dynamicClip = Math.max(baseClipUsdFloor, effectiveEquity * baseClipEquityPct);
  266. gridMakerInstance.updateBaseClipUsd(dynamicClip);
  267. }
  268. await maybeTriggerKillSwitch('risk_refresh');
  269. } catch (error) {
  270. logger.error({ symbol, error }, 'Failed to refresh risk');
  271. }
  272. };
  273. const dispatchOrder = async (order: Order): Promise<{ id: string }> => {
  274. const accountId = order.accountId ?? makerAccountId;
  275. order.accountId = accountId;
  276. globalCoordinator.validate({
  277. accountId,
  278. symbol: order.symbol,
  279. side: order.side,
  280. price: order.px
  281. });
  282. const snapshots = await adapterRegistry.collectPositions(order.symbol);
  283. const aggregated = await positionManager.snapshot(order.symbol, snapshots);
  284. const mid = shadow.mid(order.symbol) ?? order.px;
  285. riskEngine.updateDeltaAbs(aggregated.base);
  286. const equity = aggregated.quote + aggregated.base * mid;
  287. riskEngine.updateEquity(equity);
  288. riskEngine.preCheck(order, aggregated, mid);
  289. if (killSwitchActive || riskEngine.shouldHalt()) {
  290. await maybeTriggerKillSwitch('pre_check');
  291. throw new Error('kill-switch active');
  292. }
  293. const adapter = adapterRegistry.get(accountId);
  294. const { id } = await adapter.place(order);
  295. globalCoordinator.register({
  296. orderId: id,
  297. clientOrderId: order.clientId,
  298. accountId,
  299. symbol: order.symbol,
  300. side: order.side,
  301. price: order.px,
  302. timestamp: Date.now()
  303. });
  304. return { id };
  305. };
  306. const router = new OrderRouter(
  307. async (order: Order) => dispatchOrder(order),
  308. (sym: string) => shadow.snapshot(sym),
  309. {
  310. maxBps: cfg.execution?.max_slippage_bps ?? 5,
  311. minIntervalMs: cfg.execution?.min_order_interval_ms ?? 0,
  312. bulkInitIntervalMs: cfg.execution?.bulk_init_interval_ms ?? 20
  313. }
  314. );
  315. const routeOrderUpdate = (accountId: string, update: any) => {
  316. const orderId = update?.order_id ?? update?.orderId;
  317. if (!orderId) return;
  318. const statusRaw = (update?.status ?? update?.state ?? '').toString().toLowerCase();
  319. if (statusRaw === 'filled' || statusRaw === 'canceled' || statusRaw === 'cancelled' || statusRaw === 'expired' || statusRaw === 'rejected') {
  320. globalCoordinator.release(orderId);
  321. }
  322. // 将订单更新传递给 GridMaker(如果是 maker 账户且 GridMaker 已初始化)
  323. if (accountId === makerAccountId && gridMakerInstance) {
  324. gridMakerInstance.handleOrderUpdate(update);
  325. }
  326. };
  327. const routeFill = async (accountId: string, fill: Fill) => {
  328. try {
  329. const handlers = fillHandlers.get(accountId);
  330. if (handlers) {
  331. for (const handler of handlers) {
  332. await handler(fill);
  333. }
  334. }
  335. globalCoordinator.release(fill.orderId);
  336. await refreshRisk(fill.symbol);
  337. if (accountId === hedgerAccountId) {
  338. riskEngine.recordHedgeSuccess();
  339. }
  340. await maybeTriggerKillSwitch('fill');
  341. } catch (error) {
  342. logger.error({ accountId, fill, error }, 'Failed to process fill');
  343. }
  344. };
  345. await setupWsOrderGateways({
  346. wsUrl: cfg.ws_url,
  347. routeFill,
  348. routeOrder: routeOrderUpdate,
  349. routeAccount: async (accountId, update) => {
  350. try {
  351. const symbols = extractSymbolsFromAccount(update);
  352. if (symbols.size === 0) {
  353. await refreshRisk(primarySymbol);
  354. } else {
  355. for (const symbol of symbols) {
  356. await refreshRisk(symbol);
  357. }
  358. }
  359. await maybeTriggerKillSwitch('account');
  360. } catch (error) {
  361. logger.error({ accountId, update, error }, 'Failed to process account update');
  362. }
  363. }
  364. });
  365. router.attachCancelHandlers(
  366. async orderId => {
  367. try {
  368. const snapshot = globalCoordinator.peek(orderId);
  369. const accountId = snapshot?.accountId ?? makerAccountId;
  370. const adapter = adapterRegistry.get(accountId);
  371. const symbol = snapshot?.symbol ?? defaultGridSymbol;
  372. if (snapshot?.clientOrderId) {
  373. await adapter.cancelByClientId(snapshot.clientOrderId, symbol);
  374. globalCoordinator.release(orderId);
  375. } else {
  376. await adapter.cancel(orderId, symbol);
  377. globalCoordinator.release(orderId);
  378. }
  379. } catch (error) {
  380. logger.error({ orderId, error: normalizeError(error) }, 'Failed to cancel order');
  381. }
  382. },
  383. async clientId => {
  384. try {
  385. const snapshot = globalCoordinator.peekByClientId(clientId);
  386. const accountId = snapshot?.accountId ?? makerAccountId;
  387. const adapter = adapterRegistry.get(accountId);
  388. const symbol = snapshot?.symbol ?? defaultGridSymbol;
  389. await adapter.cancelByClientId(clientId, symbol);
  390. globalCoordinator.releaseByClientId(clientId);
  391. } catch (error) {
  392. logger.error({ clientId, error: normalizeError(error) }, 'Failed to cancel by clientId');
  393. }
  394. }
  395. );
  396. const hedgeEngine = new HedgeEngine(
  397. cfg.hedge || { kp: 0.6, ki: 0.05, Qmax: 0.4, minIntervalMs: 200 },
  398. async (order: Order) => {
  399. if (killSwitchActive) {
  400. throw new Error('kill-switch active');
  401. }
  402. try {
  403. const result = await dispatchOrder({ ...order, accountId: order.accountId ?? hedgerAccountId });
  404. riskEngine.recordHedgeSuccess();
  405. return result;
  406. } catch (error) {
  407. riskEngine.recordHedgeFailure();
  408. await maybeTriggerKillSwitch('hedge_failure');
  409. throw error;
  410. }
  411. },
  412. () => shadow.mid(primarySymbol)
  413. );
  414. // 根据策略模式启动不同策略
  415. if (strategyMode === 'grid' || strategyMode === 'both') {
  416. if (baseClipEquityPct > 0) {
  417. await refreshRisk(primarySymbol);
  418. }
  419. const computeBaseClipUsd = () => {
  420. if (baseClipEquityPct <= 0) return baseClipUsdFloor;
  421. const status = riskEngine.getStatus();
  422. const equity = status.currentEquity > 0 ? status.currentEquity : status.peakEquity;
  423. const effectiveEquity = equity * Math.max(1, baseClipLeverage);
  424. const dynamic = effectiveEquity * baseClipEquityPct;
  425. return Math.max(baseClipUsdFloor, dynamic);
  426. };
  427. logger.info({ gridConfig: cfg.grid }, 'Starting Grid strategy');
  428. const gridConfig = {
  429. symbol: cfg.grid.symbol || primarySymbol,
  430. gridStepBps: cfg.grid.grid_step_bps,
  431. gridRangeBps: cfg.grid.grid_range_bps,
  432. baseClipUsd: computeBaseClipUsd(),
  433. maxLayers: cfg.grid.max_layers,
  434. hedgeThresholdBase: cfg.grid.hedge_threshold_base,
  435. accountId: cfg.grid.account_id || makerAccountId,
  436. tickSize: cfg.grid.tick_size,
  437. lotSize: cfg.grid.lot_size,
  438. incrementalMode: cfg.grid.incremental_mode ?? false
  439. };
  440. const adaptiveConfig: AdaptiveGridConfig | undefined = cfg.grid.adaptive?.enabled
  441. ? {
  442. enabled: true,
  443. volatilityWindowMinutes: cfg.grid.adaptive.volatility_window_minutes ?? 30,
  444. minVolatilityBps: cfg.grid.adaptive.min_volatility_bps ?? 20,
  445. maxVolatilityBps: cfg.grid.adaptive.max_volatility_bps ?? 200,
  446. minGridStepBps: cfg.grid.adaptive.min_grid_step_bps ?? 10,
  447. maxGridStepBps: cfg.grid.adaptive.max_grid_step_bps ?? 100,
  448. recenterEnabled: cfg.grid.adaptive.recenter_enabled ?? true,
  449. recenterThresholdBps: cfg.grid.adaptive.recenter_threshold_bps ?? 150,
  450. recenterCooldownMs: cfg.grid.adaptive.recenter_cooldown_ms ?? 300_000,
  451. minStepChangeRatio: cfg.grid.adaptive.min_step_change_ratio ?? 0.2,
  452. minSamples: cfg.grid.adaptive.min_samples,
  453. maxCadenceMs: cfg.grid.adaptive.max_cadence_ms,
  454. hedgePendingTimeoutMs: cfg.grid.adaptive.hedge_pending_timeout_ms,
  455. postOnlyCushionBps: cfg.grid.adaptive.post_only_cushion_bps ?? 5,
  456. minLayers: cfg.grid.adaptive.min_layers
  457. }
  458. : undefined;
  459. const fillRateControlConfig = cfg.grid.fill_rate_control?.enabled
  460. ? {
  461. targetFillsPerMinute: cfg.grid.fill_rate_control.target_fills_per_minute ?? 30,
  462. targetMakerRatio: cfg.grid.fill_rate_control.target_maker_ratio ?? 0.85,
  463. maxSelfTradeRatio: cfg.grid.fill_rate_control.max_self_trade_ratio ?? 0.01,
  464. kp_step: cfg.grid.fill_rate_control.kp_step ?? 0.02,
  465. ki_step: cfg.grid.fill_rate_control.ki_step ?? 0.002,
  466. kp_clip: cfg.grid.fill_rate_control.kp_clip ?? 0.1,
  467. ki_clip: cfg.grid.fill_rate_control.ki_clip ?? 0.01,
  468. minGridStepBps: cfg.grid.fill_rate_control.min_grid_step_bps ?? 0.5,
  469. maxGridStepBps: cfg.grid.fill_rate_control.max_grid_step_bps ?? 3.0,
  470. minClipUsd: cfg.grid.fill_rate_control.min_clip_usd ?? 15,
  471. maxClipUsd: cfg.grid.fill_rate_control.max_clip_usd ?? 60,
  472. minMakerRatioForAdjust: cfg.grid.fill_rate_control.min_maker_ratio_for_adjust ?? 0.70,
  473. emergencyStepMultiplier: cfg.grid.fill_rate_control.emergency_step_multiplier ?? 1.5
  474. }
  475. : undefined;
  476. const cancelAllOrders = async (symbol: string) => {
  477. try {
  478. const adapter = adapterRegistry.get(gridConfig.accountId);
  479. await adapter.cancelAll(symbol);
  480. } catch (error) {
  481. logger.error({ symbol, error: normalizeError(error) }, 'Grid cancel_all failed');
  482. throw error;
  483. }
  484. };
  485. const releaseOrder = (orderId: string, clientOrderId?: string) => {
  486. if (clientOrderId) {
  487. globalCoordinator.releaseByClientId(clientOrderId);
  488. }
  489. if (orderId) {
  490. globalCoordinator.release(orderId);
  491. }
  492. };
  493. const gridMaker = new GridMaker(
  494. gridConfig,
  495. router,
  496. hedgeEngine,
  497. shadow,
  498. logger,
  499. adaptiveConfig,
  500. cancelAllOrders,
  501. releaseOrder,
  502. fillRateControlConfig
  503. );
  504. gridMakerInstance = gridMaker;
  505. registerFillHandler(gridConfig.accountId, async fill => {
  506. if (fill.symbol !== gridConfig.symbol) return;
  507. await gridMaker.onFill(fill);
  508. });
  509. registerFillHandler(hedgerAccountId, async fill => {
  510. if (fill.symbol !== gridConfig.symbol) return;
  511. await gridMaker.onHedgeFill(fill);
  512. });
  513. // 清理旧订单并初始化网格
  514. logger.info({ symbol: gridConfig.symbol, accountId: gridConfig.accountId }, 'Cleaning up old orders before grid initialization');
  515. try {
  516. await cancelAllOrders(gridConfig.symbol);
  517. logger.info('Old orders cancelled successfully');
  518. } catch (error) {
  519. logger.warn({ error: normalizeError(error) }, 'Failed to cancel old orders, proceeding with initialization');
  520. }
  521. await gridMaker.initialize();
  522. // 模拟 Fill 事件监听(实际需要从 adapter 或 WebSocket 获取)
  523. // adapter.onFill((fill: Fill) => gridMaker.onFill(fill));
  524. // 定期输出状态
  525. gridStatusTimer = setInterval(() => {
  526. if (killSwitchActive) return;
  527. const status = gridMaker.getStatus();
  528. logger.info({ status }, 'Grid status');
  529. }, 30000); // 每 30 秒
  530. gridTickTimer = setInterval(async () => {
  531. if (killSwitchActive) return;
  532. try {
  533. await gridMaker.onTick();
  534. } catch (error) {
  535. console.error('GridMaker onTick failed:', error);
  536. logger.error({
  537. error: error instanceof Error ? {
  538. message: error.message,
  539. stack: error.stack,
  540. name: error.name
  541. } : error
  542. }, 'GridMaker onTick failed');
  543. }
  544. }, cfg.grid.adaptive?.tick_interval_ms ?? 60_000);
  545. }
  546. if (strategyMode === 'scalper' || strategyMode === 'both') {
  547. logger.info('Starting MarketMaker + Scalper strategies');
  548. const mm = new MarketMaker(
  549. {
  550. symbol: primarySymbol,
  551. tickSz: marketMakerCfg.tick_sz ?? 0.5,
  552. clipSz: marketMakerCfg.clip_sz ?? 0.001,
  553. spreadBps: marketMakerCfg.spread_bps ?? 1.6
  554. },
  555. router,
  556. ()=>shadow.snapshot(primarySymbol),
  557. logger
  558. );
  559. marketMakerInstance = mm;
  560. registerFillHandler(makerAccountId, async fill => {
  561. if (fill.symbol !== primarySymbol) return;
  562. await mm.onFill(fill);
  563. });
  564. const scalp = new MicroScalper(
  565. {
  566. symbol: primarySymbol,
  567. clipSz: scalperCfg.clip_sz ?? 0.001,
  568. triggerSpreadBps: scalperCfg.trigger?.spread_bps ?? 1.8,
  569. tpBps: scalperCfg.tp_bps ?? 3,
  570. slBps: scalperCfg.sl_bps ?? 6,
  571. cooldownMs: scalperCfg.trigger?.min_cooldown_ms ?? 250
  572. },
  573. router,
  574. ()=>shadow.snapshot(primarySymbol),
  575. logger
  576. );
  577. scalperInstance = scalp;
  578. registerFillHandler(makerAccountId, async fill => {
  579. if (fill.symbol !== primarySymbol) return;
  580. await scalp.onFill(fill);
  581. });
  582. const mmInterval = marketMakerCfg.reprice_ms ?? 300;
  583. const scalperInterval = scalperCfg.on_book_interval_ms ?? 150;
  584. marketMakerTimer = setInterval(async () => {
  585. if (killSwitchActive) return;
  586. try {
  587. await mm.onTick();
  588. } catch (error) {
  589. logger.error({ error }, 'MarketMaker onTick failed');
  590. }
  591. }, mmInterval);
  592. scalperTimer = setInterval(async () => {
  593. if (killSwitchActive) return;
  594. try {
  595. await scalp.onBook();
  596. } catch (error) {
  597. logger.error({ error }, 'MicroScalper onBook failed');
  598. }
  599. }, scalperInterval);
  600. }
  601. }
  602. main().catch(e => {
  603. logger.error({ error: e }, 'Fatal error in main');
  604. setImmediate(() => process.exit(1));
  605. });
  606. function createRunnerLogger() {
  607. const logLevel = (process.env.LOG_LEVEL ?? "info") as pino.Level;
  608. const streams: pino.StreamEntry[] = [
  609. { level: logLevel, stream: process.stdout }
  610. ];
  611. let filePath: string | undefined;
  612. try {
  613. const fileLevel = (process.env.LOG_FILE_LEVEL ?? logLevel) as pino.Level;
  614. const customPath = process.env.LOG_FILE;
  615. const logDir = process.env.LOG_DIR ?? "logs";
  616. const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  617. const resolvedPath = resolve(customPath ?? join(logDir, `runner-${timestamp}.log`));
  618. mkdirSync(dirname(resolvedPath), { recursive: true });
  619. const destination = pino.destination({ dest: resolvedPath, mkdir: true, sync: false });
  620. globalFileDestination = destination;
  621. streams.push({ level: fileLevel, stream: destination });
  622. filePath = resolvedPath;
  623. } catch (error) {
  624. console.error("Failed to enable file logging", error);
  625. }
  626. const instance =
  627. streams.length > 1
  628. ? pino({ level: logLevel }, pino.multistream(streams))
  629. : pino({ level: logLevel });
  630. if (filePath) {
  631. instance.info({ logFile: filePath }, "File logging enabled");
  632. }
  633. // 优雅退出处理:确保日志刷新到磁盘
  634. const gracefulShutdown = (signal: string) => {
  635. instance.info({ signal }, 'Received shutdown signal, flushing logs...');
  636. flushLogsSafe();
  637. setTimeout(() => {
  638. instance.info('Logs flushed, exiting');
  639. process.exit(0);
  640. }, 100);
  641. };
  642. process.on('SIGINT', () => gracefulShutdown('SIGINT'));
  643. process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
  644. process.on('uncaughtException', (error) => {
  645. console.error('Uncaught exception:', error);
  646. try {
  647. instance.error({ error: error instanceof Error ? { message: error.message, stack: error.stack } : error }, 'Uncaught exception');
  648. } catch {}
  649. flushLogsSafe();
  650. process.exit(1);
  651. });
  652. process.on('unhandledRejection', (reason) => {
  653. instance.error({ reason }, 'Unhandled rejection');
  654. flushLogsSafe();
  655. process.exit(1);
  656. });
  657. return instance;
  658. }
  659. async function setupWsOrderGateways(options: {
  660. wsUrl?: string;
  661. routeFill: (accountId: string, fill: Fill) => Promise<void>;
  662. routeOrder: (accountId: string, update: any) => void;
  663. routeAccount: (accountId: string, update: any) => Promise<void>;
  664. }): Promise<void> {
  665. const { wsUrl, routeFill, routeOrder, routeAccount } = options;
  666. if (!wsUrl) {
  667. logger.warn('No ws_url configured; skipping WebSocket order gateway setup');
  668. return;
  669. }
  670. const tasks = wsGatewayConfigs
  671. .filter(entry => entry.address && entry.privateKey)
  672. .map(async entry => {
  673. try {
  674. const address = entry.address!;
  675. const privateKey = entry.privateKey!;
  676. const wsClient = new PacificaWebSocket({
  677. url: wsUrl,
  678. apiKey: address,
  679. secret: privateKey
  680. });
  681. const limiter = wsRateLimiterConfig
  682. ? new RateLimiter({
  683. burst: wsRateLimiterConfig.burst ?? 6,
  684. refillPerSec: wsRateLimiterConfig.refill_per_sec ?? 5,
  685. maxQueueDepth: wsRateLimiterConfig.max_queue_depth ?? 100
  686. })
  687. : undefined;
  688. const gateway = new PacificaWsOrderGateway(wsClient, {
  689. apiKey: address,
  690. secret: privateKey
  691. }, undefined, undefined, logger, limiter);
  692. wsClient.on('message', message => {
  693. handlePrivateMessage(address, message, routeFill, routeOrder, routeAccount);
  694. });
  695. await gateway.connect();
  696. adapterRegistry.get(entry.id).attachWsGateway(gateway);
  697. const tradesParams = { source: 'account_trades', account: address };
  698. const ordersParams = { source: 'account_order_updates', account: address };
  699. wsClient.subscribeAuthenticated('account_trades', tradesParams);
  700. wsClient.subscribeAuthenticated('account_order_updates', ordersParams);
  701. logger.info({ accountId: entry.id, tradesParams, ordersParams }, 'WS order gateway connected and subscribed to channels');
  702. } catch (error) {
  703. logger.error({ accountId: entry.id, error }, 'Failed to initialize WS order gateway');
  704. throw error;
  705. }
  706. });
  707. await Promise.all(tasks);
  708. }
  709. function handlePrivateMessage(
  710. address: string,
  711. raw: unknown,
  712. routeFill: (accountId: string, fill: Fill) => Promise<void>,
  713. routeOrder: (accountId: string, update: any) => void,
  714. routeAccount: (accountId: string, update: any) => Promise<void>
  715. ): void {
  716. const accountId = addressToAccountId.get(address);
  717. if (!accountId) return;
  718. let text: string;
  719. if (typeof raw === "string") {
  720. text = raw;
  721. } else if (raw instanceof Buffer) {
  722. text = raw.toString("utf8");
  723. } else if (typeof (raw as any)?.toString === "function") {
  724. text = (raw as any).toString();
  725. } else {
  726. return;
  727. }
  728. let parsed: any;
  729. try {
  730. parsed = JSON.parse(text);
  731. } catch {
  732. return;
  733. }
  734. const channel = parsed?.channel;
  735. if (typeof channel !== "string") return;
  736. if (channel === "account_trades" || (channel.startsWith("fills.") && channel.endsWith(address))) {
  737. const payload = parsed.data;
  738. const records = Array.isArray(payload) ? payload : [payload];
  739. logger.info({ accountId, channel, recordCount: records.length }, 'Received fill message from WebSocket');
  740. for (const record of records) {
  741. if (record?.u && record.u !== address) continue;
  742. const fill = mapFillPayload(record);
  743. if (!fill) {
  744. logger.warn({ accountId, record }, 'Failed to map fill payload');
  745. continue;
  746. }
  747. logger.info({ accountId, fill }, 'Processing fill event');
  748. routeFill(accountId, fill).catch(error => {
  749. logger.error({ accountId, error }, 'Failed to route fill');
  750. });
  751. }
  752. return;
  753. }
  754. if (channel === "account_order_updates" || (channel.startsWith("orders.") && channel.endsWith(address))) {
  755. const payload = parsed.data;
  756. const records = Array.isArray(payload) ? payload : [payload];
  757. logger.info({ accountId, channel, recordCount: records.length }, 'Received order update message from WebSocket');
  758. for (const record of records) {
  759. if (record?.u && record.u !== address) continue;
  760. const update = record ?? {};
  761. logger.info({ accountId, orderUpdate: update }, 'Processing order update event');
  762. routeOrder(accountId, update);
  763. }
  764. return;
  765. }
  766. if (channel.startsWith("account.") && channel.endsWith(address)) {
  767. const payload = parsed.data ?? parsed;
  768. routeAccount(accountId, payload).catch(error => {
  769. logger.error({ accountId, error }, 'Failed to route account update');
  770. });
  771. return;
  772. }
  773. logger.debug({ accountId, channel, payload: parsed?.data ?? parsed }, 'Unhandled Pacifica WS message');
  774. }
  775. function mapFillPayload(data: any): Fill | undefined {
  776. if (!data) return undefined;
  777. const orderId = data.order_id ?? data.orderId ?? data.i ?? data.orderId;
  778. const symbol = data.symbol ?? data.s;
  779. const rawSideInput = (data.side ?? data.direction ?? data.d ?? data.ts ?? '').toString().toLowerCase();
  780. let side: 'buy' | 'sell' | undefined;
  781. if (rawSideInput === 'bid' || rawSideInput === 'buy' || rawSideInput.includes('long')) {
  782. side = 'buy';
  783. } else if (rawSideInput === 'ask' || rawSideInput === 'sell' || rawSideInput.includes('short')) {
  784. side = 'sell';
  785. }
  786. const price = Number(data.price ?? data.px ?? data.p);
  787. const sizeRaw = Number(data.size ?? data.sz ?? data.amount ?? data.a);
  788. if (!orderId || !symbol || !side || !Number.isFinite(price) || !Number.isFinite(sizeRaw)) {
  789. return undefined;
  790. }
  791. const size = Math.abs(sizeRaw);
  792. const fee = data.fee !== undefined ? Number(data.fee) : data.f !== undefined ? Number(data.f) : 0;
  793. const liquidityRaw = (data.liquidity ?? data.te ?? '').toString().toLowerCase();
  794. const liquidity = liquidityRaw.includes('taker') ? 'taker' : 'maker';
  795. const tradeId = String(data.trade_id ?? data.tradeId ?? data.h ?? `${orderId}-${Date.now()}`);
  796. const tsValue = data.ts !== undefined && Number.isFinite(Number(data.ts)) ? Number(data.ts)
  797. : data.t !== undefined ? Number(data.t)
  798. : Date.now();
  799. const ts = Number.isFinite(tsValue) ? tsValue : Date.now();
  800. return {
  801. orderId: String(orderId),
  802. tradeId,
  803. symbol: String(symbol),
  804. side,
  805. px: price,
  806. sz: size,
  807. fee,
  808. liquidity: liquidity as "maker" | "taker",
  809. ts
  810. };
  811. }
  812. function mapKillSwitch(raw: any): KillSwitchConfig | undefined {
  813. if (!raw) return undefined;
  814. const drawdownPct = Number(raw.drawdown_pct ?? raw.drawdownPct ?? 0);
  815. const triggers = Array.isArray(raw.triggers)
  816. ? raw.triggers
  817. .map((t: any) => ({
  818. type: t.type,
  819. threshold: Number(t.threshold)
  820. }))
  821. .filter((trigger: { type?: string; threshold: number }): trigger is { type: string; threshold: number } => {
  822. return typeof trigger.type === "string" && !Number.isNaN(trigger.threshold);
  823. })
  824. : undefined;
  825. return {
  826. drawdownPct: Math.abs(drawdownPct),
  827. triggers
  828. };
  829. }
  830. function extractSymbolsFromAccount(update: any): Set<string> {
  831. const symbols = new Set<string>();
  832. if (!update) return symbols;
  833. const collect = (value: any) => {
  834. if (!value) return;
  835. const items = Array.isArray(value) ? value : [value];
  836. for (const item of items) {
  837. const symbol = item?.symbol ?? item?.asset ?? item?.pair;
  838. if (typeof symbol === "string" && symbol.trim().length > 0) {
  839. symbols.add(symbol.trim());
  840. }
  841. }
  842. };
  843. if (Array.isArray(update)) {
  844. for (const item of update) {
  845. const nested = extractSymbolsFromAccount(item);
  846. nested.forEach(s => symbols.add(s));
  847. }
  848. return symbols;
  849. }
  850. collect(update.positions ?? update.position);
  851. collect(update.balances ?? update.balance);
  852. collect(update.holdings);
  853. if (update.symbol) {
  854. collect(update);
  855. }
  856. return symbols;
  857. }
  858. function normalizeError(error: unknown): { message?: string; name?: string; status?: number; code?: string } | undefined {
  859. if (!error) return undefined;
  860. if (error instanceof Error) {
  861. const normalized: { message: string; name: string; status?: number; code?: string } = {
  862. message: error.message,
  863. name: error.name
  864. };
  865. const anyErr = error as any;
  866. if (typeof anyErr.status === "number") {
  867. normalized.status = anyErr.status;
  868. }
  869. if (anyErr.code !== undefined) {
  870. normalized.code = String(anyErr.code);
  871. }
  872. return normalized;
  873. }
  874. if (typeof error === "object") {
  875. return error as any;
  876. }
  877. return { message: String(error) };
  878. }