perp-cli 0.4.0 → 0.4.2

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Smart Order Execution — best bid/ask + 1 tick pricing.
3
+ *
4
+ * Instead of `marketOrder()` which can sweep deep into the book (Hyperliquid
5
+ * DEX uses 5% slippage by default), this fetches the orderbook and places an
6
+ * order at exactly the best available price plus one tick.
7
+ *
8
+ * For buys: limit price = best ask + 1 tick (fills at best ask or better)
9
+ * For sells: limit price = best bid - 1 tick (fills at best bid or better)
10
+ *
11
+ * Uses IOC (Immediate-Or-Cancel) so the order fills instantly at the best
12
+ * level and any unfilled remainder is cancelled — no resting orders left.
13
+ *
14
+ * Benefits:
15
+ * - Fills only at the top-of-book price (no multi-level sweep)
16
+ * - Saves 5-50+ bps vs raw market orders on thin books
17
+ * - Predictable execution price
18
+ *
19
+ * Fallback: if the IOC limit fails, falls back to market order.
20
+ */
21
+ import type { ExchangeAdapter } from "./exchanges/interface.js";
22
+ export interface SmartOrderOpts {
23
+ /** Extra ticks of tolerance beyond best price. Default: 1 */
24
+ tickTolerance?: number;
25
+ /** Fall back to market order if IOC limit fails. Default: true */
26
+ fallback?: boolean;
27
+ /** Reduce only flag for closing positions. Default: false */
28
+ reduceOnly?: boolean;
29
+ }
30
+ export interface SmartOrderResult {
31
+ result: unknown;
32
+ method: "limit_ioc" | "market_fallback";
33
+ /** The actual limit price used for the order */
34
+ price: string;
35
+ /** The best price from the orderbook */
36
+ bestBookPrice: string;
37
+ /** Tick size inferred from orderbook */
38
+ tickSize: string;
39
+ }
40
+ /**
41
+ * Execute a smart order at best bid/ask + 1 tick.
42
+ * Drop-in replacement for `adapter.marketOrder()`.
43
+ */
44
+ export declare function smartOrder(adapter: ExchangeAdapter, symbol: string, side: "buy" | "sell", size: string, opts?: SmartOrderOpts): Promise<SmartOrderResult>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Smart Order Execution — best bid/ask + 1 tick pricing.
3
+ *
4
+ * Instead of `marketOrder()` which can sweep deep into the book (Hyperliquid
5
+ * DEX uses 5% slippage by default), this fetches the orderbook and places an
6
+ * order at exactly the best available price plus one tick.
7
+ *
8
+ * For buys: limit price = best ask + 1 tick (fills at best ask or better)
9
+ * For sells: limit price = best bid - 1 tick (fills at best bid or better)
10
+ *
11
+ * Uses IOC (Immediate-Or-Cancel) so the order fills instantly at the best
12
+ * level and any unfilled remainder is cancelled — no resting orders left.
13
+ *
14
+ * Benefits:
15
+ * - Fills only at the top-of-book price (no multi-level sweep)
16
+ * - Saves 5-50+ bps vs raw market orders on thin books
17
+ * - Predictable execution price
18
+ *
19
+ * Fallback: if the IOC limit fails, falls back to market order.
20
+ */
21
+ /**
22
+ * Infer tick size from orderbook prices.
23
+ * Uses the minimum price increment visible in the book.
24
+ */
25
+ function inferTickSize(levels) {
26
+ if (levels.length < 2) {
27
+ // Single level: infer from decimal places (e.g., "1.234" → tick = 0.001)
28
+ const price = levels[0]?.[0] ?? "1";
29
+ const decimals = price.includes(".") ? price.split(".")[1].length : 0;
30
+ return decimals > 0 ? Math.pow(10, -decimals) : 1;
31
+ }
32
+ // Use minimum difference between adjacent price levels
33
+ let minDiff = Infinity;
34
+ for (let i = 1; i < Math.min(levels.length, 5); i++) {
35
+ const diff = Math.abs(Number(levels[i][0]) - Number(levels[i - 1][0]));
36
+ if (diff > 0 && diff < minDiff)
37
+ minDiff = diff;
38
+ }
39
+ return minDiff === Infinity
40
+ ? Math.pow(10, -(levels[0][0].includes(".") ? levels[0][0].split(".")[1].length : 0))
41
+ : minDiff;
42
+ }
43
+ /**
44
+ * Get price precision (decimal places) from a price string.
45
+ */
46
+ function pricePrecision(priceStr) {
47
+ return priceStr.includes(".") ? priceStr.split(".")[1].length : 0;
48
+ }
49
+ /**
50
+ * Check if an order response contains an embedded error (not thrown).
51
+ * Some exchanges (e.g., Hyperliquid) return success at HTTP level but include
52
+ * error statuses inside the response body.
53
+ */
54
+ function hasOrderError(result) {
55
+ if (!result || typeof result !== "object")
56
+ return false;
57
+ const r = result;
58
+ // Hyperliquid: { response: { data: { statuses: [{ error: "..." }] } } }
59
+ const resp = r.response;
60
+ const data = resp?.data;
61
+ const statuses = data?.statuses;
62
+ if (Array.isArray(statuses) && statuses.length > 0 && statuses[0].error)
63
+ return true;
64
+ // Generic: { ok: false } or { success: false }
65
+ if (r.ok === false || r.success === false)
66
+ return true;
67
+ return false;
68
+ }
69
+ /**
70
+ * Execute a smart order at best bid/ask + 1 tick.
71
+ * Drop-in replacement for `adapter.marketOrder()`.
72
+ */
73
+ export async function smartOrder(adapter, symbol, side, size, opts = {}) {
74
+ const { tickTolerance = 1, fallback = true, reduceOnly = false } = opts;
75
+ // 1. Fetch orderbook
76
+ const book = await adapter.getOrderbook(symbol);
77
+ // 2. Validate book has liquidity on the relevant side
78
+ const relevantSide = side === "buy" ? book.asks : book.bids;
79
+ if (relevantSide.length === 0) {
80
+ if (!fallback)
81
+ throw new Error(`No ${side === "buy" ? "asks" : "bids"} in orderbook for ${symbol}`);
82
+ const result = await adapter.marketOrder(symbol, side, size);
83
+ return { result, method: "market_fallback", price: "0", bestBookPrice: "0", tickSize: "0" };
84
+ }
85
+ // 3. Get best price and tick size
86
+ const bestEntry = relevantSide[0];
87
+ const bestPrice = Number(bestEntry[0]);
88
+ const tick = inferTickSize(relevantSide);
89
+ const decimals = pricePrecision(bestEntry[0]);
90
+ // 4. Calculate limit price: best price + N ticks in favorable direction
91
+ const limitPrice = side === "buy"
92
+ ? bestPrice + tick * tickTolerance // 1 tick above best ask
93
+ : bestPrice - tick * tickTolerance; // 1 tick below best bid
94
+ const formattedPrice = limitPrice.toFixed(decimals);
95
+ // 5. Place IOC limit order at calculated price
96
+ try {
97
+ const result = await adapter.limitOrder(symbol, side, formattedPrice, size, {
98
+ tif: "IOC",
99
+ reduceOnly,
100
+ });
101
+ // Check for exchange-level rejection inside a "success" response
102
+ // (e.g., Hyperliquid returns { status: "ok", response: { data: { statuses: [{ error: "..." }] } } })
103
+ if (fallback && hasOrderError(result)) {
104
+ const fallbackResult = await adapter.marketOrder(symbol, side, size);
105
+ return {
106
+ result: fallbackResult,
107
+ method: "market_fallback",
108
+ price: formattedPrice,
109
+ bestBookPrice: bestEntry[0],
110
+ tickSize: tick.toFixed(decimals),
111
+ };
112
+ }
113
+ return {
114
+ result,
115
+ method: "limit_ioc",
116
+ price: formattedPrice,
117
+ bestBookPrice: bestEntry[0],
118
+ tickSize: tick.toFixed(decimals),
119
+ };
120
+ }
121
+ catch (err) {
122
+ if (!fallback)
123
+ throw err;
124
+ // Fallback to raw market order for reliability
125
+ const result = await adapter.marketOrder(symbol, side, size);
126
+ return {
127
+ result,
128
+ method: "market_fallback",
129
+ price: formattedPrice,
130
+ bestBookPrice: bestEntry[0],
131
+ tickSize: tick.toFixed(decimals),
132
+ };
133
+ }
134
+ }
@@ -1,5 +1,6 @@
1
1
  import { symbolMatch } from "./utils.js";
2
- const DEFAULT_TAKER_FEE = 0.0005; // 0.05%
2
+ import { DEFAULT_TAKER_FEE } from "./constants.js";
3
+ // Use centralized DEFAULT_TAKER_FEE from constants.ts (0.035%)
3
4
  const DEFAULT_MAX_SLIPPAGE = 0.005; // 0.5%
4
5
  /**
5
6
  * Validate a trade before execution.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -5,7 +5,7 @@ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
5
5
  license: MIT
6
6
  metadata:
7
7
  author: hypurrquant
8
- version: "0.4.0"
8
+ version: "0.4.2"
9
9
  ---
10
10
 
11
11
  # perp-cli Agent Guide
@@ -59,7 +59,9 @@ perp --json arb exec <SYM> <longEx> <shortEx> <$> --leverage <N> --isolated # e
59
59
 
60
60
  # Single exchange trading (when not doing arb)
61
61
  perp --json -e <EX> trade market <SYM> buy <SIZE>
62
+ perp --json -e <EX> trade market <SYM> buy <SIZE> --smart # IOC limit at best ask + 1 tick (less slippage)
62
63
  perp --json -e <EX> trade close <SYM>
64
+ perp --json -e <EX> trade close <SYM> --smart # smart close at best bid/ask
63
65
 
64
66
  # Risk (only if you need to set limits — portfolio already includes risk)
65
67
  perp --json risk limits --max-leverage 5
@@ -87,6 +89,13 @@ perp --json --ndjson -e <EX> market list
87
89
 
88
90
  # Prevent duplicate orders with client ID
89
91
  perp --json -e <EX> trade market <SYM> buy <SIZE> --client-id <UNIQUE_ID>
92
+
93
+ # Smart order: IOC limit at best bid/ask + 1 tick instead of raw market order
94
+ # Fills only at top-of-book price — no multi-level sweep slippage
95
+ perp --json -e <EX> trade market <SYM> buy <SIZE> --smart
96
+ perp --json -e <EX> trade close <SYM> --smart
97
+ perp --json arb exec <SYM> <longEx> <shortEx> <$> --smart # smart arb entry
98
+ perp --json arb close <SYM> --smart # smart arb close
90
99
  ```
91
100
 
92
101
  All string outputs are auto-sanitized (control chars stripped, prompt injection patterns blocked).