perp-cli 0.4.2 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +46 -5
  2. package/dist/arb/reconcile.d.ts +28 -0
  3. package/dist/arb/reconcile.js +162 -0
  4. package/dist/arb/sizing.d.ts +12 -1
  5. package/dist/arb/sizing.js +38 -5
  6. package/dist/arb/state.d.ts +6 -0
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/account.d.ts +1 -1
  10. package/dist/commands/account.js +300 -63
  11. package/dist/commands/agent.js +16 -9
  12. package/dist/commands/arb/index.js +243 -49
  13. package/dist/commands/arb-auto.js +696 -9
  14. package/dist/commands/arb.d.ts +2 -1
  15. package/dist/commands/arb.js +535 -131
  16. package/dist/commands/bot.js +207 -0
  17. package/dist/commands/dex.js +7 -1
  18. package/dist/commands/funding.d.ts +2 -1
  19. package/dist/commands/funding.js +211 -4
  20. package/dist/commands/funds.d.ts +4 -0
  21. package/dist/commands/funds.js +741 -0
  22. package/dist/commands/history.d.ts +2 -1
  23. package/dist/commands/history.js +508 -5
  24. package/dist/commands/manage.js +6 -4
  25. package/dist/commands/market.js +35 -2
  26. package/dist/commands/multileg.d.ts +16 -0
  27. package/dist/commands/multileg.js +174 -0
  28. package/dist/commands/pnl.d.ts +12 -0
  29. package/dist/commands/pnl.js +243 -0
  30. package/dist/commands/risk.d.ts +2 -0
  31. package/dist/commands/risk.js +56 -0
  32. package/dist/commands/settings.js +147 -0
  33. package/dist/commands/stream.js +132 -55
  34. package/dist/commands/trade.d.ts +1 -1
  35. package/dist/commands/trade.js +270 -267
  36. package/dist/commands/wallet.d.ts +4 -0
  37. package/dist/commands/wallet.js +11 -1
  38. package/dist/config.d.ts +6 -1
  39. package/dist/config.js +21 -1
  40. package/dist/dashboard/ws-feeds.js +1 -1
  41. package/dist/equity-tracker.d.ts +57 -0
  42. package/dist/equity-tracker.js +209 -0
  43. package/dist/exchanges/hyperliquid-spot.d.ts +69 -0
  44. package/dist/exchanges/hyperliquid-spot.js +309 -0
  45. package/dist/exchanges/hyperliquid.d.ts +26 -5
  46. package/dist/exchanges/hyperliquid.js +171 -73
  47. package/dist/exchanges/interface.d.ts +1 -1
  48. package/dist/exchanges/lighter-spot.d.ts +55 -0
  49. package/dist/exchanges/lighter-spot.js +415 -0
  50. package/dist/exchanges/lighter.d.ts +10 -2
  51. package/dist/exchanges/lighter.js +210 -56
  52. package/dist/exchanges/pacifica.d.ts +11 -4
  53. package/dist/exchanges/pacifica.js +64 -37
  54. package/dist/exchanges/spot-interface.d.ts +45 -0
  55. package/dist/exchanges/spot-interface.js +22 -0
  56. package/dist/execution-log.d.ts +2 -2
  57. package/dist/funding/rates.d.ts +36 -0
  58. package/dist/funding/rates.js +146 -0
  59. package/dist/index.js +126 -161
  60. package/dist/liquidity.d.ts +12 -0
  61. package/dist/liquidity.js +43 -0
  62. package/dist/mcp-server.js +43 -96
  63. package/dist/signer/evm-local.d.ts +12 -0
  64. package/dist/signer/evm-local.js +19 -0
  65. package/dist/signer/index.d.ts +3 -0
  66. package/dist/signer/index.js +2 -0
  67. package/dist/signer/interface.d.ts +31 -0
  68. package/dist/signer/interface.js +7 -0
  69. package/dist/signer/solana-local.d.ts +13 -0
  70. package/dist/signer/solana-local.js +19 -0
  71. package/dist/smart-order.js +14 -3
  72. package/dist/strategies/split-order.d.ts +49 -0
  73. package/dist/strategies/split-order.js +138 -0
  74. package/dist/utils.js +7 -1
  75. package/dist/ws/market-feed.d.ts +74 -0
  76. package/dist/ws/market-feed.js +227 -0
  77. package/package.json +1 -1
  78. package/skills/perp-cli/SKILL.md +80 -4
  79. package/skills/perp-cli/references/agent-operations.md +19 -9
  80. package/skills/perp-cli/references/commands.md +54 -32
  81. package/skills/perp-cli/references/strategies.md +27 -17
  82. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  83. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  84. package/skills/perp-cli/scripts/preflight.sh +98 -0
  85. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  86. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  87. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  88. package/skills/perp-cli/templates/open-position.sh +0 -38
  89. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -318,6 +318,12 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
318
318
  const symbol = extractSymbol(g) || "<symbol>";
319
319
  steps.push({ step: 1, command: `perp -e ${ex} --json account positions`, description: "Check current position" }, { step: 2, command: `perp -e ${ex} --json trade stop ${symbol} <side> <stopPrice> <size>`, description: "Place stop order", dangerous: true });
320
320
  }
321
+ else if (g.includes("split") || g.includes("depth") || (g.includes("large") && (g.includes("order") || g.includes("buy") || g.includes("sell")))) {
322
+ const symbol = extractSymbol(g) || "<symbol>";
323
+ const amount = extractNumber(g) || "<usd>";
324
+ const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
325
+ steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade split ${symbol} ${side} ${amount}`, description: `Split ${side} $${amount} ${symbol} into depth-based slices`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
326
+ }
321
327
  else if (g.includes("twap")) {
322
328
  const symbol = extractSymbol(g) || "<symbol>";
323
329
  const size = extractNumber(g) || "<size>";
@@ -338,19 +344,16 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
338
344
  else if (g.includes("funding")) {
339
345
  const symbol = extractSymbol(g);
340
346
  if (symbol) {
341
- steps.push({ step: 1, command: `perp --json funding compare ${symbol}`, description: `Compare ${symbol} funding rates` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` });
347
+ steps.push({ step: 1, command: `perp --json arb rates`, description: `Funding rates across all exchanges` }, { step: 2, command: `perp -e ${ex} --json market funding ${symbol}`, description: `${symbol} funding history` });
342
348
  }
343
349
  else {
344
- steps.push({ step: 1, command: "perp --json funding rates", description: "Funding rates across all exchanges" }, { step: 2, command: "perp --json funding spread", description: "Funding rate spreads" });
350
+ steps.push({ step: 1, command: "perp --json arb rates", description: "Funding rates across all exchanges" }, { step: 2, command: "perp --json arb scan --min 5", description: "Funding rate arb opportunities" });
345
351
  }
346
352
  }
347
353
  else if (g.includes("bridge") || g.includes("transfer") || g.includes("cross-chain")) {
348
354
  const amount = extractNumber(g) || "<amount>";
349
355
  steps.push({ step: 1, command: "perp --json bridge chains", description: "List supported chains" }, { step: 2, command: `perp --json bridge quote --from <chain> --to <chain> --amount ${amount}`, description: "Get bridge quote with fees" }, { step: 3, command: `perp --json bridge send --from <chain> --to <chain> --amount ${amount}`, description: "Execute bridge transfer", dangerous: true }, { step: 4, command: "perp --json bridge status <orderId>", description: "Track bridge completion" });
350
356
  }
351
- else if (g.includes("alert") || g.includes("notify") || g.includes("alarm")) {
352
- steps.push({ step: 1, command: "perp --json alert list", description: "List existing alerts" }, { step: 2, command: "perp --json alert add", description: "Add a new price/funding/pnl/liquidation alert" }, { step: 3, command: "perp --json alert daemon", description: "Start alert monitoring daemon" });
353
- }
354
357
  else if (g.includes("grid") || g.includes("dca") || g.includes("bot")) {
355
358
  const symbol = extractSymbol(g) || "<symbol>";
356
359
  if (g.includes("grid")) {
@@ -407,7 +410,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
407
410
  steps.push({ step: 1, command: "perp --json jobs list", description: "List running background jobs" });
408
411
  }
409
412
  else if (g.includes("dex") || g.includes("hip-3") || g.includes("hip3")) {
410
- steps.push({ step: 1, command: "perp --json dex list", description: "List HIP-3 deployed dexes" }, { step: 2, command: "perp --json dex markets <dexName>", description: "Show markets on a specific dex" });
413
+ steps.push({ step: 1, command: "perp --json -e hl market hip3", description: "List HIP-3 deployed dexes" }, { step: 2, command: "perp --json -e hl --dex <name> market list", description: "Show markets on a specific dex" });
411
414
  }
412
415
  else if (g.includes("plan") || g.includes("composite") || g.includes("multi-step")) {
413
416
  steps.push({ step: 1, command: "perp plan example", description: "Show example execution plan format" }, { step: 2, command: "perp --json plan validate <file>", description: "Validate plan file" }, { step: 3, command: "perp --json plan execute <file> --dry-run", description: "Dry-run the plan", dangerous: true });
@@ -417,11 +420,11 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
417
420
  }
418
421
  else if (g.includes("deposit")) {
419
422
  const amount = extractNumber(g) || "<amount>";
420
- steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json account info`, description: "Verify deposit arrived" });
423
+ steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json account info`, description: "Verify deposit arrived" });
421
424
  }
422
425
  else if (g.includes("withdraw")) {
423
426
  const amount = extractNumber(g) || "<amount>";
424
- steps.push({ step: 1, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 2, command: `perp --json withdraw ${ex} ${amount}`, description: `Withdraw $${amount} from ${ex}`, dangerous: true }, { step: 3, command: "perp --json wallet balance", description: "Verify withdrawal received" });
427
+ steps.push({ step: 1, command: `perp -e ${ex} --json account info`, description: "Check available balance" }, { step: 2, command: `perp --json funds withdraw ${ex} ${amount}`, description: `Withdraw $${amount} from ${ex}`, dangerous: true }, { step: 3, command: "perp --json wallet balance", description: "Verify withdrawal received" });
425
428
  }
426
429
  else if (g.includes("leverage")) {
427
430
  const symbol = extractSymbol(g) || "<symbol>";
@@ -450,7 +453,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
450
453
  steps.push({ step: 1, command: "perp settings show", description: "Show current settings" }, { step: 2, command: "perp settings referrals on", description: "Enable referral codes (no extra fees, just volume tracking)" });
451
454
  }
452
455
  else {
453
- steps.push({ step: 1, command: "perp agent capabilities", description: "List all available CLI capabilities" }, { step: 2, command: `perp -e ${ex} --json status`, description: "Check account status" }, { step: 3, command: "perp --json health", description: "Check exchange connectivity" });
456
+ steps.push({ step: 1, command: "perp agent capabilities", description: "List all available CLI capabilities" }, { step: 2, command: `perp -e ${ex} --json status`, description: "Check account status" }, { step: 3, command: "perp --json agent ping", description: "Check exchange connectivity" });
454
457
  }
455
458
  return {
456
459
  content: [{
@@ -750,11 +753,12 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
750
753
  relatedCommands: ["perp bridge chains", "perp bridge quote"],
751
754
  };
752
755
  }
753
- else if (category === "deposit" || category === "withdraw") {
756
+ else if (category === "funds") {
757
+ const sub = args[1]; // deposit or withdraw
754
758
  explanation = {
755
759
  command,
756
- description: category === "deposit" ? `Deposits USDC into ${args[1] || "an exchange"} account` : `Withdraws USDC from ${args[1] || "an exchange"} account`,
757
- parameters: [{ name: "amount", value: args[2] || args[1], description: "Amount in USDC" }],
760
+ description: sub === "deposit" ? `Deposits USDC into ${args[2] || "an exchange"} account` : sub === "withdraw" ? `Withdraws USDC from ${args[2] || "an exchange"} account` : "Funds management (deposit, withdraw, bridge, transfer)",
761
+ parameters: [{ name: "amount", value: args[3] || args[2], description: "Amount in USDC" }],
758
762
  risks: ["Moves real funds — verify amount before executing"],
759
763
  category: "write",
760
764
  relatedCommands: ["perp wallet balance", "perp account info"],
@@ -777,7 +781,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
777
781
  parameters: [],
778
782
  risks: [],
779
783
  category: "read",
780
- relatedCommands: ["perp alert list", "perp alert add"],
784
+ relatedCommands: ["perp risk status", "perp risk limits"],
781
785
  };
782
786
  }
783
787
  else if (category === "bot" || category === "run") {
@@ -790,24 +794,14 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
790
794
  relatedCommands: ["perp jobs list", "perp jobs stop"],
791
795
  };
792
796
  }
793
- else if (category === "stream") {
794
- explanation = {
795
- command,
796
- description: `Live WebSocket stream: ${sub}. Outputs NDJSON in real-time.`,
797
- parameters: args.slice(2).map((a, i) => ({ name: `arg${i}`, value: a, description: "See --help" })),
798
- risks: [],
799
- category: "read",
800
- relatedCommands: ["perp stream prices", "perp stream events"],
801
- };
802
- }
803
- else if (category === "analytics") {
797
+ else if (category === "history") {
804
798
  explanation = {
805
799
  command,
806
- description: { summary: "Trading performance summary", pnl: "P&L breakdown by symbol and time period", funding: "Funding payment history and totals", report: "Detailed performance report" }[sub] || `Analytics: ${sub}`,
800
+ description: { summary: "Trading performance summary", pnl: "P&L breakdown by symbol and time period", funding: "Funding payment history and totals", report: "Detailed performance report", perf: "Performance breakdown (daily/weekly/summary)" }[sub] || `History: ${sub}`,
807
801
  parameters: [],
808
802
  risks: [],
809
803
  category: "read",
810
- relatedCommands: ["perp analytics summary", "perp history list"],
804
+ relatedCommands: ["perp history summary", "perp history list"],
811
805
  };
812
806
  }
813
807
  else if (category === "wallet") {
@@ -830,16 +824,6 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
830
824
  relatedCommands: ["perp gap show", "perp arb scan"],
831
825
  };
832
826
  }
833
- else if (category === "funding") {
834
- explanation = {
835
- command,
836
- description: { rates: "Compare funding rates across exchanges", compare: "Compare a specific symbol's funding", spread: "Show funding rate spreads", history: "Funding rate history", monitor: "Live funding rate monitor" }[sub] || `Funding: ${sub}`,
837
- parameters: [],
838
- risks: [],
839
- category: "read",
840
- relatedCommands: ["perp funding rates", "perp arb scan"],
841
- };
842
- }
843
827
  else if (category === "backtest") {
844
828
  explanation = {
845
829
  command,
@@ -861,7 +845,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
861
845
  };
862
846
  }
863
847
  else {
864
- const writeCommands = new Set(["trade", "deposit", "withdraw", "manage", "rebalance"]);
848
+ const writeCommands = new Set(["trade", "funds", "manage", "rebalance"]);
865
849
  explanation = {
866
850
  command,
867
851
  description: `CLI command: ${command}. Run 'perp ${category} --help' for detailed usage.`,
@@ -1056,32 +1040,16 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1056
1040
  status: { usage: "perp bridge status <orderId>", description: "Track bridge status" },
1057
1041
  },
1058
1042
  },
1059
- alert: {
1060
- description: "Price, funding, PnL, and liquidation alerts",
1061
- subcommands: {
1062
- add: { usage: "perp alert add", description: "Add alert" },
1063
- list: { usage: "perp alert list", description: "List alerts" },
1064
- remove: { usage: "perp alert remove <id>", description: "Remove alert" },
1065
- daemon: { usage: "perp alert daemon", description: "Start alert monitoring" },
1066
- },
1067
- },
1068
1043
  bot: {
1069
1044
  description: "Automated trading bots",
1070
1045
  subcommands: {
1071
- start: { usage: "perp bot start <config>", description: "Start bot from config" },
1046
+ twap: { usage: "perp bot twap <symbol> <side> <size> <duration>", description: "TWAP execution" },
1047
+ grid: { usage: "perp bot grid <symbol> --range <pct> --grids <n> --size <usd>", description: "Grid trading bot" },
1048
+ dca: { usage: "perp bot dca <symbol> <side> <amount> <interval>", description: "DCA bot" },
1049
+ "trailing-stop": { usage: "perp bot trailing-stop <symbol>", description: "Trailing stop bot" },
1072
1050
  "quick-grid": { usage: "perp bot quick-grid <symbol>", description: "Quick grid bot" },
1073
1051
  "quick-dca": { usage: "perp bot quick-dca <symbol> <side> <amount> <interval>", description: "Quick DCA bot" },
1074
1052
  "quick-arb": { usage: "perp bot quick-arb", description: "Quick arb bot" },
1075
- "preset-list": { usage: "perp bot preset-list", description: "List bot presets" },
1076
- },
1077
- },
1078
- stream: {
1079
- description: "Live WebSocket streams (NDJSON output)",
1080
- subcommands: {
1081
- prices: { usage: "perp stream prices", description: "Live prices" },
1082
- book: { usage: "perp stream book <symbol>", description: "Live orderbook" },
1083
- trades: { usage: "perp stream trades <symbol>", description: "Live trades" },
1084
- events: { usage: "perp stream events", description: "Account events (fills, liquidations)" },
1085
1053
  },
1086
1054
  },
1087
1055
  wallet: {
@@ -1092,37 +1060,15 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1092
1060
  generate: { usage: "perp wallet generate solana|evm", description: "Generate new wallet" },
1093
1061
  },
1094
1062
  },
1095
- analytics: {
1096
- description: "Trading performance analytics",
1097
- subcommands: {
1098
- summary: { usage: "perp analytics summary", description: "Performance summary" },
1099
- pnl: { usage: "perp analytics pnl", description: "P&L breakdown" },
1100
- funding: { usage: "perp analytics funding", description: "Funding payment history" },
1101
- report: { usage: "perp analytics report --since <period>", description: "Detailed report" },
1102
- },
1103
- },
1104
1063
  history: {
1105
- description: "Execution log & audit trail",
1064
+ description: "Execution log, performance & audit trail",
1106
1065
  subcommands: {
1107
1066
  list: { usage: "perp history list", description: "Execution log" },
1108
- stats: { usage: "perp history stats", description: "Execution statistics" },
1109
- positions: { usage: "perp history positions", description: "Position history" },
1110
- },
1111
- },
1112
- funding: {
1113
- description: "Funding rate comparison",
1114
- subcommands: {
1115
- rates: { usage: "perp funding rates", description: "Rates across exchanges" },
1116
- compare: { usage: "perp funding compare <symbol>", description: "Compare symbol's funding" },
1117
- spread: { usage: "perp funding spread", description: "Funding rate spreads" },
1118
- monitor: { usage: "perp funding monitor", description: "Live funding monitor" },
1119
- },
1120
- },
1121
- gap: {
1122
- description: "Cross-exchange price gap analysis",
1123
- subcommands: {
1124
- show: { usage: "perp gap show", description: "Current price gaps" },
1125
- watch: { usage: "perp gap watch --min <pct>", description: "Live gap monitor" },
1067
+ summary: { usage: "perp history summary", description: "Performance summary" },
1068
+ pnl: { usage: "perp history pnl", description: "P&L breakdown" },
1069
+ funding: { usage: "perp history funding", description: "Funding payment history" },
1070
+ report: { usage: "perp history report", description: "Full performance report" },
1071
+ perf: { usage: "perp history perf --period daily|weekly|summary", description: "Performance breakdown" },
1126
1072
  },
1127
1073
  },
1128
1074
  backtest: {
@@ -1132,14 +1078,6 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1132
1078
  grid: { usage: "perp backtest grid", description: "Backtest grid strategy" },
1133
1079
  },
1134
1080
  },
1135
- dex: {
1136
- description: "HIP-3 deployed perp dexes (Hyperliquid)",
1137
- subcommands: {
1138
- list: { usage: "perp dex list", description: "List dexes" },
1139
- markets: { usage: "perp dex markets <name>", description: "Dex markets" },
1140
- balance: { usage: "perp dex balance <name>", description: "Dex balance" },
1141
- },
1142
- },
1143
1081
  plan: {
1144
1082
  description: "Composite multi-step execution plans",
1145
1083
  subcommands: {
@@ -1164,11 +1102,20 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1164
1102
  logs: { usage: "perp jobs logs <id>", description: "View job logs" },
1165
1103
  },
1166
1104
  },
1167
- deposit: { usage: "perp deposit <exchange> <amount>", description: "Deposit USDC to exchange" },
1168
- withdraw: { usage: "perp withdraw <exchange> <amount>", description: "Withdraw USDC from exchange" },
1105
+ funds: {
1106
+ usage: "perp funds <deposit|withdraw|transfer|bridge> ...",
1107
+ description: "Deposit, withdraw, bridge & transfer funds",
1108
+ subcommands: {
1109
+ "deposit <exchange> <amount>": "Deposit USDC to exchange",
1110
+ "withdraw <exchange> <amount>": "Withdraw USDC from exchange",
1111
+ "transfer <amount> <address>": "HL internal transfer",
1112
+ "bridge --from <chain> --to <chain> --amount <n> --recipient <addr>": "CCTP bridge",
1113
+ info: "Show all deposit/withdraw routes & limits",
1114
+ },
1115
+ },
1169
1116
  portfolio: { usage: "perp portfolio", description: "Cross-exchange portfolio summary" },
1170
1117
  status: { usage: "perp portfolio", description: "Full account overview" },
1171
- health: { usage: "perp health", description: "Exchange connectivity check" },
1118
+ health: { usage: "perp agent ping", description: "Exchange connectivity check" },
1172
1119
  settings: {
1173
1120
  description: "CLI settings",
1174
1121
  subcommands: {
@@ -0,0 +1,12 @@
1
+ import type { EvmSigner } from "./interface.js";
2
+ export declare class LocalEvmSigner implements EvmSigner {
3
+ private _wallet;
4
+ private constructor();
5
+ static create(privateKey: string): Promise<LocalEvmSigner>;
6
+ getAddress(): string;
7
+ signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
8
+ name: string;
9
+ type: string;
10
+ }>>, value: Record<string, unknown>): Promise<string>;
11
+ signMessage(message: string | Uint8Array): Promise<string>;
12
+ }
@@ -0,0 +1,19 @@
1
+ export class LocalEvmSigner {
2
+ _wallet;
3
+ constructor() { }
4
+ static async create(privateKey) {
5
+ const { ethers } = await import("ethers");
6
+ const signer = new LocalEvmSigner();
7
+ signer._wallet = new ethers.Wallet(privateKey);
8
+ return signer;
9
+ }
10
+ getAddress() {
11
+ return this._wallet.address;
12
+ }
13
+ async signTypedData(domain, types, value) {
14
+ return this._wallet.signTypedData(domain, types, value);
15
+ }
16
+ async signMessage(message) {
17
+ return this._wallet.signMessage(message);
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ export type { EvmSigner, SolanaSigner } from "./interface.js";
2
+ export { LocalEvmSigner } from "./evm-local.js";
3
+ export { LocalSolanaSigner } from "./solana-local.js";
@@ -0,0 +1,2 @@
1
+ export { LocalEvmSigner } from "./evm-local.js";
2
+ export { LocalSolanaSigner } from "./solana-local.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Signer interfaces for exchange adapters.
3
+ *
4
+ * Separates signing logic from adapters so it can be injected
5
+ * (e.g., HSM, hardware wallet, delegated signing).
6
+ */
7
+ /** EVM signer (Hyperliquid + Lighter L1) */
8
+ export interface EvmSigner {
9
+ getAddress(): string;
10
+ /** EIP-712 typed data signing — returns flat hex signature (0x...) */
11
+ signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
12
+ name: string;
13
+ type: string;
14
+ }>>, value: Record<string, unknown>): Promise<string>;
15
+ /** EIP-191 personal message signing — returns hex signature */
16
+ signMessage(message: string | Uint8Array): Promise<string>;
17
+ }
18
+ /** Solana signer (Pacifica) */
19
+ export interface SolanaSigner {
20
+ getPublicKeyBase58(): string;
21
+ /** Ed25519 detached signature */
22
+ signMessage(message: Uint8Array): Promise<Uint8Array>;
23
+ /** Sign a Solana transaction (full sign) */
24
+ signTransaction(tx: {
25
+ sign(keypair: unknown): void;
26
+ }): void;
27
+ /** Partially sign a Solana transaction */
28
+ partialSignTransaction(tx: {
29
+ partialSign(keypair: unknown): void;
30
+ }): void;
31
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Signer interfaces for exchange adapters.
3
+ *
4
+ * Separates signing logic from adapters so it can be injected
5
+ * (e.g., HSM, hardware wallet, delegated signing).
6
+ */
7
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { SolanaSigner } from "./interface.js";
2
+ export declare class LocalSolanaSigner implements SolanaSigner {
3
+ private _keypair;
4
+ constructor(keypair: import("@solana/web3.js").Keypair);
5
+ getPublicKeyBase58(): string;
6
+ signMessage(message: Uint8Array): Promise<Uint8Array>;
7
+ signTransaction(tx: {
8
+ sign(keypair: unknown): void;
9
+ }): void;
10
+ partialSignTransaction(tx: {
11
+ partialSign(keypair: unknown): void;
12
+ }): void;
13
+ }
@@ -0,0 +1,19 @@
1
+ export class LocalSolanaSigner {
2
+ _keypair;
3
+ constructor(keypair) {
4
+ this._keypair = keypair;
5
+ }
6
+ getPublicKeyBase58() {
7
+ return this._keypair.publicKey.toBase58();
8
+ }
9
+ async signMessage(message) {
10
+ const nacl = await import("tweetnacl");
11
+ return nacl.default.sign.detached(message, this._keypair.secretKey);
12
+ }
13
+ signTransaction(tx) {
14
+ tx.sign(this._keypair);
15
+ }
16
+ partialSignTransaction(tx) {
17
+ tx.partialSign(this._keypair);
18
+ }
19
+ }
@@ -30,15 +30,26 @@ function inferTickSize(levels) {
30
30
  return decimals > 0 ? Math.pow(10, -decimals) : 1;
31
31
  }
32
32
  // Use minimum difference between adjacent price levels
33
+ // Determine max decimal places from the orderbook to round away float noise
34
+ let maxDecimals = 0;
35
+ for (let i = 0; i < Math.min(levels.length, 5); i++) {
36
+ const p = levels[i][0];
37
+ const d = p.includes(".") ? p.split(".")[1].length : 0;
38
+ if (d > maxDecimals)
39
+ maxDecimals = d;
40
+ }
33
41
  let minDiff = Infinity;
34
42
  for (let i = 1; i < Math.min(levels.length, 5); i++) {
35
43
  const diff = Math.abs(Number(levels[i][0]) - Number(levels[i - 1][0]));
36
44
  if (diff > 0 && diff < minDiff)
37
45
  minDiff = diff;
38
46
  }
39
- return minDiff === Infinity
40
- ? Math.pow(10, -(levels[0][0].includes(".") ? levels[0][0].split(".")[1].length : 0))
41
- : minDiff;
47
+ if (minDiff === Infinity) {
48
+ return Math.pow(10, -maxDecimals);
49
+ }
50
+ // Round to orderbook precision to eliminate floating-point noise
51
+ // e.g., 0.10000000000000142 → 0.1
52
+ return Number(minDiff.toFixed(maxDecimals));
42
53
  }
43
54
  /**
44
55
  * Get price precision (decimal places) from a price string.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Orderbook-aware split order execution.
3
+ *
4
+ * Unlike TWAP (time-based splitting), this strategy reads the orderbook
5
+ * before each slice and calculates optimal slice sizes based on available
6
+ * depth. Each slice is placed as an IOC limit order at the worst acceptable
7
+ * price within the slippage tolerance, ensuring no sweep beyond the target.
8
+ *
9
+ * Flow per slice:
10
+ * 1. Fetch fresh orderbook
11
+ * 2. Walk levels via computeExecutableSize to find fillable amount
12
+ * 3. Place IOC limit at best price + slippage tolerance
13
+ * 4. Wait for book to refresh, then repeat
14
+ */
15
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
16
+ export interface SplitOrderParams {
17
+ symbol: string;
18
+ side: "buy" | "sell";
19
+ totalSizeUsd: number;
20
+ maxSlippagePct?: number;
21
+ maxSlices?: number;
22
+ delayMs?: number;
23
+ minSliceUsd?: number;
24
+ }
25
+ export interface SplitSlice {
26
+ index: number;
27
+ size: string;
28
+ price: string;
29
+ notionalUsd: number;
30
+ slippagePct: number;
31
+ method: "limit_ioc" | "market";
32
+ status: "filled" | "partial" | "failed";
33
+ }
34
+ export interface SplitOrderResult {
35
+ symbol: string;
36
+ side: "buy" | "sell";
37
+ requestedUsd: number;
38
+ filledUsd: number;
39
+ filledSize: number;
40
+ avgPrice: number;
41
+ totalSlippagePct: number;
42
+ slices: SplitSlice[];
43
+ status: "complete" | "partial" | "failed";
44
+ runtime: number;
45
+ }
46
+ /**
47
+ * Execute a split order: orderbook-aware slicing with IOC limits.
48
+ */
49
+ export declare function runSplitOrder(adapter: ExchangeAdapter, params: SplitOrderParams, log?: (msg: string) => void): Promise<SplitOrderResult>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Orderbook-aware split order execution.
3
+ *
4
+ * Unlike TWAP (time-based splitting), this strategy reads the orderbook
5
+ * before each slice and calculates optimal slice sizes based on available
6
+ * depth. Each slice is placed as an IOC limit order at the worst acceptable
7
+ * price within the slippage tolerance, ensuring no sweep beyond the target.
8
+ *
9
+ * Flow per slice:
10
+ * 1. Fetch fresh orderbook
11
+ * 2. Walk levels via computeExecutableSize to find fillable amount
12
+ * 3. Place IOC limit at best price + slippage tolerance
13
+ * 4. Wait for book to refresh, then repeat
14
+ */
15
+ import { computeExecutableSize } from "../liquidity.js";
16
+ import { logExecution } from "../execution-log.js";
17
+ /**
18
+ * Infer price precision (decimal places) from an orderbook price string.
19
+ */
20
+ function pricePrecision(priceStr) {
21
+ return priceStr.includes(".") ? priceStr.split(".")[1].length : 0;
22
+ }
23
+ /**
24
+ * Execute a split order: orderbook-aware slicing with IOC limits.
25
+ */
26
+ export async function runSplitOrder(adapter, params, log = console.log) {
27
+ const { symbol, side, totalSizeUsd, maxSlippagePct = 0.3, maxSlices = 10, delayMs = 1000, minSliceUsd = 100, } = params;
28
+ const startedAt = Date.now();
29
+ const slices = [];
30
+ let remainingUsd = totalSizeUsd;
31
+ let totalFilledSize = 0;
32
+ let totalFilledNotional = 0;
33
+ // Capture initial best price for overall slippage calculation
34
+ let initialBestPrice = 0;
35
+ try {
36
+ const initialBook = await adapter.getOrderbook(symbol);
37
+ const initialLevels = side === "buy" ? initialBook.asks : initialBook.bids;
38
+ if (initialLevels.length > 0) {
39
+ initialBestPrice = Number(initialLevels[0][0]);
40
+ }
41
+ }
42
+ catch {
43
+ // Will be recaptured on first slice
44
+ }
45
+ log(`[SPLIT] ${side.toUpperCase()} $${totalSizeUsd.toFixed(0)} of ${symbol} | max slippage: ${maxSlippagePct}% | max slices: ${maxSlices}`);
46
+ for (let i = 0; i < maxSlices && remainingUsd >= minSliceUsd; i++) {
47
+ // 1. Fetch fresh orderbook
48
+ const book = await adapter.getOrderbook(symbol);
49
+ const levels = side === "buy" ? book.asks : book.bids;
50
+ if (levels.length === 0) {
51
+ log(`[SPLIT] No ${side === "buy" ? "asks" : "bids"} in orderbook, stopping`);
52
+ break;
53
+ }
54
+ // Capture initial best if we missed it
55
+ if (initialBestPrice === 0) {
56
+ initialBestPrice = Number(levels[0][0]);
57
+ }
58
+ // 2. Calculate how much we can execute within slippage on this depth
59
+ const check = computeExecutableSize(levels, remainingUsd, maxSlippagePct);
60
+ if (check.maxSize <= 0) {
61
+ log(`[SPLIT] Insufficient liquidity within ${maxSlippagePct}% slippage, stopping`);
62
+ break;
63
+ }
64
+ // Slice size: what we can fill within slippage, capped by remaining
65
+ const sliceNotional = Math.min(check.maxSize * check.avgFillPrice, remainingUsd);
66
+ const sliceSize = sliceNotional / check.avgFillPrice;
67
+ // 3. Calculate IOC limit price (worst acceptable within slippage)
68
+ const bestPrice = Number(levels[0][0]);
69
+ const limitPrice = side === "buy"
70
+ ? bestPrice * (1 + maxSlippagePct / 100)
71
+ : bestPrice * (1 - maxSlippagePct / 100);
72
+ const decimals = pricePrecision(levels[0][0]);
73
+ const formattedPrice = limitPrice.toFixed(decimals);
74
+ const formattedSize = sliceSize.toFixed(6);
75
+ log(`[SPLIT] Slice ${i + 1}: ${formattedSize} @ limit $${formattedPrice} (~$${sliceNotional.toFixed(0)}) | depth: $${check.depthUsd.toFixed(0)}`);
76
+ // 4. Execute IOC limit order
77
+ let slice;
78
+ try {
79
+ await adapter.limitOrder(symbol, side, formattedPrice, formattedSize, { tif: "IOC" });
80
+ slice = {
81
+ index: i,
82
+ size: formattedSize,
83
+ price: formattedPrice,
84
+ notionalUsd: sliceNotional,
85
+ slippagePct: check.slippagePct,
86
+ method: "limit_ioc",
87
+ status: "filled",
88
+ };
89
+ totalFilledSize += sliceSize;
90
+ totalFilledNotional += sliceNotional;
91
+ remainingUsd -= sliceNotional;
92
+ logExecution({
93
+ type: "limit_order", exchange: adapter.name, symbol,
94
+ side, size: formattedSize, price: formattedPrice,
95
+ notional: sliceNotional, status: "success", dryRun: false,
96
+ meta: { action: "split-order", slice: i + 1, totalSlices: maxSlices },
97
+ });
98
+ }
99
+ catch (err) {
100
+ log(`[SPLIT] Slice ${i + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
101
+ slice = {
102
+ index: i, size: formattedSize, price: formattedPrice,
103
+ notionalUsd: 0, slippagePct: 0, method: "limit_ioc", status: "failed",
104
+ };
105
+ logExecution({
106
+ type: "limit_order", exchange: adapter.name, symbol,
107
+ side, size: formattedSize, price: formattedPrice,
108
+ notional: 0, status: "failed", dryRun: false,
109
+ error: err instanceof Error ? err.message : String(err),
110
+ meta: { action: "split-order", slice: i + 1, totalSlices: maxSlices },
111
+ });
112
+ }
113
+ slices.push(slice);
114
+ // 5. Delay before next slice (let the book refresh)
115
+ if (remainingUsd >= minSliceUsd && i < maxSlices - 1) {
116
+ await new Promise(r => setTimeout(r, delayMs));
117
+ }
118
+ }
119
+ const avgPrice = totalFilledSize > 0 ? totalFilledNotional / totalFilledSize : 0;
120
+ const totalSlippage = initialBestPrice > 0
121
+ ? Math.abs((avgPrice - initialBestPrice) / initialBestPrice) * 100
122
+ : 0;
123
+ const status = totalFilledNotional >= totalSizeUsd * 0.95 ? "complete"
124
+ : totalFilledNotional > 0 ? "partial"
125
+ : "failed";
126
+ log(`[SPLIT] Done: ${slices.length} slices | filled $${totalFilledNotional.toFixed(0)}/$${totalSizeUsd.toFixed(0)} | avg price: $${avgPrice.toFixed(2)} | slippage: ${totalSlippage.toFixed(3)}%`);
127
+ return {
128
+ symbol, side,
129
+ requestedUsd: totalSizeUsd,
130
+ filledUsd: totalFilledNotional,
131
+ filledSize: totalFilledSize,
132
+ avgPrice,
133
+ totalSlippagePct: totalSlippage,
134
+ slices,
135
+ status,
136
+ runtime: (Date.now() - startedAt) / 1000,
137
+ };
138
+ }
package/dist/utils.js CHANGED
@@ -107,7 +107,13 @@ export function printJson(data) {
107
107
  const fields = process.argv[fieldsArg + 1].split(",").map(f => f.trim());
108
108
  const envelope = output;
109
109
  if (envelope.ok && envelope.data && typeof envelope.data === "object") {
110
- envelope.data = pickFields(envelope.data, fields);
110
+ // If data is an array, apply pickFields to each element
111
+ if (Array.isArray(envelope.data)) {
112
+ envelope.data = envelope.data.map((item) => pickFields(item, fields));
113
+ }
114
+ else {
115
+ envelope.data = pickFields(envelope.data, fields);
116
+ }
111
117
  }
112
118
  output = envelope;
113
119
  }