perp-cli 0.3.3
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
import { PacificaAdapter } from "../exchanges/pacifica.js";
|
|
2
|
+
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
3
|
+
import { LighterAdapter } from "../exchanges/lighter.js";
|
|
4
|
+
import { printJson, errorAndExit, withJsonErrors, jsonOk, jsonError, symbolMatch, formatUsd } from "../utils.js";
|
|
5
|
+
import { logExecution } from "../execution-log.js";
|
|
6
|
+
import { validateTrade } from "../trade-validator.js";
|
|
7
|
+
import { generateClientId, logClientId, isOrderDuplicate } from "../client-id-tracker.js";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
function pac(adapter) {
|
|
10
|
+
if (!(adapter instanceof PacificaAdapter))
|
|
11
|
+
throw new Error("This command requires --exchange pacifica");
|
|
12
|
+
return adapter;
|
|
13
|
+
}
|
|
14
|
+
export function registerTradeCommands(program, getAdapter, isJson, isDryRun = () => false) {
|
|
15
|
+
/** Guard: if --dry-run is active, log the intended action and return without executing. */
|
|
16
|
+
function dryRunGuard(action, details) {
|
|
17
|
+
if (!isDryRun())
|
|
18
|
+
return false;
|
|
19
|
+
const info = { dryRun: true, action, ...details, timestamp: new Date().toISOString() };
|
|
20
|
+
if (isJson()) {
|
|
21
|
+
printJson(jsonOk(info));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(chalk.yellow(`\n [DRY RUN] Would ${action}:`));
|
|
25
|
+
for (const [k, v] of Object.entries(details)) {
|
|
26
|
+
console.log(chalk.gray(` ${k}: ${v}`));
|
|
27
|
+
}
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
logExecution({
|
|
31
|
+
type: action.includes("limit") ? "limit_order" : action.includes("stop") ? "stop_order" : action.includes("cancel") ? "cancel" : "market_order",
|
|
32
|
+
exchange: details.exchange ?? "unknown",
|
|
33
|
+
symbol: (details.symbol ?? "").toUpperCase(),
|
|
34
|
+
side: details.side ?? "",
|
|
35
|
+
size: String(details.size ?? "0"),
|
|
36
|
+
price: details.price,
|
|
37
|
+
status: "simulated",
|
|
38
|
+
dryRun: true,
|
|
39
|
+
});
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const trade = program.command("trade").description("Trading commands");
|
|
43
|
+
// === Generic commands (all exchanges) ===
|
|
44
|
+
trade
|
|
45
|
+
.command("market <symbol> <side> <size>")
|
|
46
|
+
.description("Place a market order (side: buy/sell)")
|
|
47
|
+
.option("-s, --slippage <pct>", "Slippage percent", "1")
|
|
48
|
+
.option("--reduce-only", "Reduce only order")
|
|
49
|
+
.option("--client-id <id>", "Client order ID for idempotent tracking")
|
|
50
|
+
.option("--auto-id", "Auto-generate a client order ID")
|
|
51
|
+
.action(async (symbol, side, size, opts) => {
|
|
52
|
+
const s = side.toLowerCase();
|
|
53
|
+
if (s !== "buy" && s !== "sell")
|
|
54
|
+
errorAndExit("Side must be buy or sell");
|
|
55
|
+
const clientId = opts.autoId ? generateClientId() : opts.clientId;
|
|
56
|
+
if (clientId && isOrderDuplicate(clientId)) {
|
|
57
|
+
if (isJson())
|
|
58
|
+
return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
|
|
59
|
+
console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const adapter = await getAdapter();
|
|
63
|
+
if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size }))
|
|
64
|
+
return;
|
|
65
|
+
if (clientId) {
|
|
66
|
+
logClientId({
|
|
67
|
+
clientOrderId: clientId, exchange: adapter.name,
|
|
68
|
+
symbol: symbol.toUpperCase(), side: s, size, type: "market",
|
|
69
|
+
status: "pending", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = await adapter.marketOrder(symbol.toUpperCase(), s, size);
|
|
75
|
+
logExecution({
|
|
76
|
+
type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
|
|
77
|
+
side: s, size, status: "success", dryRun: false,
|
|
78
|
+
meta: clientId ? { clientOrderId: clientId } : undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
logExecution({
|
|
83
|
+
type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
|
|
84
|
+
side: s, size, status: "failed", dryRun: false,
|
|
85
|
+
error: err instanceof Error ? err.message : String(err),
|
|
86
|
+
meta: clientId ? { clientOrderId: clientId } : undefined,
|
|
87
|
+
});
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
if (clientId) {
|
|
91
|
+
logClientId({
|
|
92
|
+
clientOrderId: clientId, exchange: adapter.name,
|
|
93
|
+
symbol: symbol.toUpperCase(), side: s, size, type: "market",
|
|
94
|
+
status: "submitted", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (isJson())
|
|
98
|
+
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`));
|
|
100
|
+
printJson(jsonOk(result));
|
|
101
|
+
});
|
|
102
|
+
// Shortcuts: trade buy / trade sell
|
|
103
|
+
trade
|
|
104
|
+
.command("buy <symbol> <size>")
|
|
105
|
+
.description("Market buy (shortcut for: trade market <symbol> buy <size>)")
|
|
106
|
+
.option("-s, --slippage <pct>", "Slippage percent", "1")
|
|
107
|
+
.option("--reduce-only", "Reduce only order")
|
|
108
|
+
.option("--client-id <id>", "Client order ID")
|
|
109
|
+
.option("--auto-id", "Auto-generate client order ID")
|
|
110
|
+
.action(async (symbol, size, opts) => {
|
|
111
|
+
const clientId = opts.autoId ? generateClientId() : opts.clientId;
|
|
112
|
+
if (clientId && isOrderDuplicate(clientId)) {
|
|
113
|
+
if (isJson())
|
|
114
|
+
return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
|
|
115
|
+
console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const adapter = await getAdapter();
|
|
119
|
+
if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size }))
|
|
120
|
+
return;
|
|
121
|
+
let result;
|
|
122
|
+
try {
|
|
123
|
+
result = await adapter.marketOrder(symbol.toUpperCase(), "buy", size);
|
|
124
|
+
logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "buy", size, status: "success", dryRun: false });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
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) });
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
if (isJson())
|
|
131
|
+
return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
|
|
132
|
+
console.log(chalk.green(`\n Market BUY ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.\n`));
|
|
133
|
+
printJson(jsonOk(result));
|
|
134
|
+
});
|
|
135
|
+
trade
|
|
136
|
+
.command("sell <symbol> <size>")
|
|
137
|
+
.description("Market sell (shortcut for: trade market <symbol> sell <size>)")
|
|
138
|
+
.option("-s, --slippage <pct>", "Slippage percent", "1")
|
|
139
|
+
.option("--reduce-only", "Reduce only order")
|
|
140
|
+
.option("--client-id <id>", "Client order ID")
|
|
141
|
+
.option("--auto-id", "Auto-generate client order ID")
|
|
142
|
+
.action(async (symbol, size, opts) => {
|
|
143
|
+
const clientId = opts.autoId ? generateClientId() : opts.clientId;
|
|
144
|
+
if (clientId && isOrderDuplicate(clientId)) {
|
|
145
|
+
if (isJson())
|
|
146
|
+
return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
|
|
147
|
+
console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const adapter = await getAdapter();
|
|
151
|
+
if (dryRunGuard("market_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size }))
|
|
152
|
+
return;
|
|
153
|
+
let result;
|
|
154
|
+
try {
|
|
155
|
+
result = await adapter.marketOrder(symbol.toUpperCase(), "sell", size);
|
|
156
|
+
logExecution({ type: "market_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "sell", size, status: "success", dryRun: false });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
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) });
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
if (isJson())
|
|
163
|
+
return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
|
|
164
|
+
console.log(chalk.green(`\n Market SELL ${size} ${symbol.toUpperCase()} placed on ${adapter.name}.\n`));
|
|
165
|
+
printJson(jsonOk(result));
|
|
166
|
+
});
|
|
167
|
+
trade
|
|
168
|
+
.command("limit <symbol> <side> <price> <size>")
|
|
169
|
+
.description("Place a limit order")
|
|
170
|
+
.option("--tif <tif>", "Time in force: GTC, IOC, ALO, TOB", "GTC")
|
|
171
|
+
.option("--reduce-only", "Reduce only order")
|
|
172
|
+
.option("--client-id <id>", "Client order ID for idempotent tracking")
|
|
173
|
+
.option("--auto-id", "Auto-generate a client order ID")
|
|
174
|
+
.action(async (symbol, side, price, size, opts) => {
|
|
175
|
+
const s = side.toLowerCase();
|
|
176
|
+
if (s !== "buy" && s !== "sell")
|
|
177
|
+
errorAndExit("Side must be buy or sell");
|
|
178
|
+
const clientId = opts.autoId ? generateClientId() : opts.clientId;
|
|
179
|
+
if (clientId && isOrderDuplicate(clientId)) {
|
|
180
|
+
if (isJson())
|
|
181
|
+
return printJson(jsonOk({ duplicate: true, clientOrderId: clientId, message: "Order already submitted" }));
|
|
182
|
+
console.log(chalk.yellow(`\n Duplicate order detected (clientId: ${clientId}). Skipping.\n`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const adapter = await getAdapter();
|
|
186
|
+
if (dryRunGuard("limit_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price }))
|
|
187
|
+
return;
|
|
188
|
+
if (clientId) {
|
|
189
|
+
logClientId({
|
|
190
|
+
clientOrderId: clientId, exchange: adapter.name,
|
|
191
|
+
symbol: symbol.toUpperCase(), side: s, size, type: "limit",
|
|
192
|
+
status: "pending", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
let result;
|
|
196
|
+
try {
|
|
197
|
+
result = await adapter.limitOrder(symbol.toUpperCase(), s, price, size);
|
|
198
|
+
logExecution({
|
|
199
|
+
type: "limit_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
|
|
200
|
+
side: s, size, price, status: "success", dryRun: false,
|
|
201
|
+
meta: clientId ? { clientOrderId: clientId } : undefined,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
logExecution({
|
|
206
|
+
type: "limit_order", exchange: adapter.name, symbol: symbol.toUpperCase(),
|
|
207
|
+
side: s, size, price, status: "failed", dryRun: false,
|
|
208
|
+
error: err instanceof Error ? err.message : String(err),
|
|
209
|
+
meta: clientId ? { clientOrderId: clientId } : undefined,
|
|
210
|
+
});
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
if (clientId) {
|
|
214
|
+
logClientId({
|
|
215
|
+
clientOrderId: clientId, exchange: adapter.name,
|
|
216
|
+
symbol: symbol.toUpperCase(), side: s, size, type: "limit",
|
|
217
|
+
status: "submitted", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (isJson())
|
|
221
|
+
return printJson(jsonOk(clientId ? { ...result, clientOrderId: clientId } : result));
|
|
222
|
+
console.log(chalk.green(`\n Limit ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} @ $${price} placed on ${adapter.name}.${clientId ? ` (id: ${clientId})` : ""}\n`));
|
|
223
|
+
printJson(jsonOk(result));
|
|
224
|
+
});
|
|
225
|
+
trade
|
|
226
|
+
.command("cancel <symbol> <orderId>")
|
|
227
|
+
.description("Cancel a specific order")
|
|
228
|
+
.action(async (symbol, orderId) => {
|
|
229
|
+
const adapter = await getAdapter();
|
|
230
|
+
if (dryRunGuard("cancel", { exchange: adapter.name, symbol: symbol.toUpperCase(), orderId }))
|
|
231
|
+
return;
|
|
232
|
+
try {
|
|
233
|
+
const result = await adapter.cancelOrder(symbol.toUpperCase(), orderId);
|
|
234
|
+
logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "success", dryRun: false, meta: { orderId } });
|
|
235
|
+
if (isJson())
|
|
236
|
+
return printJson(jsonOk(result));
|
|
237
|
+
console.log(chalk.green(`\n Order ${orderId} cancelled on ${adapter.name}.\n`));
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
logExecution({ type: "cancel", exchange: adapter.name, symbol: symbol.toUpperCase(), side: "cancel", size: "0", status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { orderId } });
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
trade
|
|
245
|
+
.command("cancel-all")
|
|
246
|
+
.description("Cancel all open orders")
|
|
247
|
+
.action(async () => {
|
|
248
|
+
const adapter = await getAdapter();
|
|
249
|
+
if (dryRunGuard("cancel_all", { exchange: adapter.name }))
|
|
250
|
+
return;
|
|
251
|
+
const result = await adapter.cancelAllOrders();
|
|
252
|
+
if (isJson())
|
|
253
|
+
return printJson(jsonOk(result));
|
|
254
|
+
console.log(chalk.green(`\n All orders cancelled on ${adapter.name}.\n`));
|
|
255
|
+
});
|
|
256
|
+
// === TWAP — Pacifica (seconds) + Hyperliquid (minutes) ===
|
|
257
|
+
trade
|
|
258
|
+
.command("twap <symbol> <side> <size> <duration>")
|
|
259
|
+
.description("Place a TWAP order (Pacifica: seconds, HL: minutes, Lighter/any: client-side via --background)")
|
|
260
|
+
.option("-s, --slippage <pct>", "Slippage percent", "1")
|
|
261
|
+
.option("--reduce-only", "Reduce only order")
|
|
262
|
+
.option("--background", "Run client-side TWAP in background (tmux) — works on all exchanges")
|
|
263
|
+
.option("--slices <n>", "Number of slices for client-side TWAP")
|
|
264
|
+
.action(async (symbol, side, size, duration, opts) => {
|
|
265
|
+
const s = side.toLowerCase();
|
|
266
|
+
if (s !== "buy" && s !== "sell")
|
|
267
|
+
errorAndExit("Side must be buy or sell");
|
|
268
|
+
// --background → client-side TWAP via tmux job
|
|
269
|
+
if (opts.background) {
|
|
270
|
+
const { startJob } = await import("../jobs.js");
|
|
271
|
+
const exchange = (await getAdapter()).name;
|
|
272
|
+
const cliArgs = [
|
|
273
|
+
symbol.toUpperCase(), s, size, duration,
|
|
274
|
+
...(opts.slices ? ["--slices", opts.slices] : []),
|
|
275
|
+
];
|
|
276
|
+
// Pass exchange flag through
|
|
277
|
+
const job = startJob({
|
|
278
|
+
strategy: "twap",
|
|
279
|
+
exchange,
|
|
280
|
+
params: { symbol: symbol.toUpperCase(), side: s, size, duration, slices: opts.slices },
|
|
281
|
+
cliArgs: [`-e`, exchange, ...cliArgs],
|
|
282
|
+
});
|
|
283
|
+
if (isJson())
|
|
284
|
+
return printJson(jsonOk(job));
|
|
285
|
+
console.log(chalk.green(`\n TWAP job started in background.`));
|
|
286
|
+
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
287
|
+
console.log(` Session: ${job.tmuxSession}`);
|
|
288
|
+
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
289
|
+
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const adapter = await getAdapter();
|
|
293
|
+
// Lighter or any exchange without native TWAP → client-side TWAP (foreground)
|
|
294
|
+
if (adapter instanceof LighterAdapter) {
|
|
295
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
296
|
+
const result = await runTWAP(adapter, {
|
|
297
|
+
symbol: symbol.toUpperCase(),
|
|
298
|
+
side: s,
|
|
299
|
+
totalSize: parseFloat(size),
|
|
300
|
+
durationSec: parseInt(duration),
|
|
301
|
+
slices: opts.slices ? parseInt(opts.slices) : undefined,
|
|
302
|
+
});
|
|
303
|
+
if (isJson())
|
|
304
|
+
return printJson(jsonOk(result));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let result;
|
|
308
|
+
if (adapter instanceof PacificaAdapter) {
|
|
309
|
+
result = await adapter.sdk.createTWAP({
|
|
310
|
+
symbol: symbol.toUpperCase(),
|
|
311
|
+
amount: size,
|
|
312
|
+
side: s === "buy" ? "bid" : "ask",
|
|
313
|
+
slippage_percent: opts.slippage,
|
|
314
|
+
reduce_only: opts.reduceOnly ?? false,
|
|
315
|
+
duration_in_seconds: parseInt(duration),
|
|
316
|
+
}, adapter.publicKey, adapter.signer);
|
|
317
|
+
}
|
|
318
|
+
else if (adapter instanceof HyperliquidAdapter) {
|
|
319
|
+
const minutes = parseInt(duration);
|
|
320
|
+
if (minutes < 5 || minutes > 1440)
|
|
321
|
+
errorAndExit("HL TWAP duration must be 5-1440 minutes");
|
|
322
|
+
result = await adapter.twapOrder(symbol.toUpperCase(), s, size, minutes, { reduceOnly: opts.reduceOnly });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
errorAndExit("TWAP orders require --exchange pacifica, hyperliquid, or lighter");
|
|
326
|
+
}
|
|
327
|
+
if (isJson())
|
|
328
|
+
return printJson(jsonOk(result));
|
|
329
|
+
console.log(chalk.green(`\n TWAP ${s.toUpperCase()} ${size} ${symbol.toUpperCase()} over ${duration} placed on ${adapter.name}.\n`));
|
|
330
|
+
printJson(jsonOk(result));
|
|
331
|
+
});
|
|
332
|
+
// === Stop / Trigger orders — Pacifica + Hyperliquid ===
|
|
333
|
+
trade
|
|
334
|
+
.command("stop <symbol> <side> <stopPrice> <size>")
|
|
335
|
+
.description("Place a stop order")
|
|
336
|
+
.option("--limit-price <price>", "Limit price (makes it stop-limit)")
|
|
337
|
+
.option("--reduce-only", "Reduce only order")
|
|
338
|
+
.action(async (symbol, side, stopPrice, size, opts) => {
|
|
339
|
+
const s = side.toLowerCase();
|
|
340
|
+
if (s !== "buy" && s !== "sell")
|
|
341
|
+
errorAndExit("Side must be buy or sell");
|
|
342
|
+
const adapter = await getAdapter();
|
|
343
|
+
if (dryRunGuard("stop_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price: stopPrice }))
|
|
344
|
+
return;
|
|
345
|
+
let result;
|
|
346
|
+
try {
|
|
347
|
+
result = await adapter.stopOrder(symbol.toUpperCase(), s, size, stopPrice, { limitPrice: opts.limitPrice, reduceOnly: opts.reduceOnly });
|
|
348
|
+
logExecution({ type: "stop_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price: stopPrice, status: "success", dryRun: false });
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
logExecution({ type: "stop_order", exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, size, price: stopPrice, status: "failed", dryRun: false, error: err instanceof Error ? err.message : String(err) });
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
if (isJson())
|
|
355
|
+
return printJson(jsonOk(result));
|
|
356
|
+
console.log(chalk.green(`\n Stop order placed on ${adapter.name}.\n`));
|
|
357
|
+
printJson(jsonOk(result));
|
|
358
|
+
});
|
|
359
|
+
// === TP/SL — Pacifica + Hyperliquid ===
|
|
360
|
+
trade
|
|
361
|
+
.command("tpsl <symbol> <side>")
|
|
362
|
+
.description("Set take-profit / stop-loss on a position")
|
|
363
|
+
.option("--tp <price>", "Take profit trigger price")
|
|
364
|
+
.option("--tp-limit <price>", "Take profit limit price")
|
|
365
|
+
.option("--sl <price>", "Stop loss trigger price")
|
|
366
|
+
.option("--size <size>", "Size (HL only, omit for full position)")
|
|
367
|
+
.action(async (symbol, side, opts) => {
|
|
368
|
+
const s = side.toLowerCase();
|
|
369
|
+
if (s !== "buy" && s !== "sell")
|
|
370
|
+
errorAndExit("Side must be buy or sell");
|
|
371
|
+
if (!opts.tp && !opts.sl)
|
|
372
|
+
errorAndExit("Must specify --tp and/or --sl");
|
|
373
|
+
const adapter = await getAdapter();
|
|
374
|
+
if (dryRunGuard("tpsl", { exchange: adapter.name, symbol: symbol.toUpperCase(), side: s, tp: opts.tp ?? "none", sl: opts.sl ?? "none" }))
|
|
375
|
+
return;
|
|
376
|
+
if (adapter instanceof PacificaAdapter) {
|
|
377
|
+
// TP/SL side is opposite of position: LONG position → "ask" to close
|
|
378
|
+
const params = {
|
|
379
|
+
symbol: symbol.toUpperCase(),
|
|
380
|
+
side: s === "buy" ? "ask" : "bid",
|
|
381
|
+
};
|
|
382
|
+
if (opts.tp)
|
|
383
|
+
params.take_profit = { stop_price: opts.tp, limit_price: opts.tpLimit };
|
|
384
|
+
if (opts.sl)
|
|
385
|
+
params.stop_loss = { stop_price: opts.sl };
|
|
386
|
+
const result = await adapter.sdk.setTPSL(params, adapter.publicKey, adapter.signer);
|
|
387
|
+
if (isJson())
|
|
388
|
+
return printJson(jsonOk(result));
|
|
389
|
+
console.log(chalk.green(`\n TP/SL set for ${symbol.toUpperCase()} on Pacifica.\n`));
|
|
390
|
+
}
|
|
391
|
+
else if (adapter instanceof HyperliquidAdapter) {
|
|
392
|
+
// HL uses trigger orders for TP/SL
|
|
393
|
+
const results = [];
|
|
394
|
+
const posSize = opts.size || "0"; // 0 = full position via positionTpsl grouping
|
|
395
|
+
if (opts.tp) {
|
|
396
|
+
results.push(await adapter.triggerOrder(symbol.toUpperCase(), s === "buy" ? "sell" : "buy", // Close opposite side
|
|
397
|
+
posSize, opts.tp, "tp", { isMarket: !opts.tpLimit, reduceOnly: true, grouping: "positionTpsl" }));
|
|
398
|
+
}
|
|
399
|
+
if (opts.sl) {
|
|
400
|
+
results.push(await adapter.triggerOrder(symbol.toUpperCase(), s === "buy" ? "sell" : "buy", posSize, opts.sl, "sl", { isMarket: true, reduceOnly: true, grouping: "positionTpsl" }));
|
|
401
|
+
}
|
|
402
|
+
if (isJson())
|
|
403
|
+
return printJson(jsonOk(results));
|
|
404
|
+
console.log(chalk.green(`\n TP/SL set for ${symbol.toUpperCase()} on Hyperliquid.\n`));
|
|
405
|
+
}
|
|
406
|
+
else if (adapter instanceof LighterAdapter) {
|
|
407
|
+
// Lighter uses triggerPrice in signCreateOrder for TP/SL
|
|
408
|
+
const closeSide = s === "buy" ? "sell" : "buy"; // Close opposite side
|
|
409
|
+
const results = [];
|
|
410
|
+
if (opts.tp) {
|
|
411
|
+
results.push(await adapter.stopOrder(symbol.toUpperCase(), closeSide, opts.size || "0", opts.tp, { limitPrice: opts.tpLimit, reduceOnly: true }));
|
|
412
|
+
}
|
|
413
|
+
if (opts.sl) {
|
|
414
|
+
results.push(await adapter.stopOrder(symbol.toUpperCase(), closeSide, opts.size || "0", opts.sl, { reduceOnly: true }));
|
|
415
|
+
}
|
|
416
|
+
if (isJson())
|
|
417
|
+
return printJson(jsonOk(results));
|
|
418
|
+
console.log(chalk.green(`\n TP/SL set for ${symbol.toUpperCase()} on Lighter.\n`));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
errorAndExit("TP/SL requires --exchange pacifica, hyperliquid, or lighter");
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
// === Scaled Take-Profit (분할익절) ===
|
|
425
|
+
trade
|
|
426
|
+
.command("scale-tp <symbol>")
|
|
427
|
+
.description("Place multiple take-profit limit orders at different price levels (분할익절)")
|
|
428
|
+
.requiredOption("--levels <levels>", "Comma-separated price:percent pairs (e.g., 72000:25,75000:25,80000:50)")
|
|
429
|
+
.option("--size <size>", "Override total position size (default: uses current position)")
|
|
430
|
+
.action(async (symbol, opts) => {
|
|
431
|
+
const sym = symbol.toUpperCase();
|
|
432
|
+
const adapter = await getAdapter();
|
|
433
|
+
// Parse levels: "72000:25,75000:25,80000:50"
|
|
434
|
+
const levels = opts.levels.split(",").map(l => {
|
|
435
|
+
const [price, pct] = l.trim().split(":");
|
|
436
|
+
if (!price || !pct)
|
|
437
|
+
errorAndExit(`Invalid level format: ${l}. Use price:percent (e.g., 72000:25)`);
|
|
438
|
+
return { price: price.trim(), pct: parseFloat(pct.trim()) };
|
|
439
|
+
});
|
|
440
|
+
// Validate percentages sum to 100
|
|
441
|
+
const totalPct = levels.reduce((s, l) => s + l.pct, 0);
|
|
442
|
+
if (Math.abs(totalPct - 100) > 0.01) {
|
|
443
|
+
errorAndExit(`Percentages must sum to 100%. Got: ${totalPct}%`);
|
|
444
|
+
}
|
|
445
|
+
// Get current position to determine size and side
|
|
446
|
+
let totalSize;
|
|
447
|
+
let closeSide;
|
|
448
|
+
if (opts.size) {
|
|
449
|
+
totalSize = parseFloat(opts.size);
|
|
450
|
+
// Need to know position side — fetch it
|
|
451
|
+
const positions = await adapter.getPositions();
|
|
452
|
+
const pos = positions.find(p => p.symbol.toUpperCase() === sym);
|
|
453
|
+
closeSide = pos?.side === "short" ? "buy" : "sell";
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const positions = await adapter.getPositions();
|
|
457
|
+
const pos = positions.find(p => p.symbol.toUpperCase() === sym);
|
|
458
|
+
if (!pos)
|
|
459
|
+
errorAndExit(`No open position for ${sym}. Use --size to specify manually.`);
|
|
460
|
+
totalSize = parseFloat(pos.size);
|
|
461
|
+
closeSide = pos.side === "long" ? "sell" : "buy";
|
|
462
|
+
}
|
|
463
|
+
if (dryRunGuard("scale_tp", {
|
|
464
|
+
exchange: adapter.name, symbol: sym, side: closeSide,
|
|
465
|
+
totalSize: totalSize.toString(),
|
|
466
|
+
levels: levels.map(l => `${l.price}@${l.pct}%`).join(", "),
|
|
467
|
+
}))
|
|
468
|
+
return;
|
|
469
|
+
// Place reduce-only limit orders at each level
|
|
470
|
+
const results = [];
|
|
471
|
+
for (const level of levels) {
|
|
472
|
+
const levelSize = (totalSize * level.pct / 100).toString();
|
|
473
|
+
try {
|
|
474
|
+
const result = await adapter.limitOrder(sym, closeSide, level.price, levelSize, { reduceOnly: true });
|
|
475
|
+
results.push({ price: level.price, size: levelSize, pct: level.pct, result });
|
|
476
|
+
logExecution({
|
|
477
|
+
type: "limit_order", exchange: adapter.name, symbol: sym,
|
|
478
|
+
side: closeSide, size: levelSize, price: level.price,
|
|
479
|
+
status: "success", dryRun: false,
|
|
480
|
+
meta: { action: "scale-tp", pct: level.pct, reduceOnly: true },
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
485
|
+
logExecution({
|
|
486
|
+
type: "limit_order", exchange: adapter.name, symbol: sym,
|
|
487
|
+
side: closeSide, size: levelSize, price: level.price,
|
|
488
|
+
status: "failed", dryRun: false, error: msg,
|
|
489
|
+
meta: { action: "scale-tp", pct: level.pct },
|
|
490
|
+
});
|
|
491
|
+
if (isJson()) {
|
|
492
|
+
results.push({ price: level.price, size: levelSize, pct: level.pct, result: { error: msg } });
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
console.log(chalk.red(` Failed: ${level.price} x ${levelSize} — ${msg}`));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (isJson())
|
|
500
|
+
return printJson(jsonOk({ symbol: sym, side: closeSide, totalSize, levels: results }));
|
|
501
|
+
console.log(chalk.green(`\n Scaled TP set for ${sym} on ${adapter.name}:\n`));
|
|
502
|
+
for (const r of results) {
|
|
503
|
+
const status = r.result?.error ? chalk.red("FAILED") : chalk.green("OK");
|
|
504
|
+
console.log(` ${status} $${r.price} — ${r.pct}% (${r.size} ${sym})`);
|
|
505
|
+
}
|
|
506
|
+
console.log();
|
|
507
|
+
});
|
|
508
|
+
// === Pacifica-only commands ===
|
|
509
|
+
trade
|
|
510
|
+
.command("edit <symbol> <orderId> <price> <size>")
|
|
511
|
+
.description("Edit an existing order")
|
|
512
|
+
.action(async (symbol, orderId, price, size) => {
|
|
513
|
+
const adapter = await getAdapter();
|
|
514
|
+
if (dryRunGuard("edit_order", { exchange: adapter.name, symbol: symbol.toUpperCase(), orderId, price, size }))
|
|
515
|
+
return;
|
|
516
|
+
const result = await adapter.editOrder(symbol.toUpperCase(), orderId, price, size);
|
|
517
|
+
if (isJson())
|
|
518
|
+
return printJson(jsonOk(result));
|
|
519
|
+
console.log(chalk.green(`\n Order ${orderId} updated to $${price} x ${size} on ${adapter.name}.\n`));
|
|
520
|
+
});
|
|
521
|
+
trade
|
|
522
|
+
.command("cancel-stop <symbol> <stopOrderId>")
|
|
523
|
+
.description("Cancel a stop order (Pacifica)")
|
|
524
|
+
.action(async (symbol, stopOrderId) => {
|
|
525
|
+
const adapter = await getAdapter();
|
|
526
|
+
const p = pac(adapter);
|
|
527
|
+
const result = await p.sdk.cancelStopOrder({ symbol: symbol.toUpperCase(), order_id: Number(stopOrderId) }, p.publicKey, p.signer);
|
|
528
|
+
if (isJson())
|
|
529
|
+
return printJson(jsonOk(result));
|
|
530
|
+
console.log(chalk.green(`\n Stop order ${stopOrderId} cancelled.\n`));
|
|
531
|
+
});
|
|
532
|
+
trade
|
|
533
|
+
.command("cancel-twap <symbol> <twapOrderId>")
|
|
534
|
+
.description("Cancel a TWAP order")
|
|
535
|
+
.action(async (symbol, twapOrderId) => {
|
|
536
|
+
const adapter = await getAdapter();
|
|
537
|
+
let result;
|
|
538
|
+
if (adapter instanceof PacificaAdapter) {
|
|
539
|
+
result = await adapter.sdk.cancelTWAP({ symbol: symbol.toUpperCase(), twap_order_id: Number(twapOrderId) }, adapter.publicKey, adapter.signer);
|
|
540
|
+
}
|
|
541
|
+
else if (adapter instanceof HyperliquidAdapter) {
|
|
542
|
+
result = await adapter.twapCancel(symbol.toUpperCase(), Number(twapOrderId));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
errorAndExit("Cancel TWAP requires --exchange pacifica or hyperliquid");
|
|
546
|
+
}
|
|
547
|
+
if (isJson())
|
|
548
|
+
return printJson(jsonOk(result));
|
|
549
|
+
console.log(chalk.green(`\n TWAP order ${twapOrderId} cancelled on ${adapter.name}.\n`));
|
|
550
|
+
});
|
|
551
|
+
// === Hyperliquid-only commands ===
|
|
552
|
+
trade
|
|
553
|
+
.command("leverage <symbol> <leverage>")
|
|
554
|
+
.description("Set leverage for a symbol")
|
|
555
|
+
.option("--isolated", "Use isolated margin mode (default: cross)")
|
|
556
|
+
.action(async (symbol, leverage, opts) => {
|
|
557
|
+
const adapter = await getAdapter();
|
|
558
|
+
const mode = opts.isolated ? "isolated" : "cross";
|
|
559
|
+
if (dryRunGuard("set_leverage", { exchange: adapter.name, symbol: symbol.toUpperCase(), leverage, mode }))
|
|
560
|
+
return;
|
|
561
|
+
try {
|
|
562
|
+
const result = await adapter.setLeverage(symbol.toUpperCase(), parseInt(leverage), mode);
|
|
563
|
+
logExecution({
|
|
564
|
+
type: "rebalance", exchange: adapter.name, symbol: symbol.toUpperCase(), side: mode,
|
|
565
|
+
size: leverage, status: "success", dryRun: false,
|
|
566
|
+
meta: { action: "set_leverage", leverage: parseInt(leverage), mode },
|
|
567
|
+
});
|
|
568
|
+
if (isJson())
|
|
569
|
+
return printJson(jsonOk(result));
|
|
570
|
+
console.log(chalk.green(`\n Leverage set to ${leverage}x (${mode}) for ${symbol.toUpperCase()} on ${adapter.name}.\n`));
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
logExecution({
|
|
574
|
+
type: "rebalance", exchange: adapter.name, symbol: symbol.toUpperCase(), side: mode,
|
|
575
|
+
size: leverage, status: "failed", dryRun: false,
|
|
576
|
+
error: err instanceof Error ? err.message : String(err),
|
|
577
|
+
meta: { action: "set_leverage", leverage: parseInt(leverage), mode },
|
|
578
|
+
});
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
// ── Grid Bot (shortcut with --background) ──
|
|
583
|
+
trade
|
|
584
|
+
.command("grid <symbol>")
|
|
585
|
+
.description("Run grid trading bot (foreground or --background)")
|
|
586
|
+
.requiredOption("--upper <price>", "Upper price bound")
|
|
587
|
+
.requiredOption("--lower <price>", "Lower price bound")
|
|
588
|
+
.option("--grids <n>", "Number of grid lines", "10")
|
|
589
|
+
.option("--size <size>", "Total position size (base)", "0.1")
|
|
590
|
+
.option("--side <side>", "Grid bias: long, short, neutral", "neutral")
|
|
591
|
+
.option("--leverage <n>", "Leverage to set")
|
|
592
|
+
.option("--interval <sec>", "Check interval in seconds", "10")
|
|
593
|
+
.option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
|
|
594
|
+
.option("--trailing-stop <pct>", "Stop if equity drops by this % from peak")
|
|
595
|
+
.option("--background", "Run in background (tmux)")
|
|
596
|
+
.action(async (symbol, opts) => {
|
|
597
|
+
const exchange = (await getAdapter()).name;
|
|
598
|
+
const cliArgs = [
|
|
599
|
+
`-e`, exchange, symbol.toUpperCase(),
|
|
600
|
+
`--upper`, opts.upper, `--lower`, opts.lower,
|
|
601
|
+
`--grids`, opts.grids, `--size`, opts.size,
|
|
602
|
+
`--side`, opts.side, `--interval`, opts.interval,
|
|
603
|
+
`--max-runtime`, opts.maxRuntime,
|
|
604
|
+
...(opts.leverage ? [`--leverage`, opts.leverage] : []),
|
|
605
|
+
...(opts.trailingStop ? [`--trailing-stop`, opts.trailingStop] : []),
|
|
606
|
+
];
|
|
607
|
+
if (opts.background) {
|
|
608
|
+
const { startJob } = await import("../jobs.js");
|
|
609
|
+
const job = startJob({
|
|
610
|
+
strategy: "grid",
|
|
611
|
+
exchange,
|
|
612
|
+
params: { symbol: symbol.toUpperCase(), ...opts },
|
|
613
|
+
cliArgs,
|
|
614
|
+
});
|
|
615
|
+
if (isJson())
|
|
616
|
+
return printJson(jsonOk(job));
|
|
617
|
+
console.log(chalk.green(`\n Grid bot started in background.`));
|
|
618
|
+
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
619
|
+
console.log(` Range: $${opts.lower} - $${opts.upper} | ${opts.grids} grids`);
|
|
620
|
+
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
621
|
+
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// Foreground: delegate to `run grid`
|
|
625
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
626
|
+
const adapter = await getAdapter();
|
|
627
|
+
const log = (msg) => {
|
|
628
|
+
const ts = new Date().toLocaleTimeString();
|
|
629
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
630
|
+
};
|
|
631
|
+
await runGrid(adapter, {
|
|
632
|
+
symbol: symbol.toUpperCase(),
|
|
633
|
+
side: opts.side,
|
|
634
|
+
upperPrice: parseFloat(opts.upper),
|
|
635
|
+
lowerPrice: parseFloat(opts.lower),
|
|
636
|
+
grids: parseInt(opts.grids),
|
|
637
|
+
totalSize: parseFloat(opts.size),
|
|
638
|
+
leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
|
|
639
|
+
intervalSec: parseInt(opts.interval),
|
|
640
|
+
maxRuntime: parseInt(opts.maxRuntime),
|
|
641
|
+
trailingStop: opts.trailingStop ? parseFloat(opts.trailingStop) : undefined,
|
|
642
|
+
}, undefined, log);
|
|
643
|
+
});
|
|
644
|
+
// ── DCA (shortcut with --background) ──
|
|
645
|
+
trade
|
|
646
|
+
.command("dca <symbol> <side> <amount> <interval>")
|
|
647
|
+
.description("Run DCA strategy (foreground or --background)")
|
|
648
|
+
.option("--orders <n>", "Total number of orders (0 = unlimited)", "0")
|
|
649
|
+
.option("--price-limit <price>", "Stop buying above / selling below this price")
|
|
650
|
+
.option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
|
|
651
|
+
.option("--background", "Run in background (tmux)")
|
|
652
|
+
.action(async (symbol, side, amount, interval, opts) => {
|
|
653
|
+
const s = side.toLowerCase();
|
|
654
|
+
if (s !== "buy" && s !== "sell")
|
|
655
|
+
errorAndExit("Side must be buy or sell");
|
|
656
|
+
const exchange = (await getAdapter()).name;
|
|
657
|
+
const cliArgs = [
|
|
658
|
+
`-e`, exchange, symbol.toUpperCase(), s, amount, interval,
|
|
659
|
+
`--orders`, opts.orders, `--max-runtime`, opts.maxRuntime,
|
|
660
|
+
...(opts.priceLimit ? [`--price-limit`, opts.priceLimit] : []),
|
|
661
|
+
];
|
|
662
|
+
if (opts.background) {
|
|
663
|
+
const { startJob } = await import("../jobs.js");
|
|
664
|
+
const job = startJob({
|
|
665
|
+
strategy: "dca",
|
|
666
|
+
exchange,
|
|
667
|
+
params: { symbol: symbol.toUpperCase(), side: s, amount, interval, ...opts },
|
|
668
|
+
cliArgs,
|
|
669
|
+
});
|
|
670
|
+
if (isJson())
|
|
671
|
+
return printJson(jsonOk(job));
|
|
672
|
+
console.log(chalk.green(`\n DCA started in background.`));
|
|
673
|
+
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
674
|
+
console.log(` ${s.toUpperCase()} ${amount} ${symbol.toUpperCase()} every ${interval}s`);
|
|
675
|
+
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
676
|
+
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// Foreground
|
|
680
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
681
|
+
const adapter = await getAdapter();
|
|
682
|
+
const log = (msg) => {
|
|
683
|
+
const ts = new Date().toLocaleTimeString();
|
|
684
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
685
|
+
};
|
|
686
|
+
await runDCA(adapter, {
|
|
687
|
+
symbol: symbol.toUpperCase(),
|
|
688
|
+
side: s,
|
|
689
|
+
amountPerOrder: parseFloat(amount),
|
|
690
|
+
intervalSec: parseInt(interval),
|
|
691
|
+
totalOrders: parseInt(opts.orders),
|
|
692
|
+
priceLimit: opts.priceLimit ? parseFloat(opts.priceLimit) : undefined,
|
|
693
|
+
maxRuntime: parseInt(opts.maxRuntime),
|
|
694
|
+
}, undefined, log);
|
|
695
|
+
});
|
|
696
|
+
// ── Position Management Shortcuts ──
|
|
697
|
+
trade
|
|
698
|
+
.command("close-all")
|
|
699
|
+
.description("Close all open positions (market orders on opposite side)")
|
|
700
|
+
.action(async () => {
|
|
701
|
+
await withJsonErrors(isJson(), async () => {
|
|
702
|
+
const adapter = await getAdapter();
|
|
703
|
+
if (dryRunGuard("close_all", { exchange: adapter.name }))
|
|
704
|
+
return;
|
|
705
|
+
const positions = await adapter.getPositions();
|
|
706
|
+
if (positions.length === 0) {
|
|
707
|
+
if (isJson())
|
|
708
|
+
return printJson(jsonOk({ closed: 0, positions: [] }));
|
|
709
|
+
console.log(chalk.yellow("\n No open positions to close.\n"));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (!isJson())
|
|
713
|
+
console.log(chalk.cyan(`\n Closing ${positions.length} position(s) on ${adapter.name}...\n`));
|
|
714
|
+
const results = [];
|
|
715
|
+
for (const pos of positions) {
|
|
716
|
+
const closeSide = pos.side === "long" ? "sell" : "buy";
|
|
717
|
+
if (!isJson())
|
|
718
|
+
console.log(chalk.gray(` ${closeSide.toUpperCase()} ${pos.size} ${pos.symbol} (closing ${pos.side})...`));
|
|
719
|
+
const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
|
|
720
|
+
results.push(result);
|
|
721
|
+
logExecution({
|
|
722
|
+
type: "market_order", exchange: adapter.name, symbol: pos.symbol,
|
|
723
|
+
side: closeSide, size: pos.size, status: "success", dryRun: false,
|
|
724
|
+
meta: { action: "close-all", originalSide: pos.side },
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (isJson())
|
|
728
|
+
return printJson(jsonOk({ closed: results.length, results }));
|
|
729
|
+
console.log(chalk.green(`\n Closed ${results.length} position(s) on ${adapter.name}.\n`));
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
trade
|
|
733
|
+
.command("close <symbol>")
|
|
734
|
+
.description("Close a specific symbol's position")
|
|
735
|
+
.action(async (symbol) => {
|
|
736
|
+
await withJsonErrors(isJson(), async () => {
|
|
737
|
+
const sym = symbol.toUpperCase();
|
|
738
|
+
const adapter = await getAdapter();
|
|
739
|
+
const positions = await adapter.getPositions();
|
|
740
|
+
const pos = positions.find(p => symbolMatch(p.symbol, sym));
|
|
741
|
+
if (!pos) {
|
|
742
|
+
if (isJson())
|
|
743
|
+
return printJson(jsonOk({ closed: false, reason: "no_position" }));
|
|
744
|
+
errorAndExit(`No open position for ${sym}`);
|
|
745
|
+
}
|
|
746
|
+
const closeSide = pos.side === "long" ? "sell" : "buy";
|
|
747
|
+
if (dryRunGuard("close", { exchange: adapter.name, symbol: sym, side: closeSide, size: pos.size, originalSide: pos.side }))
|
|
748
|
+
return;
|
|
749
|
+
if (!isJson())
|
|
750
|
+
console.log(chalk.cyan(`\n Closing ${pos.side} ${pos.size} ${sym} on ${adapter.name}...\n`));
|
|
751
|
+
const result = await adapter.marketOrder(sym, closeSide, pos.size);
|
|
752
|
+
logExecution({
|
|
753
|
+
type: "market_order", exchange: adapter.name, symbol: sym,
|
|
754
|
+
side: closeSide, size: pos.size, status: "success", dryRun: false,
|
|
755
|
+
meta: { action: "close", originalSide: pos.side },
|
|
756
|
+
});
|
|
757
|
+
if (isJson())
|
|
758
|
+
return printJson(jsonOk(result));
|
|
759
|
+
console.log(chalk.green(`\n Closed ${pos.side} ${pos.size} ${sym} on ${adapter.name}.\n`));
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
trade
|
|
763
|
+
.command("flatten")
|
|
764
|
+
.description("Cancel all orders AND close all positions (full cleanup)")
|
|
765
|
+
.action(async () => {
|
|
766
|
+
await withJsonErrors(isJson(), async () => {
|
|
767
|
+
const adapter = await getAdapter();
|
|
768
|
+
if (dryRunGuard("flatten", { exchange: adapter.name }))
|
|
769
|
+
return;
|
|
770
|
+
if (!isJson())
|
|
771
|
+
console.log(chalk.cyan(`\n Flattening account on ${adapter.name}...\n`));
|
|
772
|
+
// Step 1: Cancel all orders
|
|
773
|
+
if (!isJson())
|
|
774
|
+
console.log(chalk.gray(" Cancelling all open orders..."));
|
|
775
|
+
const cancelResult = await adapter.cancelAllOrders();
|
|
776
|
+
// Step 2: Close all positions
|
|
777
|
+
const positions = await adapter.getPositions();
|
|
778
|
+
const closeResults = [];
|
|
779
|
+
for (const pos of positions) {
|
|
780
|
+
const closeSide = pos.side === "long" ? "sell" : "buy";
|
|
781
|
+
if (!isJson())
|
|
782
|
+
console.log(chalk.gray(` ${closeSide.toUpperCase()} ${pos.size} ${pos.symbol} (closing ${pos.side})...`));
|
|
783
|
+
const result = await adapter.marketOrder(pos.symbol, closeSide, pos.size);
|
|
784
|
+
closeResults.push(result);
|
|
785
|
+
logExecution({
|
|
786
|
+
type: "market_order", exchange: adapter.name, symbol: pos.symbol,
|
|
787
|
+
side: closeSide, size: pos.size, status: "success", dryRun: false,
|
|
788
|
+
meta: { action: "flatten", originalSide: pos.side },
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
if (isJson())
|
|
792
|
+
return printJson(jsonOk({ ordersCancelled: cancelResult, positionsClosed: closeResults.length, closeResults }));
|
|
793
|
+
console.log(chalk.green(`\n Flattened: cancelled orders + closed ${closeResults.length} position(s) on ${adapter.name}.\n`));
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
trade
|
|
797
|
+
.command("reduce <symbol> <percent>")
|
|
798
|
+
.description("Reduce a position by a percentage (e.g., perp trade reduce BTC 50)")
|
|
799
|
+
.action(async (symbol, percent) => {
|
|
800
|
+
await withJsonErrors(isJson(), async () => {
|
|
801
|
+
const sym = symbol.toUpperCase();
|
|
802
|
+
const pct = parseFloat(percent);
|
|
803
|
+
if (isNaN(pct) || pct <= 0 || pct > 100)
|
|
804
|
+
errorAndExit("Percent must be between 0 and 100");
|
|
805
|
+
const adapter = await getAdapter();
|
|
806
|
+
const positions = await adapter.getPositions();
|
|
807
|
+
const pos = positions.find(p => symbolMatch(p.symbol, sym));
|
|
808
|
+
if (!pos) {
|
|
809
|
+
if (isJson())
|
|
810
|
+
return printJson(jsonOk({ reduced: false, reason: "no_position" }));
|
|
811
|
+
errorAndExit(`No open position for ${sym}`);
|
|
812
|
+
}
|
|
813
|
+
const fullSize = parseFloat(pos.size);
|
|
814
|
+
const reduceSize = (fullSize * pct / 100).toString();
|
|
815
|
+
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 }))
|
|
817
|
+
return;
|
|
818
|
+
if (!isJson())
|
|
819
|
+
console.log(chalk.cyan(`\n Reducing ${sym} ${pos.side} by ${pct}% (${reduceSize} of ${pos.size}) on ${adapter.name}...\n`));
|
|
820
|
+
const result = await adapter.marketOrder(sym, closeSide, reduceSize);
|
|
821
|
+
logExecution({
|
|
822
|
+
type: "market_order", exchange: adapter.name, symbol: sym,
|
|
823
|
+
side: closeSide, size: reduceSize, status: "success", dryRun: false,
|
|
824
|
+
meta: { action: "reduce", percent: pct, originalSize: pos.size, originalSide: pos.side },
|
|
825
|
+
});
|
|
826
|
+
if (isJson())
|
|
827
|
+
return printJson(jsonOk({ reduced: true, percent: pct, sizeReduced: reduceSize, originalSize: pos.size, result }));
|
|
828
|
+
console.log(chalk.green(`\n Reduced ${sym} by ${pct}% (${closeSide} ${reduceSize}) on ${adapter.name}.\n`));
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
// ── Order Status Query ──
|
|
832
|
+
trade
|
|
833
|
+
.command("status <orderId>")
|
|
834
|
+
.description("Query order status by ID")
|
|
835
|
+
.action(async (orderId) => {
|
|
836
|
+
await withJsonErrors(isJson(), async () => {
|
|
837
|
+
const adapter = await getAdapter();
|
|
838
|
+
if (adapter instanceof HyperliquidAdapter) {
|
|
839
|
+
const result = await adapter.queryOrder(Number(orderId));
|
|
840
|
+
if (isJson())
|
|
841
|
+
return printJson(jsonOk(result));
|
|
842
|
+
const order = result?.order;
|
|
843
|
+
if (!order) {
|
|
844
|
+
console.log(chalk.gray(`\n Order ${orderId} not found.\n`));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const o = (order.order ?? order);
|
|
848
|
+
console.log(chalk.cyan.bold(`\n Order ${orderId}\n`));
|
|
849
|
+
console.log(` Symbol: ${o.coin ?? o.symbol ?? ""}`);
|
|
850
|
+
console.log(` Side: ${o.side === "B" ? chalk.green("BUY") : chalk.red("SELL")}`);
|
|
851
|
+
console.log(` Price: $${formatUsd(String(o.limitPx ?? o.price ?? "0"))}`);
|
|
852
|
+
console.log(` Size: ${o.sz ?? o.size ?? ""}`);
|
|
853
|
+
console.log(` Status: ${order.status ?? o.status ?? "unknown"}`);
|
|
854
|
+
console.log();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// Generic: search order history
|
|
858
|
+
const [openOrders, history] = await Promise.all([
|
|
859
|
+
adapter.getOpenOrders(),
|
|
860
|
+
adapter.getOrderHistory(100),
|
|
861
|
+
]);
|
|
862
|
+
const found = [...openOrders, ...history].find(o => o.orderId === orderId);
|
|
863
|
+
if (!found) {
|
|
864
|
+
if (isJson())
|
|
865
|
+
return printJson(jsonError("ORDER_NOT_FOUND", `Order ${orderId} not found`));
|
|
866
|
+
console.log(chalk.gray(`\n Order ${orderId} not found.\n`));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (isJson())
|
|
870
|
+
return printJson(jsonOk(found));
|
|
871
|
+
console.log(chalk.cyan.bold(`\n Order ${orderId}\n`));
|
|
872
|
+
console.log(` Symbol: ${found.symbol}`);
|
|
873
|
+
console.log(` Side: ${found.side === "buy" ? chalk.green("BUY") : chalk.red("SELL")}`);
|
|
874
|
+
console.log(` Type: ${found.type}`);
|
|
875
|
+
console.log(` Price: $${formatUsd(found.price)}`);
|
|
876
|
+
console.log(` Size: ${found.size}`);
|
|
877
|
+
console.log(` Filled: ${found.filled}`);
|
|
878
|
+
console.log(` Status: ${found.status}`);
|
|
879
|
+
console.log();
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
// ── Recent Fills ──
|
|
883
|
+
trade
|
|
884
|
+
.command("fills [symbol]")
|
|
885
|
+
.description("Recent trade fills, optionally filtered by symbol")
|
|
886
|
+
.option("-l, --limit <n>", "Number of fills", "30")
|
|
887
|
+
.action(async (symbol, opts) => {
|
|
888
|
+
await withJsonErrors(isJson(), async () => {
|
|
889
|
+
const adapter = await getAdapter();
|
|
890
|
+
const limit = parseInt(opts.limit);
|
|
891
|
+
const trades = await adapter.getTradeHistory(limit);
|
|
892
|
+
let filtered = trades;
|
|
893
|
+
if (symbol) {
|
|
894
|
+
const sym = symbol.toUpperCase();
|
|
895
|
+
filtered = trades.filter(t => symbolMatch(t.symbol, sym));
|
|
896
|
+
}
|
|
897
|
+
if (isJson())
|
|
898
|
+
return printJson(jsonOk(filtered));
|
|
899
|
+
if (filtered.length === 0) {
|
|
900
|
+
console.log(chalk.gray(`\n No fills${symbol ? ` for ${symbol.toUpperCase()}` : ""}.\n`));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const rows = filtered.map((t) => [
|
|
904
|
+
new Date(t.time).toLocaleString(),
|
|
905
|
+
chalk.white.bold(t.symbol),
|
|
906
|
+
t.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
|
|
907
|
+
`$${formatUsd(t.price)}`,
|
|
908
|
+
t.size,
|
|
909
|
+
`$${formatUsd(t.fee)}`,
|
|
910
|
+
]);
|
|
911
|
+
console.log((await import("../utils.js")).makeTable(["Time", "Symbol", "Side", "Price", "Size", "Fee"], rows));
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
// ── Pre-Trade Validation ──
|
|
915
|
+
trade
|
|
916
|
+
.command("check <symbol> <side> <size>")
|
|
917
|
+
.description("Validate a trade before execution (pre-flight check)")
|
|
918
|
+
.option("--price <price>", "Price for limit orders")
|
|
919
|
+
.option("--type <type>", "Order type: market, limit, stop", "market")
|
|
920
|
+
.option("--leverage <n>", "Leverage to use")
|
|
921
|
+
.option("--reduce-only", "Check as reduce-only order")
|
|
922
|
+
.action(async (symbol, side, size, opts) => {
|
|
923
|
+
await withJsonErrors(isJson(), async () => {
|
|
924
|
+
const s = side.toLowerCase();
|
|
925
|
+
if (s !== "buy" && s !== "sell")
|
|
926
|
+
errorAndExit("Side must be buy or sell");
|
|
927
|
+
const adapter = await getAdapter();
|
|
928
|
+
const validation = await validateTrade(adapter, {
|
|
929
|
+
symbol: symbol.toUpperCase(),
|
|
930
|
+
side: s,
|
|
931
|
+
size: parseFloat(size),
|
|
932
|
+
price: opts.price ? parseFloat(opts.price) : undefined,
|
|
933
|
+
type: (opts.type ?? "market"),
|
|
934
|
+
leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
|
|
935
|
+
reduceOnly: opts.reduceOnly,
|
|
936
|
+
});
|
|
937
|
+
if (isJson())
|
|
938
|
+
return printJson(jsonOk(validation));
|
|
939
|
+
console.log(chalk.cyan.bold(`\n Pre-Trade Check: ${symbol.toUpperCase()} ${s.toUpperCase()} ${size}\n`));
|
|
940
|
+
for (const check of validation.checks) {
|
|
941
|
+
const icon = check.passed ? chalk.green("✓") : chalk.red("✗");
|
|
942
|
+
console.log(` ${icon} ${check.check}: ${check.message}`);
|
|
943
|
+
}
|
|
944
|
+
if (validation.warnings.length > 0) {
|
|
945
|
+
console.log(chalk.yellow("\n Warnings:"));
|
|
946
|
+
for (const w of validation.warnings) {
|
|
947
|
+
console.log(` ⚠ ${w}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (validation.estimatedCost) {
|
|
951
|
+
const c = validation.estimatedCost;
|
|
952
|
+
console.log(chalk.white.bold("\n Estimated Cost:"));
|
|
953
|
+
console.log(` Margin: $${c.margin.toFixed(2)}`);
|
|
954
|
+
console.log(` Fee: $${c.fee.toFixed(2)}`);
|
|
955
|
+
console.log(` Slippage: $${c.slippage.toFixed(2)}`);
|
|
956
|
+
console.log(` Total: $${c.total.toFixed(2)}`);
|
|
957
|
+
}
|
|
958
|
+
const resultColor = validation.valid ? chalk.green : chalk.red;
|
|
959
|
+
console.log(`\n Result: ${resultColor(validation.valid ? "VALID — safe to execute" : "INVALID — do not execute")}\n`);
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
// ── Scale-In (분할매수) ──
|
|
963
|
+
trade
|
|
964
|
+
.command("scale-in <symbol> <side>")
|
|
965
|
+
.description("Place multiple limit orders at different price levels to build a position gradually (분할매수)")
|
|
966
|
+
.requiredOption("--levels <levels>", "Comma-separated price:percent pairs (e.g., 65000:30,63000:30,60000:40)")
|
|
967
|
+
.option("--size-usd <usd>", "Total USD amount to deploy across all levels")
|
|
968
|
+
.option("--size <base>", "Total base amount (e.g., 0.01 BTC)")
|
|
969
|
+
.action(async (symbol, side, opts) => {
|
|
970
|
+
const sym = symbol.toUpperCase();
|
|
971
|
+
const s = side.toLowerCase();
|
|
972
|
+
if (s !== "buy" && s !== "sell")
|
|
973
|
+
errorAndExit("Side must be buy or sell");
|
|
974
|
+
if (!opts.sizeUsd && !opts.size)
|
|
975
|
+
errorAndExit("Must specify --size-usd or --size");
|
|
976
|
+
// Parse levels: "65000:30,63000:30,60000:40"
|
|
977
|
+
const levels = opts.levels.split(",").map(l => {
|
|
978
|
+
const [price, pct] = l.trim().split(":");
|
|
979
|
+
if (!price || !pct)
|
|
980
|
+
errorAndExit(`Invalid level format: ${l}. Use price:percent (e.g., 65000:30)`);
|
|
981
|
+
return { price: price.trim(), pct: parseFloat(pct.trim()) };
|
|
982
|
+
});
|
|
983
|
+
// Validate percentages sum to 100
|
|
984
|
+
const totalPct = levels.reduce((sum, l) => sum + l.pct, 0);
|
|
985
|
+
if (Math.abs(totalPct - 100) > 0.01) {
|
|
986
|
+
errorAndExit(`Percentages must sum to 100%. Got: ${totalPct}%`);
|
|
987
|
+
}
|
|
988
|
+
const adapter = await getAdapter();
|
|
989
|
+
// Compute sizes for each level
|
|
990
|
+
let levelSizes;
|
|
991
|
+
if (opts.sizeUsd) {
|
|
992
|
+
const totalUsd = parseFloat(opts.sizeUsd);
|
|
993
|
+
levelSizes = levels.map(l => ({
|
|
994
|
+
price: l.price,
|
|
995
|
+
pct: l.pct,
|
|
996
|
+
size: (totalUsd * l.pct / 100 / parseFloat(l.price)).toString(),
|
|
997
|
+
}));
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
const totalBase = parseFloat(opts.size);
|
|
1001
|
+
levelSizes = levels.map(l => ({
|
|
1002
|
+
price: l.price,
|
|
1003
|
+
pct: l.pct,
|
|
1004
|
+
size: (totalBase * l.pct / 100).toString(),
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
if (dryRunGuard("scale_in", {
|
|
1008
|
+
exchange: adapter.name, symbol: sym, side: s,
|
|
1009
|
+
totalSizeUsd: opts.sizeUsd ?? "N/A",
|
|
1010
|
+
totalSizeBase: opts.size ?? "N/A",
|
|
1011
|
+
levels: levelSizes.map(l => `${l.price}@${l.pct}% (${l.size})`).join(", "),
|
|
1012
|
+
}))
|
|
1013
|
+
return;
|
|
1014
|
+
// Place limit orders at each level (NOT reduce-only — opening positions)
|
|
1015
|
+
const results = [];
|
|
1016
|
+
for (const level of levelSizes) {
|
|
1017
|
+
try {
|
|
1018
|
+
const result = await adapter.limitOrder(sym, s, level.price, level.size);
|
|
1019
|
+
results.push({ price: level.price, size: level.size, pct: level.pct, result });
|
|
1020
|
+
logExecution({
|
|
1021
|
+
type: "limit_order", exchange: adapter.name, symbol: sym,
|
|
1022
|
+
side: s, size: level.size, price: level.price,
|
|
1023
|
+
status: "success", dryRun: false,
|
|
1024
|
+
meta: { action: "scale-in", pct: level.pct },
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch (err) {
|
|
1028
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1029
|
+
logExecution({
|
|
1030
|
+
type: "limit_order", exchange: adapter.name, symbol: sym,
|
|
1031
|
+
side: s, size: level.size, price: level.price,
|
|
1032
|
+
status: "failed", dryRun: false, error: msg,
|
|
1033
|
+
meta: { action: "scale-in", pct: level.pct },
|
|
1034
|
+
});
|
|
1035
|
+
if (isJson()) {
|
|
1036
|
+
results.push({ price: level.price, size: level.size, pct: level.pct, result: { error: msg } });
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
console.log(chalk.red(` Failed: ${level.price} x ${level.size} — ${msg}`));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (isJson())
|
|
1044
|
+
return printJson(jsonOk({ symbol: sym, side: s, levels: results }));
|
|
1045
|
+
console.log(chalk.green(`\n Scale-in orders placed for ${sym} on ${adapter.name}:\n`));
|
|
1046
|
+
for (const r of results) {
|
|
1047
|
+
const status = r.result?.error ? chalk.red("FAILED") : chalk.green("OK");
|
|
1048
|
+
console.log(` ${status} $${r.price} — ${r.pct}% (${r.size} ${sym})`);
|
|
1049
|
+
}
|
|
1050
|
+
console.log();
|
|
1051
|
+
});
|
|
1052
|
+
// ── Trailing Stop ──
|
|
1053
|
+
trade
|
|
1054
|
+
.command("trailing-stop <symbol>")
|
|
1055
|
+
.description("Client-side trailing stop that monitors price and closes position when price drops by X% from peak")
|
|
1056
|
+
.requiredOption("--trail <pct>", "Trail percentage (e.g., 3 = close when price drops 3% from high)")
|
|
1057
|
+
.option("--interval <sec>", "Check interval in seconds", "5")
|
|
1058
|
+
.option("--activation <price>", "Only start trailing after price reaches this level")
|
|
1059
|
+
.option("--background", "Run in background (tmux)")
|
|
1060
|
+
.action(async (symbol, opts) => {
|
|
1061
|
+
const sym = symbol.toUpperCase();
|
|
1062
|
+
const trailPct = parseFloat(opts.trail);
|
|
1063
|
+
const intervalSec = parseInt(opts.interval);
|
|
1064
|
+
const activationPrice = opts.activation ? parseFloat(opts.activation) : undefined;
|
|
1065
|
+
if (isNaN(trailPct) || trailPct <= 0)
|
|
1066
|
+
errorAndExit("Trail percentage must be > 0");
|
|
1067
|
+
const exchange = (await getAdapter()).name;
|
|
1068
|
+
// --background → run via tmux
|
|
1069
|
+
if (opts.background) {
|
|
1070
|
+
const { startJob } = await import("../jobs.js");
|
|
1071
|
+
const cliArgs = [
|
|
1072
|
+
`-e`, exchange, sym,
|
|
1073
|
+
`--trail`, opts.trail,
|
|
1074
|
+
`--interval`, opts.interval,
|
|
1075
|
+
...(opts.activation ? [`--activation`, opts.activation] : []),
|
|
1076
|
+
];
|
|
1077
|
+
const job = startJob({
|
|
1078
|
+
strategy: "trailing-stop",
|
|
1079
|
+
exchange,
|
|
1080
|
+
params: { symbol: sym, trail: trailPct, interval: intervalSec, activation: activationPrice },
|
|
1081
|
+
cliArgs,
|
|
1082
|
+
});
|
|
1083
|
+
if (isJson())
|
|
1084
|
+
return printJson(jsonOk(job));
|
|
1085
|
+
console.log(chalk.green(`\n Trailing stop started in background.`));
|
|
1086
|
+
console.log(` ID: ${chalk.white.bold(job.id)}`);
|
|
1087
|
+
console.log(` Trail: ${trailPct}%${activationPrice ? ` | Activation: $${activationPrice}` : ""}`);
|
|
1088
|
+
console.log(` Logs: ${chalk.gray(`perp jobs logs ${job.id}`)}`);
|
|
1089
|
+
console.log(` Stop: ${chalk.gray(`perp jobs stop ${job.id}`)}\n`);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
// Foreground: run trailing stop loop
|
|
1093
|
+
const adapter = await getAdapter();
|
|
1094
|
+
// Auto-detect position side
|
|
1095
|
+
const positions = await adapter.getPositions();
|
|
1096
|
+
const pos = positions.find(p => symbolMatch(p.symbol, sym));
|
|
1097
|
+
if (!pos)
|
|
1098
|
+
errorAndExit(`No open position for ${sym}. Open a position first.`);
|
|
1099
|
+
const positionSide = pos.side; // "long" or "short"
|
|
1100
|
+
const closeSide = positionSide === "long" ? "sell" : "buy";
|
|
1101
|
+
const posSize = pos.size;
|
|
1102
|
+
console.log(chalk.cyan(`\n Trailing Stop for ${sym} (${positionSide} ${posSize})`));
|
|
1103
|
+
console.log(chalk.cyan(` Trail: ${trailPct}% | Interval: ${intervalSec}s${activationPrice ? ` | Activation: $${activationPrice}` : ""}`));
|
|
1104
|
+
console.log(chalk.gray(` Press Ctrl+C to cancel.\n`));
|
|
1105
|
+
let peakPrice = 0;
|
|
1106
|
+
let activated = !activationPrice; // if no activation price, start immediately
|
|
1107
|
+
let running = true;
|
|
1108
|
+
const cleanup = () => { running = false; };
|
|
1109
|
+
process.on("SIGINT", cleanup);
|
|
1110
|
+
process.on("SIGTERM", cleanup);
|
|
1111
|
+
try {
|
|
1112
|
+
while (running) {
|
|
1113
|
+
const markets = await adapter.getMarkets();
|
|
1114
|
+
const market = markets.find(m => symbolMatch(m.symbol, sym));
|
|
1115
|
+
if (!market) {
|
|
1116
|
+
console.log(chalk.yellow(` Market data for ${sym} not found, retrying...`));
|
|
1117
|
+
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const currentPrice = parseFloat(market.markPrice);
|
|
1121
|
+
// Check activation
|
|
1122
|
+
if (!activated && activationPrice) {
|
|
1123
|
+
if (positionSide === "long" && currentPrice >= activationPrice) {
|
|
1124
|
+
activated = true;
|
|
1125
|
+
console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (>= $${activationPrice})`));
|
|
1126
|
+
}
|
|
1127
|
+
else if (positionSide === "short" && currentPrice <= activationPrice) {
|
|
1128
|
+
activated = true;
|
|
1129
|
+
console.log(chalk.green(` Activated at $${currentPrice.toFixed(2)} (<= $${activationPrice})`));
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
const ts = new Date().toLocaleTimeString();
|
|
1133
|
+
console.log(chalk.gray(` ${ts} | $${currentPrice.toFixed(2)} | Waiting for activation ($${activationPrice})...`));
|
|
1134
|
+
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Update peak
|
|
1139
|
+
if (positionSide === "long") {
|
|
1140
|
+
if (currentPrice > peakPrice)
|
|
1141
|
+
peakPrice = currentPrice;
|
|
1142
|
+
const dropPct = ((peakPrice - currentPrice) / peakPrice) * 100;
|
|
1143
|
+
const ts = new Date().toLocaleTimeString();
|
|
1144
|
+
console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Peak: $${peakPrice.toFixed(2)} | Drop: ${dropPct.toFixed(2)}%`));
|
|
1145
|
+
if (dropPct >= trailPct) {
|
|
1146
|
+
console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price dropped ${dropPct.toFixed(2)}% from peak $${peakPrice.toFixed(2)}`));
|
|
1147
|
+
console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
|
|
1148
|
+
const result = await adapter.marketOrder(sym, closeSide, posSize);
|
|
1149
|
+
logExecution({
|
|
1150
|
+
type: "market_order", exchange: adapter.name, symbol: sym,
|
|
1151
|
+
side: closeSide, size: posSize, status: "success", dryRun: false,
|
|
1152
|
+
meta: { action: "trailing-stop", trailPct, peakPrice, triggerPrice: currentPrice },
|
|
1153
|
+
});
|
|
1154
|
+
if (isJson())
|
|
1155
|
+
return printJson(jsonOk({ triggered: true, peakPrice, triggerPrice: currentPrice, dropPct, result }));
|
|
1156
|
+
console.log(chalk.green(` Position closed. Peak: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
// Short position: track lowest price, trigger when price rises
|
|
1162
|
+
if (peakPrice === 0 || currentPrice < peakPrice)
|
|
1163
|
+
peakPrice = currentPrice;
|
|
1164
|
+
const risePct = ((currentPrice - peakPrice) / peakPrice) * 100;
|
|
1165
|
+
const ts = new Date().toLocaleTimeString();
|
|
1166
|
+
console.log(chalk.gray(` ${ts} | Price: $${currentPrice.toFixed(2)} | Trough: $${peakPrice.toFixed(2)} | Rise: ${risePct.toFixed(2)}%`));
|
|
1167
|
+
if (risePct >= trailPct) {
|
|
1168
|
+
console.log(chalk.red(`\n TRAILING STOP TRIGGERED! Price rose ${risePct.toFixed(2)}% from trough $${peakPrice.toFixed(2)}`));
|
|
1169
|
+
console.log(chalk.red(` Closing ${positionSide} ${posSize} ${sym}...\n`));
|
|
1170
|
+
const result = await adapter.marketOrder(sym, closeSide, posSize);
|
|
1171
|
+
logExecution({
|
|
1172
|
+
type: "market_order", exchange: adapter.name, symbol: sym,
|
|
1173
|
+
side: closeSide, size: posSize, status: "success", dryRun: false,
|
|
1174
|
+
meta: { action: "trailing-stop", trailPct, troughPrice: peakPrice, triggerPrice: currentPrice },
|
|
1175
|
+
});
|
|
1176
|
+
if (isJson())
|
|
1177
|
+
return printJson(jsonOk({ triggered: true, troughPrice: peakPrice, triggerPrice: currentPrice, risePct, result }));
|
|
1178
|
+
console.log(chalk.green(` Position closed. Trough: $${peakPrice.toFixed(2)}, Exit: $${currentPrice.toFixed(2)}\n`));
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
finally {
|
|
1186
|
+
process.removeListener("SIGINT", cleanup);
|
|
1187
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1188
|
+
}
|
|
1189
|
+
if (isJson())
|
|
1190
|
+
return printJson(jsonOk({ triggered: false, reason: "cancelled" }));
|
|
1191
|
+
console.log(chalk.yellow(`\n Trailing stop cancelled.\n`));
|
|
1192
|
+
});
|
|
1193
|
+
// ── PnL Tracker ──
|
|
1194
|
+
trade
|
|
1195
|
+
.command("pnl-track")
|
|
1196
|
+
.description("Live-monitor positions with real-time PnL updates")
|
|
1197
|
+
.option("--interval <sec>", "Refresh interval in seconds", "3")
|
|
1198
|
+
.option("--symbol <sym>", "Filter to a specific symbol")
|
|
1199
|
+
.action(async (opts) => {
|
|
1200
|
+
const intervalSec = parseInt(opts.interval);
|
|
1201
|
+
const filterSym = opts.symbol?.toUpperCase();
|
|
1202
|
+
const adapter = await getAdapter();
|
|
1203
|
+
console.log(chalk.cyan(`\n PnL Tracker | ${adapter.name} | Interval: ${intervalSec}s`));
|
|
1204
|
+
if (filterSym)
|
|
1205
|
+
console.log(chalk.cyan(` Filtering: ${filterSym}`));
|
|
1206
|
+
console.log(chalk.gray(` Press Ctrl+C to stop.\n`));
|
|
1207
|
+
let running = true;
|
|
1208
|
+
const cleanup = () => { running = false; };
|
|
1209
|
+
process.on("SIGINT", cleanup);
|
|
1210
|
+
process.on("SIGTERM", cleanup);
|
|
1211
|
+
try {
|
|
1212
|
+
while (running) {
|
|
1213
|
+
const [positions, balance] = await Promise.all([
|
|
1214
|
+
adapter.getPositions(),
|
|
1215
|
+
adapter.getBalance(),
|
|
1216
|
+
]);
|
|
1217
|
+
let filtered = positions;
|
|
1218
|
+
if (filterSym) {
|
|
1219
|
+
filtered = positions.filter(p => symbolMatch(p.symbol, filterSym));
|
|
1220
|
+
}
|
|
1221
|
+
// Fetch funding payments (recent) for display
|
|
1222
|
+
let fundingBySymbol = {};
|
|
1223
|
+
try {
|
|
1224
|
+
const payments = await adapter.getFundingPayments(50);
|
|
1225
|
+
for (const fp of payments) {
|
|
1226
|
+
const sym = fp.symbol.toUpperCase();
|
|
1227
|
+
fundingBySymbol[sym] = (fundingBySymbol[sym] || 0) + parseFloat(fp.payment);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch {
|
|
1231
|
+
// funding payments may not be supported on all exchanges
|
|
1232
|
+
}
|
|
1233
|
+
console.clear();
|
|
1234
|
+
console.log(chalk.cyan.bold(`\n PnL Tracker — ${adapter.name} | ${new Date().toLocaleTimeString()}\n`));
|
|
1235
|
+
console.log(` Equity: $${formatUsd(balance.equity)} | Available: $${formatUsd(balance.available)} | Margin: $${formatUsd(balance.marginUsed)} | uPnL: $${formatUsd(balance.unrealizedPnl)}\n`);
|
|
1236
|
+
if (filtered.length === 0) {
|
|
1237
|
+
console.log(chalk.gray(` No open positions${filterSym ? ` for ${filterSym}` : ""}.`));
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
const { makeTable } = await import("../utils.js");
|
|
1241
|
+
const rows = filtered.map(p => {
|
|
1242
|
+
const entry = parseFloat(p.entryPrice);
|
|
1243
|
+
const mark = parseFloat(p.markPrice);
|
|
1244
|
+
const pnl = parseFloat(p.unrealizedPnl);
|
|
1245
|
+
const notional = parseFloat(p.size) * entry;
|
|
1246
|
+
const pnlPct = notional > 0 ? (pnl / notional) * 100 : 0;
|
|
1247
|
+
const funding = fundingBySymbol[p.symbol.toUpperCase()] || 0;
|
|
1248
|
+
const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
|
|
1249
|
+
const pctColor = pnlPct >= 0 ? chalk.green : chalk.red;
|
|
1250
|
+
return [
|
|
1251
|
+
chalk.white.bold(p.symbol),
|
|
1252
|
+
p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
|
|
1253
|
+
p.size,
|
|
1254
|
+
`$${formatUsd(p.entryPrice)}`,
|
|
1255
|
+
`$${formatUsd(p.markPrice)}`,
|
|
1256
|
+
pnlColor(`${pnl >= 0 ? "+" : ""}$${pnl.toFixed(2)}`),
|
|
1257
|
+
pctColor(`${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(2)}%`),
|
|
1258
|
+
funding !== 0 ? `$${funding.toFixed(4)}` : "-",
|
|
1259
|
+
];
|
|
1260
|
+
});
|
|
1261
|
+
console.log(makeTable(["Symbol", "Side", "Size", "Entry", "Mark", "PnL", "PnL%", "Funding"], rows));
|
|
1262
|
+
}
|
|
1263
|
+
console.log(chalk.gray(`\n Refreshing every ${intervalSec}s... Press Ctrl+C to stop.`));
|
|
1264
|
+
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
finally {
|
|
1268
|
+
process.removeListener("SIGINT", cleanup);
|
|
1269
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1270
|
+
}
|
|
1271
|
+
console.log(chalk.yellow(`\n PnL tracker stopped.\n`));
|
|
1272
|
+
});
|
|
1273
|
+
}
|