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.
- package/dist/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/commands/agent.js +5 -5
- package/dist/commands/arb/index.js +18 -3
- package/dist/commands/arb-auto.js +242 -63
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/trade.js +89 -35
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- package/dist/exchanges/hyperliquid.js +13 -22
- package/dist/exchanges/lighter.js +4 -2
- package/dist/index.js +1 -1
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- package/dist/smart-order.d.ts +44 -0
- package/dist/smart-order.js +134 -0
- package/dist/trade-validator.js +2 -1
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +10 -1
|
@@ -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
|
+
}
|
package/dist/trade-validator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { symbolMatch } from "./utils.js";
|
|
2
|
-
|
|
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
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -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.
|
|
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).
|