perp-cli 0.4.0 → 0.4.8

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 (79) hide show
  1. package/README.md +2 -0
  2. package/dist/arb/sizing.d.ts +12 -1
  3. package/dist/arb/sizing.js +38 -5
  4. package/dist/arb/state.d.ts +6 -0
  5. package/dist/arb/utils.d.ts +2 -0
  6. package/dist/arb/utils.js +1 -1
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/agent.js +12 -9
  10. package/dist/commands/arb/index.js +18 -3
  11. package/dist/commands/arb-auto.js +885 -67
  12. package/dist/commands/dex.js +7 -1
  13. package/dist/commands/funding.d.ts +2 -1
  14. package/dist/commands/funding.js +211 -4
  15. package/dist/commands/manage.js +1 -1
  16. package/dist/commands/market.js +1 -1
  17. package/dist/commands/multileg.d.ts +16 -0
  18. package/dist/commands/multileg.js +174 -0
  19. package/dist/commands/pnl.d.ts +12 -0
  20. package/dist/commands/pnl.js +243 -0
  21. package/dist/commands/settings.d.ts +2 -1
  22. package/dist/commands/settings.js +133 -2
  23. package/dist/commands/stream.js +132 -55
  24. package/dist/commands/trade.js +118 -43
  25. package/dist/commands/wallet.js +1 -1
  26. package/dist/config.d.ts +5 -0
  27. package/dist/config.js +12 -0
  28. package/dist/constants.d.ts +14 -0
  29. package/dist/constants.js +40 -0
  30. package/dist/dashboard/ws-feeds.js +1 -1
  31. package/dist/equity-tracker.d.ts +57 -0
  32. package/dist/equity-tracker.js +209 -0
  33. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  34. package/dist/exchanges/hyperliquid-spot.js +317 -0
  35. package/dist/exchanges/hyperliquid.d.ts +26 -5
  36. package/dist/exchanges/hyperliquid.js +181 -92
  37. package/dist/exchanges/interface.d.ts +1 -1
  38. package/dist/exchanges/lighter-spot.d.ts +55 -0
  39. package/dist/exchanges/lighter-spot.js +417 -0
  40. package/dist/exchanges/lighter.d.ts +10 -2
  41. package/dist/exchanges/lighter.js +132 -37
  42. package/dist/exchanges/pacifica.d.ts +11 -4
  43. package/dist/exchanges/pacifica.js +52 -37
  44. package/dist/exchanges/spot-interface.d.ts +45 -0
  45. package/dist/exchanges/spot-interface.js +22 -0
  46. package/dist/execution-log.d.ts +2 -2
  47. package/dist/funding/rates.d.ts +36 -0
  48. package/dist/funding/rates.js +146 -0
  49. package/dist/index.js +148 -99
  50. package/dist/liquidity.d.ts +12 -0
  51. package/dist/liquidity.js +43 -0
  52. package/dist/settings.d.ts +6 -0
  53. package/dist/settings.js +18 -2
  54. package/dist/signer/evm-local.d.ts +12 -0
  55. package/dist/signer/evm-local.js +19 -0
  56. package/dist/signer/index.d.ts +3 -0
  57. package/dist/signer/index.js +2 -0
  58. package/dist/signer/interface.d.ts +31 -0
  59. package/dist/signer/interface.js +7 -0
  60. package/dist/signer/solana-local.d.ts +13 -0
  61. package/dist/signer/solana-local.js +19 -0
  62. package/dist/smart-order.d.ts +44 -0
  63. package/dist/smart-order.js +145 -0
  64. package/dist/trade-validator.js +2 -1
  65. package/dist/utils.js +7 -1
  66. package/dist/ws/market-feed.d.ts +74 -0
  67. package/dist/ws/market-feed.js +227 -0
  68. package/package.json +1 -1
  69. package/skills/perp-cli/SKILL.md +102 -4
  70. package/skills/perp-cli/references/commands.md +16 -1
  71. package/skills/perp-cli/references/strategies.md +1 -1
  72. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  73. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  74. package/skills/perp-cli/scripts/preflight.sh +98 -0
  75. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  76. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  77. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  78. package/skills/perp-cli/templates/open-position.sh +0 -38
  79. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -5,6 +5,7 @@ import { printJson, errorAndExit, withJsonErrors, jsonOk, jsonError, symbolMatch
5
5
  import { logExecution } from "../execution-log.js";
6
6
  import { validateTrade } from "../trade-validator.js";
7
7
  import { generateClientId, logClientId, isOrderDuplicate } from "../client-id-tracker.js";
8
+ import { smartOrder } from "../smart-order.js";
8
9
  import chalk from "chalk";
9
10
  function pac(adapter) {
10
11
  if (!(adapter instanceof PacificaAdapter))
@@ -46,6 +47,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
46
47
  .description("Place a market order (side: buy/sell)")
47
48
  .option("-s, --slippage <pct>", "Slippage percent", "1")
48
49
  .option("--reduce-only", "Reduce only order")
50
+ .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick (reduces slippage)")
49
51
  .option("--client-id <id>", "Client order ID for idempotent tracking")
50
52
  .option("--auto-id", "Auto-generate a client order ID")
51
53
  .action(async (symbol, side, size, opts) => {
@@ -60,7 +62,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
60
62
  return;
61
63
  }
62
64
  const adapter = await getAdapter();
63
- if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size }))
65
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, smart: !!opts.smart }))
64
66
  return;
65
67
  if (clientId) {
66
68
  logClientId({
@@ -71,11 +73,17 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
71
73
  }
72
74
  let result;
73
75
  try {
74
- result = await adapter.marketOrder(symbol.toUpperCase(), s, size);
76
+ if (opts.smart) {
77
+ const sr = await smartOrder(adapter, symbol.toUpperCase(), s, size);
78
+ result = { ...sr.result, smartOrder: { method: sr.method, price: sr.price, bestBookPrice: sr.bestBookPrice, tickSize: sr.tickSize } };
79
+ }
80
+ else {
81
+ result = await adapter.marketOrder(symbol.toUpperCase(), s, size);
82
+ }
75
83
  logExecution({
76
84
  type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
77
85
  side: s, size, status: "success", dryRun: false,
78
- meta: clientId ? { clientOrderId: clientId } : undefined,
86
+ meta: { ...(clientId ? { clientOrderId: clientId } : {}), ...(opts.smart ? { smart: true } : {}) },
79
87
  });
80
88
  }
81
89
  catch (err) {
@@ -83,7 +91,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
83
91
  type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
84
92
  side: s, size, status: "failed", dryRun: false,
85
93
  error: err instanceof Error ? err.message : String(err),
86
- meta: clientId ? { clientOrderId: clientId } : undefined,
94
+ meta: { ...(clientId ? { clientOrderId: clientId } : {}), ...(opts.smart ? { smart: true } : {}) },
87
95
  });
88
96
  throw err;
89
97
  }
@@ -96,7 +104,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
96
104
  }
97
105
  if (isJson())
98
106
  return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
99
- console.log(chalk.green(`\n Market ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${clientId ? ` (id: ${clientId})` : ""}\n`));
107
+ console.log(chalk.green(`\n Market ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${opts.smart ? " (smart)" : ""}${clientId ? ` (id: ${clientId})` : ""}\n`));
100
108
  printJson(jsonOk(result));
101
109
  });
102
110
  // Shortcuts: trade buy / trade sell
@@ -105,6 +113,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
105
113
  .description("Market buy (shortcut for: trade market <symbol> buy <size>)")
106
114
  .option("-s, --slippage <pct>", "Slippage percent", "1")
107
115
  .option("--reduce-only", "Reduce only order")
116
+ .option("--smart", "Smart execution: IOC limit at best ask + 1 tick")
108
117
  .option("--client-id <id>", "Client order ID")
109
118
  .option("--auto-id", "Auto-generate client order ID")
110
119
  .action(async (symbol, size, opts) => {
@@ -116,12 +125,18 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
116
125
  return;
117
126
  }
118
127
  const adapter = await getAdapter();
119
- if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size }))
128
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, smart: !!opts.smart }))
120
129
  return;
121
130
  let result;
122
131
  try {
123
- result = await adapter.marketOrder(symbol.toUpperCase(), "buy", size);
124
- logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, status: "success", dryRun: false });
132
+ if (opts.smart) {
133
+ const sr = await smartOrder(adapter, symbol.toUpperCase(), "buy", size);
134
+ result = { ...sr.result, smartOrder: { method: sr.method, price: sr.price, bestBookPrice: sr.bestBookPrice, tickSize: sr.tickSize } };
135
+ }
136
+ else {
137
+ result = await adapter.marketOrder(symbol.toUpperCase(), "buy", size);
138
+ }
139
+ logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, status: "success", dryRun: false, meta: opts.smart ? { smart: true } : undefined });
125
140
  }
126
141
  catch (err) {
127
142
  logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
@@ -129,7 +144,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
129
144
  }
130
145
  if (isJson())
131
146
  return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
132
- console.log(chalk.green(`\n Market BUY ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.\n`));
147
+ console.log(chalk.green(`\n Market BUY ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${opts.smart ? " (smart)" : ""}\n`));
133
148
  printJson(jsonOk(result));
134
149
  });
135
150
  trade
@@ -137,6 +152,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
137
152
  .description("Market sell (shortcut for: trade market <symbol> sell <size>)")
138
153
  .option("-s, --slippage <pct>", "Slippage percent", "1")
139
154
  .option("--reduce-only", "Reduce only order")
155
+ .option("--smart", "Smart execution: IOC limit at best bid - 1 tick")
140
156
  .option("--client-id <id>", "Client order ID")
141
157
  .option("--auto-id", "Auto-generate client order ID")
142
158
  .action(async (symbol, size, opts) => {
@@ -148,12 +164,18 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
148
164
  return;
149
165
  }
150
166
  const adapter = await getAdapter();
151
- if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size }))
167
+ if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, smart: !!opts.smart }))
152
168
  return;
153
169
  let result;
154
170
  try {
155
- result = await adapter.marketOrder(symbol.toUpperCase(), "sell", size);
156
- logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, status: "success", dryRun: false });
171
+ if (opts.smart) {
172
+ const sr = await smartOrder(adapter, symbol.toUpperCase(), "sell", size);
173
+ result = { ...sr.result, smartOrder: { method: sr.method, price: sr.price, bestBookPrice: sr.bestBookPrice, tickSize: sr.tickSize } };
174
+ }
175
+ else {
176
+ result = await adapter.marketOrder(symbol.toUpperCase(), "sell", size);
177
+ }
178
+ logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, status: "success", dryRun: false, meta: opts.smart ? { smart: true } : undefined });
157
179
  }
158
180
  catch (err) {
159
181
  logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
@@ -161,7 +183,7 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
161
183
  }
162
184
  if (isJson())
163
185
  return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
164
- console.log(chalk.green(`\n Market SELL ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.\n`));
186
+ console.log(chalk.green(`\n Market SELL ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.${opts.smart ? " (smart)" : ""}\n`));
165
187
  printJson(jsonOk(result));
166
188
  });
167
189
  trade
@@ -223,21 +245,42 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
223
245
  printJson(jsonOk(result));
224
246
  });
225
247
  trade
226
- .command("cancel <symbol> <orderId>")
227
- .description("Cancel a specific order")
228
- .action(async (symbol, orderId) => {
248
+ .command("cancel <symbolOrOrderId> [orderId]")
249
+ .description("Cancel an order by orderId (auto-detects symbol), or by symbol + orderId")
250
+ .action(async (symbolOrOrderId, orderId) => {
229
251
  const adapter = await getAdapter();
230
- if (dryRunGuard("cancel", { exchange: adapter.name, symbol: symbol.toUpperCase(), orderId }))
252
+ let symbol;
253
+ let oid;
254
+ if (orderId) {
255
+ // cancel <symbol> <orderId>
256
+ symbol = symbolOrOrderId.toUpperCase();
257
+ oid = orderId;
258
+ }
259
+ else {
260
+ // cancel <orderId> — look up symbol from open orders
261
+ oid = symbolOrOrderId;
262
+ const orders = await adapter.getOpenOrders();
263
+ const match = orders.find((o) => String(o.orderId) === oid);
264
+ if (!match) {
265
+ const err = `Order ${oid} not found in open orders`;
266
+ if (isJson())
267
+ return printJson(jsonOk({ cancelled: false, reason: err }));
268
+ console.log(chalk.yellow(`\n ${err}\n`));
269
+ return;
270
+ }
271
+ symbol = match.symbol;
272
+ }
273
+ if (dryRunGuard("cancel", { exchange: adapter.name, symbol, orderId: oid }))
231
274
  return;
232
275
  try {
233
- const result = await adapter.cancelOrder(symbol.toUpperCase(), orderId);
234
- logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId } });
276
+ const result = await adapter.cancelOrder(symbol, oid);
277
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId: oid } });
235
278
  if (isJson())
236
279
  return printJson(jsonOk(result));
237
- console.log(chalk.green(`\n Order ${orderId} cancelled on ${adapter.name}.\n`));
280
+ console.log(chalk.green(`\n Order ${oid} cancelled on ${adapter.name}.\n`));
238
281
  }
239
282
  catch (err) {
240
- logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId } });
283
+ logExecution({ type: "cancel", exchange: adapter.name, symbol, side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId: oid } });
241
284
  throw err;
242
285
  }
243
286
  });
@@ -697,10 +740,11 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
697
740
  trade
698
741
  .command("close-all")
699
742
  .description("Close all open positions (market orders on opposite side)")
700
- .action(async () => {
743
+ .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick")
744
+ .action(async (opts) => {
701
745
  await withJsonErrors(isJson(), async () => {
702
746
  const adapter = await getAdapter();
703
- if (dryRunGuard("close_all", { exchange: adapter.name }))
747
+ if (dryRunGuard("close_all", { exchange: adapter.name, smart: !!opts.smart }))
704
748
  return;
705
749
  const positions = await adapter.getPositions();
706
750
  if (positions.length === 0) {
@@ -710,18 +754,25 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
710
754
  return;
711
755
  }
712
756
  if (!isJson())
713
- console.log(chalk.cyan(`\n Closing ${positions.length} position(s) on ${adapter.name}...\n`));
757
+ console.log(chalk.cyan(`\n Closing ${positions.length} position(s) on ${adapter.name}...${opts.smart ? " (smart)" : ""}\n`));
714
758
  const results = [];
715
759
  for (const pos of positions) {
716
760
  const closeSide = pos.side === "long" ? "sell" : "buy";
717
761
  if (!isJson())
718
762
  console.log(chalk.gray(` ${closeSide.toUpperCase()} ${pos.size} ${pos.symbol} (closing ${pos.side})...`));
719
- const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
763
+ let result;
764
+ if (opts.smart) {
765
+ const sr = await smartOrder(adapter, pos.symbol, closeSide, pos.size, { reduceOnly: true });
766
+ result = { ...sr.result, smartOrder: { method: sr.method, price: sr.price, bestBookPrice: sr.bestBookPrice } };
767
+ }
768
+ else {
769
+ result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
770
+ }
720
771
  results.push(result);
721
772
  logExecution({
722
773
  type: "market_order", exchange: adapter.name, symbol: pos.symbol,
723
774
  side: closeSide, size: pos.size, status: "success", dryRun: false,
724
- meta: { action: "close-all", originalSide: pos.side },
775
+ meta: { action: "close-all", originalSide: pos.side, smart: !!opts.smart },
725
776
  });
726
777
  }
727
778
  if (isJson())
@@ -732,7 +783,8 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
732
783
  trade
733
784
  .command("close <symbol>")
734
785
  .description("Close a specific symbol's position")
735
- .action(async (symbol) => {
786
+ .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick")
787
+ .action(async (symbol, opts) => {
736
788
  await withJsonErrors(isJson(), async () => {
737
789
  const sym = symbol.toUpperCase();
738
790
  const adapter = await getAdapter();
@@ -744,31 +796,39 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
744
796
  errorAndExit(`No open position for ${sym}`);
745
797
  }
746
798
  const closeSide = pos.side === "long" ? "sell" : "buy";
747
- if (dryRunGuard("close", { exchange: adapter.name, symbol: sym, side: closeSide, size: pos.size, originalSide: pos.side }))
799
+ if (dryRunGuard("close", { exchange: adapter.name, symbol: sym, side: closeSide, size: pos.size, originalSide: pos.side, smart: !!opts.smart }))
748
800
  return;
749
801
  if (!isJson())
750
- console.log(chalk.cyan(`\n Closing ${pos.side} ${pos.size} ${sym} on ${adapter.name}...\n`));
751
- const result = await adapter.marketOrder(sym, closeSide, pos.size);
802
+ console.log(chalk.cyan(`\n Closing ${pos.side} ${pos.size} ${sym} on ${adapter.name}...${opts.smart ? " (smart)" : ""}\n`));
803
+ let result;
804
+ if (opts.smart) {
805
+ const sr = await smartOrder(adapter, sym, closeSide, pos.size, { reduceOnly: true });
806
+ result = { ...sr.result, smartOrder: { method: sr.method, price: sr.price, bestBookPrice: sr.bestBookPrice } };
807
+ }
808
+ else {
809
+ result = await adapter.marketOrder(sym, closeSide, pos.size);
810
+ }
752
811
  logExecution({
753
812
  type: "market_order", exchange: adapter.name, symbol: sym,
754
813
  side: closeSide, size: pos.size, status: "success", dryRun: false,
755
- meta: { action: "close", originalSide: pos.side },
814
+ meta: { action: "close", originalSide: pos.side, smart: !!opts.smart },
756
815
  });
757
816
  if (isJson())
758
817
  return printJson(jsonOk(result));
759
- console.log(chalk.green(`\n Closed ${pos.side} ${pos.size} ${sym} on ${adapter.name}.\n`));
818
+ console.log(chalk.green(`\n Closed ${pos.side} ${pos.size} ${sym} on ${adapter.name}.${opts.smart ? " (smart)" : ""}\n`));
760
819
  });
761
820
  });
762
821
  trade
763
822
  .command("flatten")
764
823
  .description("Cancel all orders AND close all positions (full cleanup)")
765
- .action(async () => {
824
+ .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick")
825
+ .action(async (opts) => {
766
826
  await withJsonErrors(isJson(), async () => {
767
827
  const adapter = await getAdapter();
768
- if (dryRunGuard("flatten", { exchange: adapter.name }))
828
+ if (dryRunGuard("flatten", { exchange: adapter.name, smart: !!opts.smart }))
769
829
  return;
770
830
  if (!isJson())
771
- console.log(chalk.cyan(`\n Flattening account on ${adapter.name}...\n`));
831
+ console.log(chalk.cyan(`\n Flattening account on ${adapter.name}...${opts.smart ? " (smart)" : ""}\n`));
772
832
  // Step 1: Cancel all orders
773
833
  if (!isJson())
774
834
  console.log(chalk.gray(" Cancelling all open orders..."));
@@ -780,12 +840,19 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
780
840
  const closeSide = pos.side === "long" ? "sell" : "buy";
781
841
  if (!isJson())
782
842
  console.log(chalk.gray(` ${closeSide.toUpperCase()} ${pos.size} ${pos.symbol} (closing ${pos.side})...`));
783
- const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
843
+ let result;
844
+ if (opts.smart) {
845
+ const sr = await smartOrder(adapter, pos.symbol, closeSide, pos.size, { reduceOnly: true });
846
+ result = sr.result;
847
+ }
848
+ else {
849
+ result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
850
+ }
784
851
  closeResults.push(result);
785
852
  logExecution({
786
853
  type: "market_order", exchange: adapter.name, symbol: pos.symbol,
787
854
  side: closeSide, size: pos.size, status: "success", dryRun: false,
788
- meta: { action: "flatten", originalSide: pos.side },
855
+ meta: { action: "flatten", originalSide: pos.side, smart: !!opts.smart },
789
856
  });
790
857
  }
791
858
  if (isJson())
@@ -796,7 +863,8 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
796
863
  trade
797
864
  .command("reduce <symbol> <percent>")
798
865
  .description("Reduce a position by a percentage (e.g., perp trade reduce BTC 50)")
799
- .action(async (symbol, percent) => {
866
+ .option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick")
867
+ .action(async (symbol, percent, opts) => {
800
868
  await withJsonErrors(isJson(), async () => {
801
869
  const sym = symbol.toUpperCase();
802
870
  const pct = parseFloat(percent);
@@ -813,19 +881,26 @@ export function registerTradeCommands(program, getAdapter, isJson, isDryRun = ()
813
881
  const fullSize = parseFloat(pos.size);
814
882
  const reduceSize = (fullSize * pct / 100).toString();
815
883
  const closeSide = pos.side === "long" ? "sell" : "buy";
816
- if (dryRunGuard("reduce", { exchange: adapter.name, symbol: sym, side: closeSide, size: reduceSize, percent: pct, originalSize: pos.size }))
884
+ if (dryRunGuard("reduce", { exchange: adapter.name, symbol: sym, side: closeSide, size: reduceSize, percent: pct, originalSize: pos.size, smart: !!opts.smart }))
817
885
  return;
818
886
  if (!isJson())
819
- console.log(chalk.cyan(`\n Reducing ${sym} ${pos.side} by ${pct}% (${reduceSize} of ${pos.size}) on ${adapter.name}...\n`));
820
- const result = await adapter.marketOrder(sym, closeSide, reduceSize);
887
+ console.log(chalk.cyan(`\n Reducing ${sym} ${pos.side} by ${pct}% (${reduceSize} of ${pos.size}) on ${adapter.name}...${opts.smart ? " (smart)" : ""}\n`));
888
+ let result;
889
+ if (opts.smart) {
890
+ const sr = await smartOrder(adapter, sym, closeSide, reduceSize, { reduceOnly: true });
891
+ result = sr.result;
892
+ }
893
+ else {
894
+ result = await adapter.marketOrder(sym, closeSide, reduceSize);
895
+ }
821
896
  logExecution({
822
897
  type: "market_order", exchange: adapter.name, symbol: sym,
823
898
  side: closeSide, size: reduceSize, status: "success", dryRun: false,
824
- meta: { action: "reduce", percent: pct, originalSize: pos.size, originalSide: pos.side },
899
+ meta: { action: "reduce", percent: pct, originalSize: pos.size, originalSide: pos.side, smart: !!opts.smart },
825
900
  });
826
901
  if (isJson())
827
902
  return printJson(jsonOk({ reduced: true, percent: pct, sizeReduced: reduceSize, originalSize: pos.size, result }));
828
- console.log(chalk.green(`\n Reduced ${sym} by ${pct}% (${closeSide} ${reduceSize}) on ${adapter.name}.\n`));
903
+ console.log(chalk.green(`\n Reduced ${sym} by ${pct}% (${closeSide} ${reduceSize}) on ${adapter.name}.${opts.smart ? " (smart)" : ""}\n`));
829
904
  });
830
905
  });
831
906
  // ── Order Status Query ──
@@ -389,7 +389,7 @@ export function registerWalletCommands(program, isJson) {
389
389
  const { LighterAdapter } = await import("../exchanges/lighter.js");
390
390
  const adapter = new LighterAdapter(normalized);
391
391
  await adapter.init();
392
- const apiKeyIndex = 2;
392
+ const apiKeyIndex = 4;
393
393
  const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
394
394
  setEnvVar("LIGHTER_API_KEY", apiKey);
395
395
  setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
package/dist/config.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Keypair } from "@solana/web3.js";
2
2
  export type Exchange = "pacifica" | "hyperliquid" | "lighter";
3
+ /**
4
+ * Try to load a private key without throwing if none is found.
5
+ * Returns null if no key is configured.
6
+ */
7
+ export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string | null>;
3
8
  export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string): Promise<string>;
4
9
  export declare function parseSolanaKeypair(input: string): Keypair;
5
10
  export declare function isEvmPrivateKey(input: string): boolean;
package/dist/config.js CHANGED
@@ -3,6 +3,18 @@ import bs58 from "bs58";
3
3
  import chalk from "chalk";
4
4
  import { resolve } from "path";
5
5
  import { existsSync, readFileSync } from "fs";
6
+ /**
7
+ * Try to load a private key without throwing if none is found.
8
+ * Returns null if no key is configured.
9
+ */
10
+ export async function tryLoadPrivateKey(exchange, pkOverride) {
11
+ try {
12
+ return await loadPrivateKey(exchange, pkOverride);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
6
18
  export async function loadPrivateKey(exchange, pkOverride) {
7
19
  // 1. CLI flag
8
20
  if (pkOverride)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared constants for perp-cli.
3
+ */
4
+ import { type ExchangeFees } from "./settings.js";
5
+ /** Default taker fee per exchange (as fraction, e.g. 0.00035 = 0.035%) */
6
+ export declare const TAKER_FEES: Record<string, number>;
7
+ /** Get taker fee for an exchange. Uses settings if available, falls back to defaults. */
8
+ export declare function getTakerFee(exchange: string): number;
9
+ /** Get maker fee for an exchange. Uses settings if available. */
10
+ export declare function getMakerFee(exchange: string): number;
11
+ /** Get full fee info for an exchange. */
12
+ export declare function getExchangeFees(exchange: string): ExchangeFees;
13
+ /** Default taker fee for single-exchange use (when exchange is unknown). */
14
+ export declare const DEFAULT_TAKER_FEE = 0.00035;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared constants for perp-cli.
3
+ */
4
+ import { loadSettings } from "./settings.js";
5
+ /** Default taker fee per exchange (as fraction, e.g. 0.00035 = 0.035%) */
6
+ export const TAKER_FEES = {
7
+ hyperliquid: 0.00035,
8
+ pacifica: 0.00035,
9
+ lighter: 0,
10
+ };
11
+ /** Get taker fee for an exchange. Uses settings if available, falls back to defaults. */
12
+ export function getTakerFee(exchange) {
13
+ const key = exchange.toLowerCase();
14
+ try {
15
+ const settings = loadSettings();
16
+ if (settings.fees[key]?.taker !== undefined) {
17
+ return settings.fees[key].taker;
18
+ }
19
+ }
20
+ catch { /* fallback to hardcoded */ }
21
+ return TAKER_FEES[key] ?? 0.00035;
22
+ }
23
+ /** Get maker fee for an exchange. Uses settings if available. */
24
+ export function getMakerFee(exchange) {
25
+ const key = exchange.toLowerCase();
26
+ try {
27
+ const settings = loadSettings();
28
+ if (settings.fees[key]?.maker !== undefined) {
29
+ return settings.fees[key].maker;
30
+ }
31
+ }
32
+ catch { /* fallback */ }
33
+ return 0;
34
+ }
35
+ /** Get full fee info for an exchange. */
36
+ export function getExchangeFees(exchange) {
37
+ return { taker: getTakerFee(exchange), maker: getMakerFee(exchange) };
38
+ }
39
+ /** Default taker fee for single-exchange use (when exchange is unknown). */
40
+ export const DEFAULT_TAKER_FEE = 0.00035;
@@ -569,7 +569,7 @@ class LighterFeed extends ExchangeWsFeed {
569
569
  liquidationPrice: String(p.liquidation_price || "N/A"),
570
570
  unrealizedPnl: String(p.unrealized_pnl ?? "0"),
571
571
  leverage: Number(p.initial_margin_fraction ?? 0) > 0
572
- ? Math.round(100 / Number(p.initial_margin_fraction))
572
+ ? Math.round(10000 / Number(p.initial_margin_fraction))
573
573
  : 1,
574
574
  };
575
575
  });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Equity Tracker — time-series equity snapshots for PnL analysis.
3
+ *
4
+ * Stores snapshots in ~/.perp/equity-history/YYYY-MM.jsonl (monthly files).
5
+ * Computes daily PnL, Sharpe ratio, max drawdown, win/loss stats.
6
+ */
7
+ export interface EquitySnapshot {
8
+ ts: string;
9
+ exchange: string;
10
+ equity: number;
11
+ available: number;
12
+ marginUsed: number;
13
+ unrealizedPnl: number;
14
+ positionCount: number;
15
+ }
16
+ export interface DailyPnl {
17
+ date: string;
18
+ exchange: string;
19
+ startEquity: number;
20
+ endEquity: number;
21
+ pnl: number;
22
+ pnlPct: number;
23
+ }
24
+ export interface PnlMetrics {
25
+ totalReturn: number;
26
+ totalReturnPct: number;
27
+ dailyReturns: DailyPnl[];
28
+ sharpeRatio: number;
29
+ maxDrawdown: number;
30
+ maxDrawdownPct: number;
31
+ winDays: number;
32
+ lossDays: number;
33
+ winRate: number;
34
+ peakEquity: number;
35
+ currentDrawdown: number;
36
+ currentDrawdownPct: number;
37
+ bestDay: DailyPnl | null;
38
+ worstDay: DailyPnl | null;
39
+ avgDailyPnl: number;
40
+ period: {
41
+ from: string;
42
+ to: string;
43
+ days: number;
44
+ };
45
+ }
46
+ export declare function saveEquitySnapshot(snap: EquitySnapshot): void;
47
+ export declare function readEquityHistory(opts?: {
48
+ exchange?: string;
49
+ since?: Date;
50
+ until?: Date;
51
+ }): EquitySnapshot[];
52
+ /** Group snapshots by date, take first and last per day for daily PnL. */
53
+ export declare function computeDailyPnl(snapshots: EquitySnapshot[], exchange?: string): DailyPnl[];
54
+ /** Compute comprehensive PnL metrics from equity snapshots. */
55
+ export declare function computePnlMetrics(snapshots: EquitySnapshot[], exchange?: string): PnlMetrics;
56
+ /** Compute weekly PnL from daily PnL data. */
57
+ export declare function aggregateWeekly(daily: DailyPnl[]): DailyPnl[];