perp-cli 0.6.4 → 0.7.1

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 (177) hide show
  1. package/README.md +7 -2
  2. package/dist/api/public/hyperliquid.d.ts +2 -2
  3. package/dist/api/public/hyperliquid.js +0 -10
  4. package/dist/api/public/lighter.d.ts +2 -1
  5. package/dist/api/public/pacifica.d.ts +2 -1
  6. package/dist/arb/index.d.ts +7 -4
  7. package/dist/arb/index.js +5 -4
  8. package/dist/arb/sizing.d.ts +1 -1
  9. package/dist/arb/state.d.ts +1 -1
  10. package/dist/arb/state.js +4 -3
  11. package/dist/arb/utils.d.ts +0 -10
  12. package/dist/arb/utils.js +0 -42
  13. package/dist/bot/apex/guard.d.ts +26 -0
  14. package/dist/bot/apex/guard.js +86 -0
  15. package/dist/bot/apex/index.d.ts +6 -0
  16. package/dist/bot/apex/index.js +4 -0
  17. package/dist/bot/apex/pulse.d.ts +22 -0
  18. package/dist/bot/apex/pulse.js +107 -0
  19. package/dist/bot/apex/radar.d.ts +17 -0
  20. package/dist/bot/apex/radar.js +150 -0
  21. package/dist/bot/conditions.d.ts +1 -1
  22. package/dist/bot/config.d.ts +5 -1
  23. package/dist/bot/config.js +1 -1
  24. package/dist/bot/engine.d.ts +23 -2
  25. package/dist/bot/engine.js +296 -351
  26. package/dist/bot/index.d.ts +16 -0
  27. package/dist/bot/index.js +10 -0
  28. package/dist/bot/indicators.d.ts +29 -0
  29. package/dist/bot/indicators.js +144 -0
  30. package/dist/bot/reflect.d.ts +34 -0
  31. package/dist/bot/reflect.js +155 -0
  32. package/dist/bot/strategies/aggressive-taker.d.ts +36 -0
  33. package/dist/bot/strategies/aggressive-taker.js +125 -0
  34. package/dist/bot/strategies/apex-strategy.d.ts +36 -0
  35. package/dist/bot/strategies/apex-strategy.js +185 -0
  36. package/dist/bot/strategies/avellaneda-mm.d.ts +27 -0
  37. package/dist/bot/strategies/avellaneda-mm.js +111 -0
  38. package/dist/bot/strategies/basis-arb.d.ts +27 -0
  39. package/dist/bot/strategies/basis-arb.js +97 -0
  40. package/dist/bot/strategies/claude-agent.d.ts +31 -0
  41. package/dist/bot/strategies/claude-agent.js +155 -0
  42. package/dist/bot/strategies/dca-strategy.d.ts +25 -0
  43. package/dist/bot/strategies/dca-strategy.js +59 -0
  44. package/dist/bot/strategies/engine-mm.d.ts +29 -0
  45. package/dist/bot/strategies/engine-mm.js +141 -0
  46. package/dist/bot/strategies/funding-arb-strategy.d.ts +32 -0
  47. package/dist/bot/strategies/funding-arb-strategy.js +168 -0
  48. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  49. package/dist/bot/strategies/funding-arb-v2.js +222 -0
  50. package/dist/bot/strategies/funding-auto.d.ts +1 -0
  51. package/dist/bot/strategies/funding-auto.js +572 -0
  52. package/dist/bot/strategies/grid-mm.d.ts +25 -0
  53. package/dist/bot/strategies/grid-mm.js +99 -0
  54. package/dist/bot/strategies/grid-strategy.d.ts +39 -0
  55. package/dist/bot/strategies/grid-strategy.js +176 -0
  56. package/dist/bot/strategies/hedge-agent.d.ts +25 -0
  57. package/dist/bot/strategies/hedge-agent.js +79 -0
  58. package/dist/bot/strategies/liquidation-mm.d.ts +25 -0
  59. package/dist/bot/strategies/liquidation-mm.js +108 -0
  60. package/dist/bot/strategies/mean-reversion.d.ts +35 -0
  61. package/dist/bot/strategies/mean-reversion.js +118 -0
  62. package/dist/bot/strategies/momentum-breakout.d.ts +36 -0
  63. package/dist/bot/strategies/momentum-breakout.js +159 -0
  64. package/dist/bot/strategies/regime-mm.d.ts +25 -0
  65. package/dist/bot/strategies/regime-mm.js +111 -0
  66. package/dist/bot/strategies/rfq-agent.d.ts +25 -0
  67. package/dist/bot/strategies/rfq-agent.js +71 -0
  68. package/dist/bot/strategies/simple-mm.d.ts +25 -0
  69. package/dist/bot/strategies/simple-mm.js +76 -0
  70. package/dist/bot/strategy-registry.d.ts +9 -0
  71. package/dist/bot/strategy-registry.js +14 -0
  72. package/dist/bot/strategy-types.d.ts +64 -0
  73. package/dist/bot/strategy-types.js +5 -0
  74. package/dist/bot/trade-journal.d.ts +27 -0
  75. package/dist/bot/trade-journal.js +53 -0
  76. package/dist/bot/tui/BotDashboard.d.ts +49 -0
  77. package/dist/bot/tui/BotDashboard.js +93 -0
  78. package/dist/bot/tui/index.d.ts +6 -0
  79. package/dist/bot/tui/index.js +8 -0
  80. package/dist/bridge-engine.d.ts +0 -1
  81. package/dist/bridge-engine.js +0 -1
  82. package/dist/cache.d.ts +0 -1
  83. package/dist/cache.js +0 -1
  84. package/dist/commands/account.d.ts +1 -1
  85. package/dist/commands/account.js +94 -91
  86. package/dist/commands/agent.d.ts +1 -1
  87. package/dist/commands/arb/index.d.ts +1 -1
  88. package/dist/commands/arb/index.js +1 -1
  89. package/dist/commands/arb-auto.d.ts +1 -1
  90. package/dist/commands/arb-auto.js +1 -1
  91. package/dist/commands/arb.d.ts +1 -1
  92. package/dist/commands/bot.d.ts +1 -1
  93. package/dist/commands/bot.js +198 -14
  94. package/dist/commands/dashboard.d.ts +1 -1
  95. package/dist/commands/dashboard.js +1 -1
  96. package/dist/commands/funds.d.ts +1 -1
  97. package/dist/commands/funds.js +33 -20
  98. package/dist/commands/history.d.ts +1 -1
  99. package/dist/commands/history.js +5 -4
  100. package/dist/commands/manage.d.ts +2 -3
  101. package/dist/commands/manage.js +11 -18
  102. package/dist/commands/market.d.ts +1 -1
  103. package/dist/commands/market.js +7 -7
  104. package/dist/commands/plan.d.ts +1 -1
  105. package/dist/commands/rebalance.d.ts +1 -1
  106. package/dist/commands/rebalance.js +8 -9
  107. package/dist/commands/risk.d.ts +1 -1
  108. package/dist/commands/settings.d.ts +1 -1
  109. package/dist/commands/settings.js +3 -1
  110. package/dist/commands/trade.d.ts +1 -1
  111. package/dist/commands/trade.js +39 -52
  112. package/dist/commands/wallet.d.ts +0 -2
  113. package/dist/commands/wallet.js +0 -5
  114. package/dist/constants.d.ts +0 -3
  115. package/dist/constants.js +0 -4
  116. package/dist/cross-chain-margin.d.ts +1 -1
  117. package/dist/dashboard/index.d.ts +4 -0
  118. package/dist/dashboard/index.js +3 -0
  119. package/dist/dashboard/server.d.ts +1 -1
  120. package/dist/dashboard/server.js +1 -1
  121. package/dist/dashboard/ws-feeds.d.ts +1 -1
  122. package/dist/event-stream.d.ts +1 -1
  123. package/dist/exchanges/aster.d.ts +58 -0
  124. package/dist/exchanges/aster.js +409 -0
  125. package/dist/exchanges/capabilities.d.ts +74 -0
  126. package/dist/exchanges/capabilities.js +44 -0
  127. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  128. package/dist/exchanges/hyperliquid-spot.js +20 -12
  129. package/dist/exchanges/hyperliquid.d.ts +8 -2
  130. package/dist/exchanges/hyperliquid.js +21 -6
  131. package/dist/exchanges/index.d.ts +11 -0
  132. package/dist/exchanges/index.js +10 -0
  133. package/dist/exchanges/interface.d.ts +3 -0
  134. package/dist/exchanges/lighter-spot.d.ts +13 -0
  135. package/dist/exchanges/lighter-spot.js +98 -26
  136. package/dist/exchanges/lighter.d.ts +9 -3
  137. package/dist/exchanges/lighter.js +18 -8
  138. package/dist/exchanges/nado.d.ts +52 -0
  139. package/dist/exchanges/nado.js +223 -0
  140. package/dist/exchanges/pacifica.d.ts +7 -1
  141. package/dist/exchanges/pacifica.js +17 -2
  142. package/dist/exchanges/registry.d.ts +13 -0
  143. package/dist/exchanges/registry.js +37 -0
  144. package/dist/exchanges/spot-interface.d.ts +9 -0
  145. package/dist/funding/rates.js +1 -1
  146. package/dist/index.js +2 -6
  147. package/dist/jobs.js +1 -1
  148. package/dist/liquidity.d.ts +1 -2
  149. package/dist/liquidity.js +12 -5
  150. package/dist/mcp-server.js +4 -2
  151. package/dist/pacifica/constants.d.ts +0 -3
  152. package/dist/pacifica/constants.js +0 -4
  153. package/dist/pacifica/types/account.d.ts +0 -8
  154. package/dist/pacifica/types/lake.d.ts +0 -6
  155. package/dist/pacifica/types/position.d.ts +0 -12
  156. package/dist/pacifica/types/ws.d.ts +0 -24
  157. package/dist/pacifica/types/ws.js +1 -41
  158. package/dist/plan-executor.d.ts +1 -1
  159. package/dist/rebalance.d.ts +1 -7
  160. package/dist/rebalance.js +0 -6
  161. package/dist/risk.d.ts +1 -1
  162. package/dist/risk.js +3 -1
  163. package/dist/smart-order.d.ts +1 -1
  164. package/dist/strategies/dca.d.ts +1 -1
  165. package/dist/strategies/funding-arb.d.ts +1 -1
  166. package/dist/strategies/funding-arb.js +193 -181
  167. package/dist/strategies/grid.d.ts +1 -1
  168. package/dist/strategies/grid.js +1 -1
  169. package/dist/strategies/index.d.ts +12 -0
  170. package/dist/strategies/index.js +7 -0
  171. package/dist/strategies/split-order.d.ts +1 -1
  172. package/dist/strategies/trailing-stop.d.ts +1 -1
  173. package/dist/strategies/twap.d.ts +1 -1
  174. package/dist/trade-validator.d.ts +1 -1
  175. package/package.json +7 -1
  176. package/skills/perp-cli/SKILL.md +20 -0
  177. package/skills/perp-cli/references/commands.md +22 -8
@@ -1,8 +1,7 @@
1
- import { PacificaAdapter } from "../exchanges/pacifica.js";
2
- import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
1
  import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMatch, withJsonErrors } from "../utils.js";
4
2
  import chalk from "chalk";
5
3
  import { assessRisk } from "../risk.js";
4
+ import { hasPacificaSdk, isDexCapable } from "../exchanges/capabilities.js";
6
5
  const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
7
6
  async function fetchExchangeSnapshot(name, getAdapter) {
8
7
  try {
@@ -96,7 +95,7 @@ function buildSummary(snapshots) {
96
95
  };
97
96
  }
98
97
  function pac(adapter) {
99
- if (!(adapter instanceof PacificaAdapter))
98
+ if (!hasPacificaSdk(adapter))
100
99
  throw new Error("Market settings are only available on Pacifica.");
101
100
  return adapter;
102
101
  }
@@ -112,6 +111,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
112
111
  try {
113
112
  if (name === "hyperliquid") {
114
113
  const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
114
+ const { HyperliquidAdapter } = await import("../exchanges/hyperliquid.js");
115
115
  const hlSpot = new HyperliquidSpotAdapter(adapter);
116
116
  await hlSpot.init();
117
117
  const raw = await hlSpot.getSpotBalances();
@@ -149,7 +149,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
149
149
  catch { /* spot not available */ }
150
150
  // HL unified account: perp equity already includes spot USDC balance.
151
151
  // Detect by checking if adapter is HL without a dex override.
152
- const isUnified = name === "hyperliquid" && !adapter.dex;
152
+ const isUnified = name === "hyperliquid" && (!isDexCapable(adapter) || !adapter.dex);
153
153
  const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
154
154
  const recent = fundingPayments.filter(f => f.time >= cutoff24h);
155
155
  const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
@@ -368,7 +368,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
368
368
  ]);
369
369
  }
370
370
  // --hip3: fetch HIP-3 dex positions for HL
371
- if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
371
+ if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
372
372
  const hip3 = await fetchHip3Data(adapter, a => a.getPositions());
373
373
  for (const { dex, items } of hip3) {
374
374
  for (const p of items) {
@@ -397,7 +397,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
397
397
  try {
398
398
  const adapter = await getAdapterForExchange(ex);
399
399
  grouped[ex] = await adapter.getPositions();
400
- if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
400
+ if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
401
401
  const hip3 = await fetchHip3Data(adapter, a => a.getPositions());
402
402
  for (const { dex, items } of hip3)
403
403
  grouped[`hip3:${dex}`] = items;
@@ -423,7 +423,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
423
423
  const positions = await adapter.getPositions();
424
424
  // --hip3: append HIP-3 dex positions
425
425
  const hip3Positions = [];
426
- if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
426
+ if (opts.hip3 && isDexCapable(adapter)) {
427
427
  hip3Positions.push(...await fetchHip3Data(adapter, a => a.getPositions()));
428
428
  }
429
429
  if (isJson()) {
@@ -494,7 +494,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
494
494
  ]);
495
495
  }
496
496
  // --hip3: fetch HIP-3 dex orders for HL
497
- if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
497
+ if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
498
498
  const hip3 = await fetchHip3Data(adapter, a => a.getOpenOrders());
499
499
  for (const { dex, items } of hip3) {
500
500
  for (const o of items) {
@@ -523,7 +523,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
523
523
  try {
524
524
  const adapter = await getAdapterForExchange(ex);
525
525
  grouped[ex] = await adapter.getOpenOrders();
526
- if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
526
+ if (opts.hip3 && ex === "hyperliquid" && isDexCapable(adapter)) {
527
527
  const hip3 = await fetchHip3Data(adapter, a => a.getOpenOrders());
528
528
  for (const { dex, items } of hip3)
529
529
  grouped[`hip3:${dex}`] = items;
@@ -549,7 +549,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
549
549
  const orders = await adapter.getOpenOrders();
550
550
  // --hip3: append HIP-3 dex orders
551
551
  const hip3Orders = [];
552
- if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
552
+ if (opts.hip3 && isDexCapable(adapter)) {
553
553
  hip3Orders.push(...await fetchHip3Data(adapter, a => a.getOpenOrders()));
554
554
  }
555
555
  if (isJson()) {
@@ -623,8 +623,9 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
623
623
  .action(async () => {
624
624
  const adapter = await getAdapter();
625
625
  // Pacifica: use dedicated SDK endpoint
626
- if (adapter instanceof PacificaAdapter) {
627
- const settings = await adapter.sdk.getAccountSettings(adapter.publicKey);
626
+ if (hasPacificaSdk(adapter)) {
627
+ const sdk = adapter.sdk;
628
+ const settings = await sdk.getAccountSettings(adapter.publicKey);
628
629
  if (isJson())
629
630
  return printJson(jsonOk(settings));
630
631
  if (!Array.isArray(settings) || settings.length === 0) {
@@ -902,9 +903,19 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
902
903
  .command("twap-orders")
903
904
  .description("Active TWAP orders")
904
905
  .action(async () => {
905
- const adapter = await getAdapter();
906
- const p = pac(adapter);
907
- const orders = await p.sdk.getTWAPOrders(p.publicKey);
906
+ let p;
907
+ try {
908
+ const adapter = await getAdapter();
909
+ p = pac(adapter);
910
+ }
911
+ catch (err) {
912
+ if (isJson())
913
+ return printJson(jsonError("EXCHANGE_ERROR", err instanceof Error ? err.message : String(err)));
914
+ console.error(chalk.red(`\n ${err instanceof Error ? err.message : String(err)}\n`));
915
+ return;
916
+ }
917
+ const sdk = p.sdk;
918
+ const orders = await sdk.getTWAPOrders(p.publicKey);
908
919
  if (isJson())
909
920
  return printJson(jsonOk(orders));
910
921
  if (!orders || orders.length === 0) {
@@ -932,7 +943,73 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
932
943
  const exchanges = opts.exchange
933
944
  ? opts.exchange.split(",").map(e => e.trim())
934
945
  : [...EXCHANGES];
935
- const snapshots = await Promise.all(exchanges.map(ex => fetchExchangeSnapshot(ex, getAdapterForExchange)));
946
+ // Fetch snapshots + funding + spot ALL in parallel
947
+ const fundingByExSym = new Map();
948
+ const spotRows = [];
949
+ let totalSpotValue = 0;
950
+ const [snapshots] = await Promise.all([
951
+ // Phase 1: balance + positions + orders
952
+ Promise.all(exchanges.map(ex => fetchExchangeSnapshot(ex, getAdapterForExchange))),
953
+ // Phase 2: funding payments (parallel with Phase 1)
954
+ Promise.allSettled(exchanges.map(async (exName) => {
955
+ try {
956
+ const adapter = await getAdapterForExchange(exName);
957
+ const payments = await adapter.getFundingPayments(200);
958
+ for (const p of payments) {
959
+ const sym = p.symbol.replace("-PERP", "").toUpperCase();
960
+ const key = `${exName}:${sym}`;
961
+ fundingByExSym.set(key, (fundingByExSym.get(key) ?? 0) + Number(p.payment));
962
+ }
963
+ }
964
+ catch { /* skip */ }
965
+ })),
966
+ // Phase 3: spot balances (parallel with Phase 1+2)
967
+ Promise.allSettled(exchanges.map(async (exName) => {
968
+ try {
969
+ const adapter = await getAdapterForExchange(exName);
970
+ if (exName === "hyperliquid") {
971
+ const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
972
+ const { HyperliquidAdapter } = await import("../exchanges/hyperliquid.js");
973
+ const hlSpot = new HyperliquidSpotAdapter(adapter);
974
+ await hlSpot.init();
975
+ const bals = await hlSpot.getSpotBalances();
976
+ const markets = await hlSpot.getSpotMarkets();
977
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
978
+ for (const b of bals) {
979
+ const base = b.token.replace(/-SPOT$/i, "").toUpperCase();
980
+ if (base === "USDC" || Number(b.total) <= 0)
981
+ continue;
982
+ const price = priceMap.get(base) ?? 0;
983
+ const value = price * Number(b.total);
984
+ if (value < 1)
985
+ continue;
986
+ totalSpotValue += value;
987
+ spotRows.push([chalk.white.bold(base), chalk.gray("hyperliquid"), b.total, `$${formatUsd(price)}`, `$${formatUsd(value)}`]);
988
+ }
989
+ }
990
+ else if (exName === "lighter") {
991
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
992
+ const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
993
+ const ltSpot = new LighterSpotAdapter(adapter);
994
+ await ltSpot.init();
995
+ const bals = await ltSpot.getSpotBalances();
996
+ const markets = await ltSpot.getSpotMarkets();
997
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
998
+ for (const b of bals) {
999
+ if (b.token === "USDC" || b.token === "USDC_SPOT" || Number(b.total) <= 0)
1000
+ continue;
1001
+ const price = priceMap.get(b.token.toUpperCase()) ?? 0;
1002
+ const value = price * Number(b.total);
1003
+ if (value < 1)
1004
+ continue;
1005
+ totalSpotValue += value;
1006
+ spotRows.push([chalk.white.bold(b.token), chalk.gray("lighter"), b.total, `$${formatUsd(price)}`, `$${formatUsd(value)}`]);
1007
+ }
1008
+ }
1009
+ }
1010
+ catch { /* spot not available */ }
1011
+ })),
1012
+ ]);
936
1013
  const summary = buildSummary(snapshots);
937
1014
  if (isJson()) {
938
1015
  return printJson(jsonOk(summary));
@@ -955,7 +1032,6 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
955
1032
  chalk.green("connected"),
956
1033
  ];
957
1034
  });
958
- // Totals row
959
1035
  balRows.push([
960
1036
  chalk.cyan.bold("TOTAL"),
961
1037
  chalk.cyan.bold(`$${formatUsd(summary.totalEquity)}`),
@@ -965,21 +1041,6 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
965
1041
  "",
966
1042
  ]);
967
1043
  console.log(makeTable(["Exchange", "Equity", "Available", "Margin Used", "uPnL", "Status"], balRows));
968
- // ── Positions + Funding ──
969
- // Fetch funding per exchange in parallel
970
- const fundingByExSym = new Map();
971
- await Promise.allSettled(exchanges.map(async (exName) => {
972
- try {
973
- const adapter = await getAdapterForExchange(exName);
974
- const payments = await adapter.getFundingPayments(200);
975
- for (const p of payments) {
976
- const sym = p.symbol.replace("-PERP", "").toUpperCase();
977
- const key = `${exName}:${sym}`;
978
- fundingByExSym.set(key, (fundingByExSym.get(key) ?? 0) + Number(p.payment));
979
- }
980
- }
981
- catch { /* skip */ }
982
- }));
983
1044
  if (summary.positions.length > 0) {
984
1045
  console.log(chalk.white.bold("\n Open Positions"));
985
1046
  const posRows = summary.positions.map(p => {
@@ -1005,65 +1066,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
1005
1066
  else {
1006
1067
  console.log(chalk.gray("\n No open positions.\n"));
1007
1068
  }
1008
- // ── Spot Holdings ──
1009
- const spotRows = [];
1010
- let totalSpotValue = 0;
1011
- for (const exName of exchanges) {
1012
- try {
1013
- const adapter = await getAdapterForExchange(exName);
1014
- if (exName === "hyperliquid") {
1015
- const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
1016
- const hlSpot = new HyperliquidSpotAdapter(adapter);
1017
- await hlSpot.init();
1018
- const bals = await hlSpot.getSpotBalances();
1019
- const markets = await hlSpot.getSpotMarkets();
1020
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
1021
- for (const b of bals) {
1022
- const base = b.token.replace(/-SPOT$/i, "").toUpperCase();
1023
- if (base === "USDC" || Number(b.total) <= 0)
1024
- continue;
1025
- const price = priceMap.get(base) ?? 0;
1026
- const value = price * Number(b.total);
1027
- if (value < 1)
1028
- continue; // skip dust
1029
- totalSpotValue += value;
1030
- spotRows.push([
1031
- chalk.white.bold(base),
1032
- chalk.gray("hyperliquid"),
1033
- b.total,
1034
- `$${formatUsd(price)}`,
1035
- `$${formatUsd(value)}`,
1036
- ]);
1037
- }
1038
- }
1039
- else if (exName === "lighter") {
1040
- const { LighterAdapter } = await import("../exchanges/lighter.js");
1041
- const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
1042
- const ltSpot = new LighterSpotAdapter(adapter);
1043
- await ltSpot.init();
1044
- const bals = await ltSpot.getSpotBalances();
1045
- const markets = await ltSpot.getSpotMarkets();
1046
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
1047
- for (const b of bals) {
1048
- if (b.token === "USDC" || b.token === "USDC_SPOT" || Number(b.total) <= 0)
1049
- continue;
1050
- const price = priceMap.get(b.token.toUpperCase()) ?? 0;
1051
- const value = price * Number(b.total);
1052
- if (value < 1)
1053
- continue;
1054
- totalSpotValue += value;
1055
- spotRows.push([
1056
- chalk.white.bold(b.token),
1057
- chalk.gray("lighter"),
1058
- b.total,
1059
- `$${formatUsd(price)}`,
1060
- `$${formatUsd(value)}`,
1061
- ]);
1062
- }
1063
- }
1064
- }
1065
- catch { /* spot not available */ }
1066
- }
1069
+ // ── Spot Holdings (already fetched in parallel above) ──
1067
1070
  if (spotRows.length > 0) {
1068
1071
  console.log(chalk.white.bold("\n Spot Holdings"));
1069
1072
  console.log(makeTable(["Token", "Exchange", "Amount", "Price", "Value"], spotRows));
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
- import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ import type { ExchangeAdapter } from "../exchanges/index.js";
3
3
  export declare function registerAgentCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean): void;
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
- import type { ExchangeAdapter } from "../../exchanges/interface.js";
2
+ import { type ExchangeAdapter } from "../../exchanges/index.js";
3
3
  export declare function registerArbManageCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
@@ -1,12 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import { makeTable, formatUsd, formatPnl, printJson, jsonOk } from "../../utils.js";
3
+ import { SPOT_PERP_TOKEN_MAP } from "../../exchanges/index.js";
3
4
  import { readExecutionLog, logExecution } from "../../execution-log.js";
4
5
  import { toHourlyRate, computeAnnualSpread } from "../../funding.js";
5
6
  import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
6
7
  import { computeBasisRisk } from "../../arb-utils.js";
7
8
  import { smartOrder } from "../../smart-order.js";
8
9
  import { removePosition as persistRemovePosition, getPositions as getArbStatePositions, loadArbState } from "../../arb-state.js";
9
- import { SPOT_PERP_TOKEN_MAP } from "../../exchanges/spot-interface.js";
10
10
  import { fetchAllBalances, computeRebalancePlan } from "../../rebalance.js";
11
11
  import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
12
12
  import { computeEnhancedStats } from "../../arb-history-stats.js";
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ import type { ExchangeAdapter } from "../exchanges/index.js";
3
3
  interface FundingSnapshot {
4
4
  symbol: string;
5
5
  pacRate: number;
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { formatUsd, printJson, jsonOk, makeTable } from "../utils.js";
3
3
  import { computeExecutableSize } from "../liquidity.js";
4
- import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/sizing.js";
4
+ import { computeMatchedSize, computeSpotPerpMatchedSize } from "../arb/index.js";
5
5
  import { computeAnnualSpread, toHourlyRate } from "../funding.js";
6
6
  import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, fetchLighterOrderBookDetailsRaw, fetchLighterFundingRatesRaw, parseLighterRaw, } from "../shared-api.js";
7
7
  import { scanDexArb } from "../dex-asset-map.js";
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ import type { ExchangeAdapter } from "../exchanges/index.js";
3
3
  export declare function registerArbCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
4
4
  export interface PriceSnapshot {
5
5
  symbol: string;
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
- import type { ExchangeAdapter } from "../exchanges/interface.js";
2
+ import type { ExchangeAdapter } from "../exchanges/index.js";
3
3
  export declare function registerBotCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
@@ -1,14 +1,8 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk } from "../utils.js";
3
- import { loadBotConfig, quickGridConfig, quickDCAConfig } from "../bot/config.js";
4
- import { runBot } from "../bot/engine.js";
5
- import { PRESETS, getPreset, getPresetsByStrategy } from "../bot/presets.js";
3
+ import { loadBotConfig, quickGridConfig, quickDCAConfig, runBot, PRESETS, getPreset, getPresetsByStrategy } from "../bot/index.js";
6
4
  import { updateJobState } from "../jobs.js";
7
- import { runTWAP } from "../strategies/twap.js";
8
- import { runFundingArb } from "../strategies/funding-arb.js";
9
- import { runGrid } from "../strategies/grid.js";
10
- import { runDCA } from "../strategies/dca.js";
11
- import { runTrailingStop } from "../strategies/trailing-stop.js";
5
+ import { runTWAP, runFundingArb, runGrid, runDCA, runTrailingStop } from "../strategies/index.js";
12
6
  export function registerBotCommands(program, getAdapter, getAdapterFor, isJson) {
13
7
  const bot = program.command("bot").description("Automated trading bots with condition monitoring & risk management");
14
8
  // ── bot start <config> ──
@@ -16,6 +10,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
16
10
  .command("start <config>")
17
11
  .description("Start a bot from a YAML/JSON config file")
18
12
  .option("--background", "Run in background (tmux)")
13
+ .option("--headless", "Run without TUI dashboard")
19
14
  .option("--job-id <id>", "Job ID (set by background runner)")
20
15
  .action(async (configPath, opts) => {
21
16
  const config = loadBotConfig(configPath);
@@ -35,8 +30,9 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
35
30
  const adapter = config.strategy.type === "funding-arb"
36
31
  ? await getAdapterFor(config.exchange)
37
32
  : await getAdapter();
33
+ const mode = resolveOutputMode(isJson, opts.headless);
38
34
  const log = makeLog();
39
- await runBot(adapter, config, opts.jobId, log);
35
+ await runBot(adapter, config, opts.jobId, log, undefined, mode);
40
36
  });
41
37
  // ── bot quick grid ──
42
38
  bot
@@ -50,6 +46,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
50
46
  .option("--max-drawdown <usd>", "Stop if drawdown exceeds ($)", "100")
51
47
  .option("--max-runtime <sec>", "Max runtime in seconds", "0")
52
48
  .option("--background", "Run in background (tmux)")
49
+ .option("--headless", "Run without TUI dashboard")
53
50
  .option("--job-id <id>", "Job ID")
54
51
  .action(async (symbol, opts) => {
55
52
  const adapter = await getAdapter();
@@ -84,8 +81,9 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
84
81
  printBotJobStarted(config.name, job.id);
85
82
  return;
86
83
  }
84
+ const mode = resolveOutputMode(isJson, opts.headless);
87
85
  const log = makeLog();
88
- await runBot(adapter, config, opts.jobId, log);
86
+ await runBot(adapter, config, opts.jobId, log, undefined, mode);
89
87
  });
90
88
  // ── bot quick dca ──
91
89
  bot
@@ -96,6 +94,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
96
94
  .option("--trigger-drop <pct>", "Only buy when price drops X% from recent high")
97
95
  .option("--max-drawdown <usd>", "Stop if drawdown exceeds ($)", "100")
98
96
  .option("--background", "Run in background (tmux)")
97
+ .option("--headless", "Run without TUI dashboard")
99
98
  .option("--job-id <id>", "Job ID")
100
99
  .action(async (symbol, side, amount, interval, opts) => {
101
100
  const adapter = await getAdapter();
@@ -129,8 +128,9 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
129
128
  printBotJobStarted(config.name, job.id);
130
129
  return;
131
130
  }
131
+ const mode = resolveOutputMode(isJson, opts.headless);
132
132
  const log = makeLog();
133
- await runBot(adapter, config, opts.jobId, log);
133
+ await runBot(adapter, config, opts.jobId, log, undefined, mode);
134
134
  });
135
135
  // ── bot quick arb ──
136
136
  bot
@@ -144,6 +144,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
144
144
  .option("--interval <sec>", "Check interval in seconds", "60")
145
145
  .option("--max-drawdown <usd>", "Stop if drawdown exceeds ($)", "200")
146
146
  .option("--background", "Run in background (tmux)")
147
+ .option("--headless", "Run without TUI dashboard")
147
148
  .option("--job-id <id>", "Job ID")
148
149
  .action(async (opts) => {
149
150
  const exchangeNames = opts.exchanges.split(",").map(e => e.trim());
@@ -204,8 +205,9 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
204
205
  printBotJobStarted(config.name, job.id);
205
206
  return;
206
207
  }
208
+ const mode = resolveOutputMode(isJson, opts.headless);
207
209
  const log = makeLog();
208
- await runBot(primaryAdapter, config, opts.jobId, log, adapters);
210
+ await runBot(primaryAdapter, config, opts.jobId, log, adapters, mode);
209
211
  });
210
212
  // ── bot preset list ──
211
213
  bot
@@ -240,6 +242,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
240
242
  .command("preset <name> [symbol]")
241
243
  .description("Start a bot from a preset (use 'preset-list' to see options)")
242
244
  .option("--background", "Run in background (tmux)")
245
+ .option("--headless", "Run without TUI dashboard")
243
246
  .option("--job-id <id>", "Job ID")
244
247
  .action(async (name, symbol, opts) => {
245
248
  const preset = getPreset(name);
@@ -287,7 +290,8 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
287
290
  console.log(chalk.cyan.bold(`\n Starting preset: ${chalk.white(preset.name)}`));
288
291
  console.log(chalk.gray(` ${preset.description}\n`));
289
292
  }
290
- await runBot(primaryAdapter, config, opts.jobId, log, adapters);
293
+ const mode = resolveOutputMode(isJson, opts.headless);
294
+ await runBot(primaryAdapter, config, opts.jobId, log, adapters, mode);
291
295
  return;
292
296
  }
293
297
  // Grid / DCA presets
@@ -312,7 +316,180 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
312
316
  console.log(chalk.cyan.bold(`\n Starting preset: ${chalk.white(preset.name)}`));
313
317
  console.log(chalk.gray(` ${preset.description}\n`));
314
318
  }
315
- await runBot(adapter, config, opts.jobId, log);
319
+ const mode = resolveOutputMode(isJson, opts.headless);
320
+ await runBot(adapter, config, opts.jobId, log, undefined, mode);
321
+ });
322
+ // ── bot list-strategies ──
323
+ bot
324
+ .command("list-strategies")
325
+ .description("List all available trading strategies")
326
+ .action(async () => {
327
+ await import("../bot/engine.js");
328
+ const { listStrategies } = await import("../bot/strategy-registry.js");
329
+ const strategies = listStrategies();
330
+ if (isJson())
331
+ return printJson(jsonOk({ strategies }));
332
+ console.log(chalk.cyan.bold("\n Available Strategies:\n"));
333
+ for (const s of strategies) {
334
+ console.log(` ${chalk.white(s)}`);
335
+ }
336
+ console.log();
337
+ });
338
+ // ── bot apex ──
339
+ bot
340
+ .command("apex [symbol]")
341
+ .description("Start APEX autonomous orchestrator")
342
+ .option("--preset <preset>", "Preset: default, conservative, aggressive", "default")
343
+ .option("--max-slots <n>", "Max concurrent positions", "3")
344
+ .option("--daily-limit <usd>", "Daily loss limit in USD", "250")
345
+ .option("--headless", "Run without TUI dashboard")
346
+ .action(async (symbol, opts) => {
347
+ const adapter = await getAdapter();
348
+ const sym = (symbol ?? "ETH").toUpperCase();
349
+ const config = {
350
+ name: `apex-${sym.toLowerCase()}-${Date.now().toString(36)}`,
351
+ exchange: adapter.name,
352
+ symbol: sym,
353
+ strategy: {
354
+ type: "apex",
355
+ preset: opts.preset,
356
+ max_slots: parseInt(opts.maxSlots),
357
+ },
358
+ entry_conditions: [{ type: "always", value: 0 }],
359
+ exit_conditions: [],
360
+ risk: {
361
+ max_position_usd: 1000,
362
+ max_daily_loss: parseFloat(opts.dailyLimit),
363
+ max_drawdown: parseFloat(opts.dailyLimit) * 2,
364
+ pause_after_loss_sec: 300,
365
+ max_open_bots: 1,
366
+ },
367
+ monitor_interval_sec: 10,
368
+ };
369
+ const mode = resolveOutputMode(isJson, opts.headless);
370
+ const log = makeLog();
371
+ await runBot(adapter, config, undefined, log, undefined, mode);
372
+ });
373
+ // ── bot reflect ──
374
+ bot
375
+ .command("reflect")
376
+ .description("Analyze trading performance")
377
+ .option("--period <days>", "Analysis period in days", "7")
378
+ .action(async (opts) => {
379
+ const { readJournal } = await import("../bot/trade-journal.js");
380
+ const { analyzePerformance } = await import("../bot/reflect.js");
381
+ const periodDays = Number(opts.period);
382
+ const entries = readJournal({ from: Date.now() - periodDays * 86400000 });
383
+ const report = analyzePerformance(entries, periodDays);
384
+ if (isJson())
385
+ return printJson(jsonOk(report));
386
+ console.log(chalk.cyan.bold("\n Performance Report\n"));
387
+ console.log(` Period: ${chalk.white(`${report.period.days}d`)} (${new Date(report.period.from).toLocaleDateString()} – ${new Date(report.period.to).toLocaleDateString()})`);
388
+ console.log(` Total trades: ${chalk.white(report.totalTrades)}`);
389
+ if (report.totalTrades === 0) {
390
+ console.log(chalk.gray("\n No trades in the selected period.\n"));
391
+ return;
392
+ }
393
+ const wrColor = report.winRate >= 0.5 ? chalk.green : chalk.red;
394
+ console.log(` Win rate: ${wrColor((report.winRate * 100).toFixed(1) + "%")}`);
395
+ console.log(` Avg win: ${chalk.green(`$${report.avgWin.toFixed(2)}`)}`);
396
+ console.log(` Avg loss: ${chalk.red(`$${report.avgLoss.toFixed(2)}`)}`);
397
+ const pfColor = report.profitFactor >= 1.0 ? chalk.green : chalk.red;
398
+ console.log(` Profit factor: ${pfColor(report.profitFactor === Infinity ? "∞" : report.profitFactor.toFixed(2))}`);
399
+ console.log(` Fee drag: ${chalk.yellow((report.feeDragRatio * 100).toFixed(1) + "%")}`);
400
+ console.log(` Direction: ${chalk.gray(`long ${(report.directionSplit.long * 100).toFixed(0)}% / short ${(report.directionSplit.short * 100).toFixed(0)}%`)}`);
401
+ if (report.bestStrategy)
402
+ console.log(` Best strategy: ${chalk.green(report.bestStrategy)}`);
403
+ if (report.worstStrategy && report.worstStrategy !== report.bestStrategy)
404
+ console.log(` Worst strategy:${chalk.red(report.worstStrategy)}`);
405
+ console.log(chalk.cyan.bold("\n Suggestions:\n"));
406
+ for (const s of report.suggestions) {
407
+ console.log(` ${chalk.white("•")} ${chalk.gray(s)}`);
408
+ }
409
+ console.log();
410
+ });
411
+ // ── bot run <strategy> <symbol> ──
412
+ bot
413
+ .command("run <strategy> [symbol]")
414
+ .description("Run any registered strategy (symbol optional for multi-symbol strategies like funding-auto)")
415
+ .option("--config <path>", "YAML/JSON config file")
416
+ .option("--headless", "Run without TUI dashboard")
417
+ .option("--param <key=value>", "Strategy parameter (repeatable)", (val, acc) => [...acc, val], [])
418
+ .action(async (strategyName, symbol, opts) => {
419
+ const sym = symbol?.toUpperCase() || "ALL";
420
+ // Load or build config
421
+ let config;
422
+ if (opts.config) {
423
+ config = loadBotConfig(opts.config);
424
+ // Override strategy type and symbol from CLI args
425
+ config.strategy = { ...config.strategy, type: strategyName };
426
+ config.symbol = sym;
427
+ }
428
+ else {
429
+ // Parse --param key=value pairs
430
+ const params = { type: strategyName };
431
+ for (const kv of opts.param) {
432
+ const eq = kv.indexOf("=");
433
+ if (eq === -1)
434
+ continue;
435
+ const key = kv.slice(0, eq);
436
+ const raw = kv.slice(eq + 1);
437
+ // Try to coerce numbers and booleans
438
+ if (raw === "true")
439
+ params[key] = true;
440
+ else if (raw === "false")
441
+ params[key] = false;
442
+ else if (!isNaN(Number(raw)) && raw !== "")
443
+ params[key] = Number(raw);
444
+ else
445
+ params[key] = raw;
446
+ }
447
+ const adapter = await getAdapter();
448
+ config = {
449
+ name: `${strategyName}-${sym.toLowerCase()}-${Date.now().toString(36)}`,
450
+ exchange: adapter.name,
451
+ symbol: sym,
452
+ strategy: params,
453
+ entry_conditions: [{ type: "always", value: 0 }],
454
+ exit_conditions: [],
455
+ risk: {
456
+ max_position_usd: 1000,
457
+ max_daily_loss: 100,
458
+ max_drawdown: 200,
459
+ pause_after_loss_sec: 300,
460
+ max_open_bots: 5,
461
+ },
462
+ monitor_interval_sec: 10,
463
+ };
464
+ }
465
+ // Verify strategy is registered
466
+ await import("../bot/engine.js");
467
+ const { getStrategy } = await import("../bot/strategy-registry.js");
468
+ if (!getStrategy(strategyName)) {
469
+ console.error(chalk.red(`\n Unknown strategy: "${strategyName}"`));
470
+ console.error(chalk.gray(` Run 'perp bot list-strategies' to see available strategies.\n`));
471
+ return;
472
+ }
473
+ const adapter = await getAdapter();
474
+ config.exchange = adapter.name;
475
+ const mode = resolveOutputMode(isJson, opts.headless);
476
+ const log = makeLog();
477
+ // Multi-exchange strategies need all adapters
478
+ let extraAdapters;
479
+ const multiExchangeStrategies = ["funding-auto", "funding-arb", "funding-arb-v2", "basis-arb", "hedge-agent"];
480
+ if (multiExchangeStrategies.includes(strategyName) && getAdapterFor) {
481
+ extraAdapters = new Map();
482
+ for (const ex of ["pacifica", "hyperliquid", "lighter"]) {
483
+ if (ex === adapter.name)
484
+ continue;
485
+ try {
486
+ const a = await getAdapterFor(ex);
487
+ extraAdapters.set(ex, a);
488
+ }
489
+ catch { /* skip unavailable exchanges */ }
490
+ }
491
+ }
492
+ await runBot(adapter, config, undefined, log, extraAdapters, mode);
316
493
  });
317
494
  // ── bot example ──
318
495
  bot
@@ -341,6 +518,13 @@ function makeLog() {
341
518
  console.log(`${chalk.gray(ts)} ${msg}`);
342
519
  };
343
520
  }
521
+ function resolveOutputMode(isJson, headless) {
522
+ if (isJson())
523
+ return "json";
524
+ if (headless)
525
+ return "headless";
526
+ return process.stdout.isTTY ? "tui" : "headless";
527
+ }
344
528
  function printBotJobStarted(name, jobId) {
345
529
  console.log(chalk.green(`\n Bot "${name}" started in background.`));
346
530
  console.log(` Job ID: ${chalk.white.bold(jobId)}`);