perp-cli 0.9.8 → 0.12.0

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 (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -0,0 +1,86 @@
1
+ import { loadOws } from "./ows-loader.js";
2
+ /**
3
+ * EVM signer backed by Open Wallet Standard.
4
+ * Keys never leave the OWS encrypted vault.
5
+ */
6
+ export class OwsEvmSigner {
7
+ _address;
8
+ _walletName;
9
+ _passphrase;
10
+ constructor(walletName, address, passphrase) {
11
+ this._walletName = walletName;
12
+ this._address = address;
13
+ this._passphrase = passphrase;
14
+ }
15
+ static create(walletName, passphrase = "") {
16
+ const ows = loadOws();
17
+ const wallet = ows.getWallet(walletName);
18
+ const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155:"));
19
+ if (!evmAccount) {
20
+ throw new Error(`OWS wallet "${walletName}" has no EVM account`);
21
+ }
22
+ return new OwsEvmSigner(walletName, evmAccount.address, passphrase);
23
+ }
24
+ getAddress() {
25
+ return this._address;
26
+ }
27
+ async signTypedData(domain, types, value) {
28
+ const ows = loadOws();
29
+ // Build EIP-712 structure expected by OWS
30
+ const typedData = JSON.stringify({
31
+ types: {
32
+ EIP712Domain: Object.keys(domain).map((key) => ({
33
+ name: key,
34
+ type: inferEip712DomainType(key),
35
+ })),
36
+ ...types,
37
+ },
38
+ primaryType: Object.keys(types)[0],
39
+ domain,
40
+ message: value,
41
+ });
42
+ const result = ows.signTypedData(this._walletName, "ethereum", typedData, this._passphrase);
43
+ return canonicalizeOwsSignature(result);
44
+ }
45
+ async signMessage(message) {
46
+ const ows = loadOws();
47
+ const msgStr = typeof message === "string"
48
+ ? message
49
+ : Buffer.from(message).toString("hex");
50
+ const encoding = typeof message === "string" ? "utf8" : "hex";
51
+ const result = ows.signMessage(this._walletName, "ethereum", msgStr, this._passphrase, encoding);
52
+ return canonicalizeOwsSignature(result);
53
+ }
54
+ }
55
+ /**
56
+ * Normalize OWS signature output to canonical 65-byte hex (0x-prefixed).
57
+ *
58
+ * OWS may return the signature in two shapes depending on version:
59
+ * (A) 65-byte hex with v already embedded — use as-is
60
+ * (B) 64-byte hex (r+s only) + separate recoveryId — append v
61
+ *
62
+ * For shape B, recoveryId may be either canonical (27/28) or raw (0/1).
63
+ * If raw, add 27 to canonicalize.
64
+ */
65
+ export function canonicalizeOwsSignature(result) {
66
+ const sigHex = result.signature.startsWith("0x") ? result.signature.slice(2) : result.signature;
67
+ if (sigHex.length === 130) {
68
+ // Already 65-byte sig with embedded v — trust OWS
69
+ return `0x${sigHex}`;
70
+ }
71
+ if (sigHex.length !== 128) {
72
+ throw new Error(`Unexpected OWS signature length: ${sigHex.length} hex chars (expected 128 or 130)`);
73
+ }
74
+ // 64-byte r+s — append v from recoveryId
75
+ const rid = result.recoveryId ?? 0;
76
+ const v = rid >= 27 ? rid : rid + 27; // accept canonical or raw recoveryId
77
+ return `0x${sigHex}${v.toString(16).padStart(2, "0")}`;
78
+ }
79
+ function inferEip712DomainType(key) {
80
+ switch (key) {
81
+ case "chainId": return "uint256";
82
+ case "verifyingContract": return "address";
83
+ case "salt": return "bytes32";
84
+ default: return "string";
85
+ }
86
+ }
@@ -0,0 +1,2 @@
1
+ /** Lazy-load the OWS native module (NAPI binding, requires CJS require). */
2
+ export declare function loadOws(): typeof import("@open-wallet-standard/core");
@@ -0,0 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ const _require = createRequire(import.meta.url);
3
+ /** Lazy-load the OWS native module (NAPI binding, requires CJS require). */
4
+ export function loadOws() {
5
+ return _require("@open-wallet-standard/core");
6
+ }
@@ -0,0 +1,25 @@
1
+ import type { SolanaSigner } from "./interface.js";
2
+ /**
3
+ * Solana signer backed by Open Wallet Standard.
4
+ * Keys never leave the OWS encrypted vault.
5
+ */
6
+ export declare class OwsSolanaSigner implements SolanaSigner {
7
+ private _address;
8
+ private _walletName;
9
+ private _passphrase;
10
+ private constructor();
11
+ static create(walletName: string, passphrase?: string): OwsSolanaSigner;
12
+ getPublicKeyBase58(): string;
13
+ signMessage(message: Uint8Array): Promise<Uint8Array>;
14
+ signTransaction(tx: {
15
+ sign(keypair: unknown): void;
16
+ }): void;
17
+ partialSignTransaction(tx: {
18
+ partialSign(keypair: unknown): void;
19
+ }): void;
20
+ /**
21
+ * Sign raw Solana transaction bytes via OWS.
22
+ * The caller is responsible for serializing/deserializing the transaction.
23
+ */
24
+ signTransactionBytes(txBytes: Uint8Array): Uint8Array;
25
+ }
@@ -0,0 +1,56 @@
1
+ import { loadOws } from "./ows-loader.js";
2
+ /**
3
+ * Solana signer backed by Open Wallet Standard.
4
+ * Keys never leave the OWS encrypted vault.
5
+ */
6
+ export class OwsSolanaSigner {
7
+ _address;
8
+ _walletName;
9
+ _passphrase;
10
+ constructor(walletName, address, passphrase) {
11
+ this._walletName = walletName;
12
+ this._address = address;
13
+ this._passphrase = passphrase;
14
+ }
15
+ static create(walletName, passphrase = "") {
16
+ const ows = loadOws();
17
+ const wallet = ows.getWallet(walletName);
18
+ const solAccount = wallet.accounts.find((a) => a.chainId.startsWith("solana:"));
19
+ if (!solAccount) {
20
+ throw new Error(`OWS wallet "${walletName}" has no Solana account`);
21
+ }
22
+ return new OwsSolanaSigner(walletName, solAccount.address, passphrase);
23
+ }
24
+ getPublicKeyBase58() {
25
+ return this._address;
26
+ }
27
+ async signMessage(message) {
28
+ const ows = loadOws();
29
+ const msgHex = Buffer.from(message).toString("hex");
30
+ const result = ows.signMessage(this._walletName, "solana", msgHex, this._passphrase, "hex");
31
+ // Convert hex signature to Uint8Array
32
+ const sigHex = result.signature.startsWith("0x") ? result.signature.slice(2) : result.signature;
33
+ return Uint8Array.from(Buffer.from(sigHex, "hex"));
34
+ }
35
+ signTransaction(tx) {
36
+ // OWS doesn't expose raw keypairs. For Solana transactions that require
37
+ // a keypair, we serialize -> sign via OWS -> inject signature back.
38
+ throw new Error("OWS signer does not support in-place tx.sign(). " +
39
+ "Use signTransactionBytes() or the deposit relayer instead.");
40
+ }
41
+ partialSignTransaction(tx) {
42
+ throw new Error("OWS signer does not support in-place tx.partialSign(). " +
43
+ "Use signTransactionBytes() or the deposit relayer instead.");
44
+ }
45
+ /**
46
+ * Sign raw Solana transaction bytes via OWS.
47
+ * The caller is responsible for serializing/deserializing the transaction.
48
+ */
49
+ signTransactionBytes(txBytes) {
50
+ const ows = loadOws();
51
+ const txHex = Buffer.from(txBytes).toString("hex");
52
+ const result = ows.signTransaction(this._walletName, "solana", txHex, this._passphrase);
53
+ const sigHex = result.signature.startsWith("0x") ? result.signature.slice(2) : result.signature;
54
+ return Uint8Array.from(Buffer.from(sigHex, "hex"));
55
+ }
56
+ }
@@ -16,13 +16,19 @@
16
16
  * - Saves 5-50+ bps vs raw market orders on thin books
17
17
  * - Predictable execution price
18
18
  *
19
- * Fallback: if the IOC limit fails, falls back to market order.
19
+ * SSOT rule #2: silent fallback is forbidden. The opt-in `fallback: true` flag
20
+ * is preserved for callers that explicitly want best-effort market substitution
21
+ * after IOC reject, but the DEFAULT is `false` — failure must surface as a
22
+ * thrown error, never a silent market sweep.
20
23
  */
21
24
  import type { ExchangeAdapter } from "./exchanges/index.js";
22
25
  export interface SmartOrderOpts {
23
26
  /** Extra ticks of tolerance beyond best price. Default: 1 */
24
27
  tickTolerance?: number;
25
- /** Fall back to market order if IOC limit fails. Default: true */
28
+ /**
29
+ * Opt-in: fall back to market order if IOC limit fails.
30
+ * SSOT rule #2 default: `false` — failure throws unless caller explicitly opts in.
31
+ */
26
32
  fallback?: boolean;
27
33
  /** Reduce only flag for closing positions. Default: false */
28
34
  reduceOnly?: boolean;
@@ -16,7 +16,10 @@
16
16
  * - Saves 5-50+ bps vs raw market orders on thin books
17
17
  * - Predictable execution price
18
18
  *
19
- * Fallback: if the IOC limit fails, falls back to market order.
19
+ * SSOT rule #2: silent fallback is forbidden. The opt-in `fallback: true` flag
20
+ * is preserved for callers that explicitly want best-effort market substitution
21
+ * after IOC reject, but the DEFAULT is `false` — failure must surface as a
22
+ * thrown error, never a silent market sweep.
20
23
  */
21
24
  /**
22
25
  * Infer tick size from orderbook prices.
@@ -77,12 +80,38 @@ function hasOrderError(result) {
77
80
  return true;
78
81
  return false;
79
82
  }
83
+ /**
84
+ * Best-effort extraction of the embedded reject reason for the thrown error
85
+ * message. Falls back to JSON.stringify so the caller always gets actionable
86
+ * context.
87
+ */
88
+ function describeOrderError(result) {
89
+ if (!result || typeof result !== "object")
90
+ return String(result);
91
+ const r = result;
92
+ const resp = r.response;
93
+ const data = resp?.data;
94
+ const statuses = data?.statuses;
95
+ if (Array.isArray(statuses) && statuses.length > 0 && statuses[0].error) {
96
+ return String(statuses[0].error);
97
+ }
98
+ if (typeof r.error === "string")
99
+ return r.error;
100
+ if (typeof r.message === "string")
101
+ return r.message;
102
+ try {
103
+ return JSON.stringify(result);
104
+ }
105
+ catch {
106
+ return String(result);
107
+ }
108
+ }
80
109
  /**
81
110
  * Execute a smart order at best bid/ask + 1 tick.
82
111
  * Drop-in replacement for `adapter.marketOrder()`.
83
112
  */
84
113
  export async function smartOrder(adapter, symbol, side, size, opts = {}) {
85
- const { tickTolerance = 1, fallback = true, reduceOnly = false } = opts;
114
+ const { tickTolerance = 1, fallback = false, reduceOnly = false } = opts;
86
115
  // 1. Fetch orderbook
87
116
  const book = await adapter.getOrderbook(symbol);
88
117
  // 2. Validate book has liquidity on the relevant side
@@ -111,15 +140,21 @@ export async function smartOrder(adapter, symbol, side, size, opts = {}) {
111
140
  });
112
141
  // Check for exchange-level rejection inside a "success" response
113
142
  // (e.g., Hyperliquid returns { status: "ok", response: { data: { statuses: [{ error: "..." }] } } })
114
- if (fallback && hasOrderError(result)) {
115
- const fallbackResult = await adapter.marketOrder(symbol, side, size);
116
- return {
117
- result: fallbackResult,
118
- method: "market_fallback",
119
- price: formattedPrice,
120
- bestBookPrice: bestEntry[0],
121
- tickSize: tick.toFixed(decimals),
122
- };
143
+ // SSOT rule #2: when fallback is opt-out (default), an embedded reject
144
+ // must throw — otherwise the caller would silently receive a "limit_ioc"
145
+ // result for a rejected order and treat the failure as success.
146
+ if (hasOrderError(result)) {
147
+ if (fallback) {
148
+ const fallbackResult = await adapter.marketOrder(symbol, side, size);
149
+ return {
150
+ result: fallbackResult,
151
+ method: "market_fallback",
152
+ price: formattedPrice,
153
+ bestBookPrice: bestEntry[0],
154
+ tickSize: tick.toFixed(decimals),
155
+ };
156
+ }
157
+ throw new Error(`smartOrder: exchange rejected IOC limit (${describeOrderError(result)})`);
123
158
  }
124
159
  return {
125
160
  result,
@@ -1,4 +1,4 @@
1
- import { symbolMatch } from "../utils.js";
1
+ import { symbolMatch, logSettledRejections } from "../utils.js";
2
2
  import { updateJobState } from "../jobs.js";
3
3
  import { fetchAllBalances, computeRebalancePlan, hasEnoughBalance } from "../rebalance.js";
4
4
  import { checkArbLiquidity } from "../liquidity.js";
@@ -6,6 +6,9 @@ import { computeAnnualSpread } from "../funding.js";
6
6
  import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../shared-api.js";
7
7
  import { computeMatchedSize, reconcileArbFills } from "../arb-sizing.js";
8
8
  async function fetchAllRates() {
9
+ // SSOT rule #2: each DEX is independent. allSettled keeps the comparison
10
+ // alive when one DEX errors out, while rejection reasons get logged to
11
+ // stderr (explicit failure, never silent).
9
12
  const [pacRates, hlRates, ltRates] = await Promise.allSettled([
10
13
  fetchPacificaPrices().then(assets => assets.map(p => ({
11
14
  exchange: "pacifica", symbol: p.symbol, fundingRate: p.funding, markPrice: p.mark,
@@ -20,14 +23,32 @@ async function fetchAllRates() {
20
23
  ]);
21
24
  const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
22
25
  const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
23
- return funding.map(fr => ({
24
- exchange: "lighter",
25
- symbol: fr.symbol || symMap.get(fr.marketId) || "",
26
- fundingRate: fr.rate,
27
- markPrice: fr.markPrice || priceMap.get(fr.marketId) || 0,
28
- }));
26
+ const out = [];
27
+ for (const fr of funding) {
28
+ const symbol = fr.symbol || symMap.get(fr.marketId) || "";
29
+ if (!symbol)
30
+ continue;
31
+ // Prefer fr.markPrice; fall back to orderBook last-trade as documented
32
+ // price source preference (NOT an error fallback).
33
+ const directMark = fr.markPrice;
34
+ const fallbackMark = priceMap.get(fr.marketId);
35
+ let markPrice;
36
+ if (directMark !== null && Number.isFinite(directMark) && directMark > 0) {
37
+ markPrice = directMark;
38
+ }
39
+ else if (fallbackMark !== undefined && Number.isFinite(fallbackMark) && fallbackMark > 0) {
40
+ markPrice = fallbackMark;
41
+ }
42
+ // SSOT rule #2: skip rows whose mark price cannot be resolved at all
43
+ // — never publish a 0 mark price downstream.
44
+ if (markPrice === undefined)
45
+ continue;
46
+ out.push({ exchange: "lighter", symbol, fundingRate: fr.rate, markPrice });
47
+ }
48
+ return out;
29
49
  })(),
30
50
  ]);
51
+ logSettledRejections([pacRates, hlRates, ltRates], ["pacifica", "hyperliquid", "lighter"], "funding-arb");
31
52
  return [
32
53
  ...(pacRates.status === "fulfilled" ? pacRates.value : []),
33
54
  ...(hlRates.status === "fulfilled" ? hlRates.value : []),
@@ -141,7 +162,7 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
141
162
  for (const move of plan.moves) {
142
163
  log(`[ARB] $${move.amount} ${move.from} → ${move.to}`);
143
164
  }
144
- log(`[ARB] Run 'perp rebalance execute' to rebalance.`);
165
+ log(`[ARB] Run 'perp funds rebalance execute' to rebalance.`);
145
166
  }
146
167
  }
147
168
  }
@@ -10,13 +10,26 @@ export async function validateTrade(adapter, params) {
10
10
  const checks = [];
11
11
  const warnings = [];
12
12
  const sym = params.symbol.toUpperCase();
13
- // Fetch market data, balance, positions, orderbook in parallel
13
+ // Fetch market data, balance, positions, orderbook in parallel.
14
+ // SSOT rule #2: errors must propagate, never silently substitute defaults.
15
+ // getMarkets retains a tracked-error wrapper so the failed-load case surfaces
16
+ // a structured symbol_valid check (not a generic exception).
17
+ let marketsError = null;
14
18
  const [markets, balance, positions, orderbook] = await Promise.all([
15
- adapter.getMarkets().catch(() => []),
16
- adapter.getBalance().catch(() => ({ equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" })),
17
- adapter.getPositions().catch(() => []),
18
- params.type !== "limit" ? adapter.getOrderbook(sym).catch(() => ({ bids: [], asks: [] })) : Promise.resolve({ bids: [], asks: [] }),
19
+ adapter.getMarkets().catch((e) => {
20
+ marketsError = e instanceof Error ? e.message : String(e);
21
+ return [];
22
+ }),
23
+ adapter.getBalance(),
24
+ adapter.getPositions(),
25
+ params.type !== "limit"
26
+ ? adapter.getOrderbook(sym)
27
+ : Promise.resolve({ bids: [], asks: [] }),
19
28
  ]);
29
+ if (markets.length === 0 && marketsError) {
30
+ checks.push({ check: "symbol_valid", passed: false, message: `Failed to load markets from ${adapter.name}: ${marketsError}` });
31
+ return { valid: false, checks, warnings, timestamp: new Date().toISOString() };
32
+ }
20
33
  // 1. Symbol validity (handle BTC vs BTC-PERP suffix variants)
21
34
  const market = markets.find(m => {
22
35
  const ms = m.symbol.toUpperCase();
@@ -30,9 +43,21 @@ export async function validateTrade(adapter, params) {
30
43
  return { valid: false, checks, warnings, timestamp: new Date().toISOString() };
31
44
  }
32
45
  const markPrice = Number(market.markPrice);
46
+ if (params.type === "limit" && (params.price === undefined || !Number.isFinite(params.price))) {
47
+ throw new Error(`Limit order requires explicit price (received ${params.price}); refusing silent mark-price substitution.`);
48
+ }
49
+ if (markPrice <= 0 && params.price === undefined) {
50
+ throw new Error(`No mark price available for ${sym} on ${adapter.name} and no override price supplied; cannot validate trade.`);
51
+ }
33
52
  const price = params.price ?? markPrice;
34
53
  const notional = params.size * price;
54
+ if (params.leverage !== undefined && (!Number.isFinite(params.leverage) || params.leverage <= 0)) {
55
+ throw new Error(`Leverage must be a positive number (received ${params.leverage}).`);
56
+ }
35
57
  const leverage = params.leverage ?? market.maxLeverage;
58
+ if (!Number.isFinite(leverage) || leverage <= 0) {
59
+ throw new Error(`No usable leverage for ${sym} (resolved=${leverage}); supply params.leverage explicitly.`);
60
+ }
36
61
  const marginRequired = notional / leverage;
37
62
  // 2. Balance check
38
63
  const available = Number(balance.available);
package/dist/utils.d.ts CHANGED
@@ -38,3 +38,13 @@ export declare function jsonError(code: string, message: string, meta?: Partial<
38
38
  * In JSON mode, errors are returned as JSON instead of crashing.
39
39
  */
40
40
  export declare function withJsonErrors<T>(isJson: boolean, fn: () => Promise<T>, exchange?: string): Promise<T | undefined>;
41
+ /**
42
+ * Log each rejected outcome from Promise.allSettled to stderr with a
43
+ * per-source label. SSOT rule #2 helper: turn a quiet rejection inside a
44
+ * multi-DEX comparison into an explicit one-line failure on stderr.
45
+ *
46
+ * @param settled Outcomes from Promise.allSettled.
47
+ * @param labels Source label per index (e.g. ["pacifica", "hyperliquid", ...]).
48
+ * @param prefix Bracketed prefix prepended to each line (e.g. "arb-auto").
49
+ */
50
+ export declare function logSettledRejections(settled: PromiseSettledResult<unknown>[], labels: string[], prefix: string): void;
package/dist/utils.js CHANGED
@@ -221,3 +221,22 @@ export async function withJsonErrors(isJson, fn, exchange) {
221
221
  return undefined;
222
222
  }
223
223
  }
224
+ /**
225
+ * Log each rejected outcome from Promise.allSettled to stderr with a
226
+ * per-source label. SSOT rule #2 helper: turn a quiet rejection inside a
227
+ * multi-DEX comparison into an explicit one-line failure on stderr.
228
+ *
229
+ * @param settled Outcomes from Promise.allSettled.
230
+ * @param labels Source label per index (e.g. ["pacifica", "hyperliquid", ...]).
231
+ * @param prefix Bracketed prefix prepended to each line (e.g. "arb-auto").
232
+ */
233
+ export function logSettledRejections(settled, labels, prefix) {
234
+ for (let i = 0; i < settled.length; i++) {
235
+ const r = settled[i];
236
+ if (r.status === "rejected") {
237
+ const reason = r.reason instanceof Error ? r.reason.message : String(r.reason);
238
+ // eslint-disable-next-line no-console
239
+ console.error(`[${prefix}] ${labels[i] ?? `index-${i}`} fetch failed: ${reason}`);
240
+ }
241
+ }
242
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.9.8",
3
+ "version": "0.12.0",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
7
- "perp-mcp": "dist/mcp-server.js"
7
+ "perp-mcp": "dist/mcp-server.js",
8
+ "perp-guardrail": "dist/guardrail/perp-guardrail.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",
@@ -45,6 +46,7 @@
45
46
  "dependencies": {
46
47
  "@modelcontextprotocol/sdk": "^1.27.1",
47
48
  "@msgpack/msgpack": "^3.1.3",
49
+ "@open-wallet-standard/core": "1.3.2",
48
50
  "@solana/web3.js": "^1.98.0",
49
51
  "@types/react": "^19.2.14",
50
52
  "bs58": "^6.0.0",
@@ -99,20 +99,30 @@ perp --json arb close ETH # sells spot + buys back perp
99
99
 
100
100
  Spot exchanges: HL, LT. Pacifica and Aster are perp-only.
101
101
 
102
- ## Bot Engine (19 Strategies)
102
+ ## Strategy Engine (19 bot algorithms + nested scripted plans)
103
103
 
104
104
  ```bash
105
- perp bot list-strategies # list all 19 strategies
106
- perp bot run <strategy> [symbol] # run any registered strategy
107
- perp bot run funding-auto # multi-exchange funding arb
108
- perp bot apex [symbol] # APEX orchestrator (Radar+Pulse+Guard)
109
- perp bot reflect # performance analysis (win rate, fees, PnL)
110
- perp bot preset-list # list strategy presets
111
- perp bot preset <name> [symbol] # run from preset
112
- perp bot start <config.yaml> # start from YAML/JSON config
113
- perp bot quick-grid <symbol> # quick grid bot
114
- perp bot quick-dca <symbol> <side> <amount> <interval>
115
- perp bot quick-arb # quick funding arb
105
+ perp strategy list-strategies # list all 19 strategies
106
+ perp strategy run <strategy> [symbol] # run any registered strategy
107
+ perp strategy run funding-auto # multi-exchange funding arb
108
+ perp strategy apex [symbol] # APEX orchestrator (Radar+Pulse+Guard)
109
+ perp strategy reflect # performance analysis (win rate, fees, PnL)
110
+ perp strategy preset-list # list strategy presets
111
+ perp strategy preset <name> [symbol] # run from preset
112
+ perp strategy start <config.yaml> # start from YAML/JSON config
113
+ perp strategy quick-grid <symbol> # quick grid bot
114
+ perp strategy quick-dca <symbol> <side> <amount> <interval>
115
+ perp strategy quick-arb # quick funding arb
116
+
117
+ # Scripted plans (one-shot multi-step execution)
118
+ perp strategy plan example # show plan format
119
+ perp --json strategy plan validate <file> # validate before run
120
+ perp --json strategy plan execute <file> --dry-run # dry-run plan
121
+
122
+ # Background process supervisor (tmux)
123
+ perp background list # list running jobs
124
+ perp background stop <id> # stop a job
125
+ perp background logs <id> -f # follow logs
116
126
  ```
117
127
 
118
128
  **Strategies:** grid, dca, funding-arb, funding-arb-v2, funding-auto, basis-arb, simple-mm, engine-mm, avellaneda-mm, regime-mm, grid-mm, liquidation-mm, momentum-breakout, mean-reversion, aggressive-taker, hedge-agent, rfq-agent, claude-agent, apex
@@ -136,7 +146,6 @@ perp --json -e <EX> trade cancel <SYM> # cancel all orders for
136
146
  ## Agent Tools
137
147
 
138
148
  ```bash
139
- perp --json agent schema # discover all commands (don't guess)
140
149
  perp --json -e <EX> trade check <SYM> <SIDE> <SIZE> # pre-validate trade
141
150
  perp --json portfolio # balances + positions + risk level
142
151
  perp --json --fields totalEquity,positions portfolio # filtered output
@@ -172,7 +181,7 @@ Response format: `{ "ok": true, "data": {...} }` or `{ "ok": false, "error": { "
172
181
  | `SYMBOL_NOT_FOUND` | `market list` to verify symbol |
173
182
  | `SIZE_TOO_SMALL` | `market info <SYM>` for min size |
174
183
  | `DUPLICATE_ORDER` | already submitted — check positions |
175
- | Lighter `invalid signature` | `manage setup-api-key` to regenerate |
184
+ | Lighter `invalid signature` | `perp wallet agent approve lighter` to regenerate |
176
185
 
177
186
  ## Examples
178
187
 
@@ -81,12 +81,14 @@ perp --json arb scan --min 5
81
81
  ```
82
82
 
83
83
  ### Lighter API Key Setup
84
- Lighter uses a separate API key for trading, but **this is handled automatically**.
85
- When `LIGHTER_PRIVATE_KEY` is set (env var or `wallet set`), the CLI auto-generates and saves the API key on first use.
84
+ Lighter uses a separate API key for trading. There are two paths:
86
85
 
87
- If auto-setup fails (e.g. no ETH for gas on Lighter chain), retry manually:
86
+ 1. **Auto-setup (default)**: When `LIGHTER_PRIVATE_KEY` is set (env var or `wallet set`), the CLI auto-generates and saves the API key (slot 4) on first use.
87
+ 2. **Managed agent (recommended for long-running agents)**: `perp wallet agent approve lighter --master <wallet> --api-key-index <slot>` registers a per-agent slot (4-254) with explicit expiry tracking + 3-tier signer routing.
88
+
89
+ If auto-setup fails (e.g. transient network error), retry manually:
88
90
  ```bash
89
- perp --json -e lighter manage setup-api-key
91
+ perp --json wallet agent approve lighter --master <wallet>
90
92
  ```
91
93
 
92
94
  ### Using the Same EVM Key for Multiple Exchanges