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.
- package/README.md +2 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +12 -9
- package/dist/commands/arb/index.js +18 -3
- package/dist/commands/arb-auto.js +885 -67
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +118 -43
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +181 -92
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +132 -37
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +148 -99
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.d.ts +44 -0
- package/dist/smart-order.js +145 -0
- package/dist/trade-validator.js +2 -1
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +102 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
package/dist/commands/trade.js
CHANGED
|
@@ -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
|
-
|
|
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 } :
|
|
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 } :
|
|
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
|
-
|
|
124
|
-
|
|
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}
|
|
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
|
-
|
|
156
|
-
|
|
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}
|
|
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 <
|
|
227
|
-
.description("Cancel
|
|
228
|
-
.action(async (
|
|
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
|
-
|
|
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
|
|
234
|
-
logExecution({ type: "cancel", exchange: adapter.name, symbol
|
|
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 ${
|
|
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
|
|
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
|
-
.
|
|
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}
|
|
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
|
-
|
|
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
|
-
.
|
|
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}
|
|
751
|
-
|
|
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}
|
|
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
|
-
.
|
|
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}
|
|
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
|
-
|
|
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
|
-
.
|
|
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}
|
|
820
|
-
|
|
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}
|
|
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 ──
|
package/dist/commands/wallet.js
CHANGED
|
@@ -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 =
|
|
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(
|
|
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[];
|