perp-cli 0.12.17 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +142 -55
- package/skills/perp-cli/references/agent-operations.md +110 -56
- package/skills/perp-cli/references/commands.md +266 -71
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Orderbook-aware split order execution.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The USD total is converted ONCE to a base size (at the opening best price, floored to the
|
|
5
|
+
* venue lot) and that size is split across the slices in whole lots. Every slice is an IOC
|
|
6
|
+
* limit at the worst acceptable price within the slippage tolerance (on the venue tick), and
|
|
7
|
+
* its fill is VERIFIED (placePerpVerified: venue-final IOC outcome or measured position delta)
|
|
8
|
+
* — a resolved order call is never counted as a fill.
|
|
9
|
+
*
|
|
10
|
+
* Before anything is sent the plan is checked against the venue minimums: a slice below the
|
|
11
|
+
* venue's minimum size / order value refuses the whole run (SIZE_TOO_SMALL, nothing sent).
|
|
8
12
|
*
|
|
9
13
|
* Flow per slice:
|
|
10
14
|
* 1. Fetch fresh orderbook
|
|
11
|
-
* 2.
|
|
12
|
-
* 3. Place IOC limit at best price
|
|
13
|
-
* 4. Wait for book to refresh, then repeat
|
|
15
|
+
* 2. Slice = remaining size / slices left (whole lots; the last slice takes the rest)
|
|
16
|
+
* 3. Place IOC limit at best price ± slippage tolerance, verify the fill
|
|
17
|
+
* 4. Wait for the book to refresh, then repeat
|
|
18
|
+
* A slice with an unknown outcome stops the run (nothing is resent); a definite rejection
|
|
19
|
+
* (incl. an IOC that filled nothing) leaves its size to the following slices.
|
|
14
20
|
*/
|
|
15
21
|
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
22
|
+
import { PerpError } from "../errors.js";
|
|
16
23
|
export interface SplitOrderParams {
|
|
17
24
|
symbol: string;
|
|
18
25
|
side: "buy" | "sell";
|
|
19
26
|
totalSizeUsd: number;
|
|
27
|
+
/** Venue lot step of the market (required: slices are sized in whole lots). */
|
|
28
|
+
stepSize: string;
|
|
29
|
+
/** Venue price tick; null = the venue publishes none (Hyperliquid) → the orderbook's price precision is used. */
|
|
30
|
+
tickSize: string | null;
|
|
31
|
+
/** Venue-published order minimums (null = none published). */
|
|
32
|
+
minSize: string | null;
|
|
33
|
+
minNotional: string | null;
|
|
20
34
|
maxSlippagePct?: number;
|
|
21
35
|
maxSlices?: number;
|
|
22
36
|
delayMs?: number;
|
|
@@ -26,24 +40,38 @@ export interface SplitSlice {
|
|
|
26
40
|
index: number;
|
|
27
41
|
size: string;
|
|
28
42
|
price: string;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
/** Verified filled base size; null = outcome unknown. */
|
|
44
|
+
filledSize: string | null;
|
|
45
|
+
/** Venue-reported average fill price; null = the venue did not report one. */
|
|
46
|
+
avgFillPrice: string | null;
|
|
47
|
+
method: "limit_ioc";
|
|
48
|
+
status: "filled" | "partial" | "rejected" | "unknown";
|
|
49
|
+
error: string | null;
|
|
33
50
|
}
|
|
34
51
|
export interface SplitOrderResult {
|
|
35
52
|
symbol: string;
|
|
36
53
|
side: "buy" | "sell";
|
|
37
54
|
requestedUsd: number;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
/** Base size the USD total was converted to (opening best price, floored to the lot). */
|
|
56
|
+
requestedSize: string;
|
|
57
|
+
filledSize: string;
|
|
58
|
+
/** Filled notional at the reported fill prices; null when a filled slice has no reported price. */
|
|
59
|
+
filledUsd: number | null;
|
|
60
|
+
/** Size-weighted average of the venue-reported fill prices; null when any filled slice has none. */
|
|
61
|
+
avgPrice: string | null;
|
|
62
|
+
totalSlippagePct: number | null;
|
|
42
63
|
slices: SplitSlice[];
|
|
43
|
-
|
|
64
|
+
/** complete = the whole size filled; partial = some; failed = nothing filled (all definite); unknown = a slice's outcome is unknown. */
|
|
65
|
+
status: "complete" | "partial" | "failed" | "unknown";
|
|
44
66
|
runtime: number;
|
|
45
67
|
}
|
|
46
68
|
/**
|
|
47
|
-
*
|
|
69
|
+
* The error a split run that did not fill completely must surface (never ok:true): ORDER_PENDING
|
|
70
|
+
* when a slice's outcome is unknown, ORDER_REJECTED when nothing filled and every slice is
|
|
71
|
+
* definite, ORDER_PARTIAL when only part filled. null = complete.
|
|
72
|
+
*/
|
|
73
|
+
export declare function splitIncompleteError(venue: string, r: SplitOrderResult): PerpError | null;
|
|
74
|
+
/**
|
|
75
|
+
* Execute a split order: lot-sized slices of the total, IOC limits, verified fills.
|
|
48
76
|
*/
|
|
49
77
|
export declare function runSplitOrder(adapter: ExchangeAdapter, params: SplitOrderParams, log?: (msg: string) => void): Promise<SplitOrderResult>;
|
|
@@ -1,136 +1,202 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Orderbook-aware split order execution.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The USD total is converted ONCE to a base size (at the opening best price, floored to the
|
|
5
|
+
* venue lot) and that size is split across the slices in whole lots. Every slice is an IOC
|
|
6
|
+
* limit at the worst acceptable price within the slippage tolerance (on the venue tick), and
|
|
7
|
+
* its fill is VERIFIED (placePerpVerified: venue-final IOC outcome or measured position delta)
|
|
8
|
+
* — a resolved order call is never counted as a fill.
|
|
9
|
+
*
|
|
10
|
+
* Before anything is sent the plan is checked against the venue minimums: a slice below the
|
|
11
|
+
* venue's minimum size / order value refuses the whole run (SIZE_TOO_SMALL, nothing sent).
|
|
8
12
|
*
|
|
9
13
|
* Flow per slice:
|
|
10
14
|
* 1. Fetch fresh orderbook
|
|
11
|
-
* 2.
|
|
12
|
-
* 3. Place IOC limit at best price
|
|
13
|
-
* 4. Wait for book to refresh, then repeat
|
|
15
|
+
* 2. Slice = remaining size / slices left (whole lots; the last slice takes the rest)
|
|
16
|
+
* 3. Place IOC limit at best price ± slippage tolerance, verify the fill
|
|
17
|
+
* 4. Wait for the book to refresh, then repeat
|
|
18
|
+
* A slice with an unknown outcome stops the run (nothing is resent); a definite rejection
|
|
19
|
+
* (incl. an IOC that filled nothing) leaves its size to the following slices.
|
|
14
20
|
*/
|
|
15
21
|
import { computeExecutableSize } from "../liquidity.js";
|
|
16
22
|
import { logExecution } from "../execution-log.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
import { PerpError, extractErrorMessage, isDefiniteOrderFailure } from "../errors.js";
|
|
24
|
+
import { alignComputedLimitPrice } from "../bot/quote-rules.js";
|
|
25
|
+
import { alignProtectionPrice, floorToStep, formatComputedPrice, toUnitsExact, unitsToString } from "../exchanges/precision.js";
|
|
26
|
+
import { placePerpVerified } from "../bot/perp-order.js";
|
|
27
|
+
import { reportedAvgFillPrice } from "../exchanges/ioc-outcome.js";
|
|
28
|
+
/** Decimal places of an orderbook price string. */
|
|
20
29
|
function pricePrecision(priceStr) {
|
|
21
30
|
return priceStr.includes(".") ? priceStr.split(".")[1].length : 0;
|
|
22
31
|
}
|
|
23
32
|
/**
|
|
24
|
-
*
|
|
33
|
+
* IOC limit for a slice: best ± slippage, aligned to the tick TOWARD the best price (never
|
|
34
|
+
* beyond the tolerance); without a tick, cut to the orderbook's own price precision.
|
|
35
|
+
*/
|
|
36
|
+
function sliceLimitPrice(best, side, maxSlippagePct, tickSize) {
|
|
37
|
+
const raw = Number(best) * (side === "buy" ? 1 + maxSlippagePct / 100 : 1 - maxSlippagePct / 100);
|
|
38
|
+
if (tickSize === null)
|
|
39
|
+
return raw.toFixed(pricePrecision(best));
|
|
40
|
+
const aligned = alignProtectionPrice(raw.toFixed(12), tickSize, side === "buy" ? "sell" : "buy");
|
|
41
|
+
// A tolerance smaller than one tick aligns back onto / past the best price: the best price itself still crosses.
|
|
42
|
+
const crosses = side === "buy" ? Number(aligned) >= Number(best) : Number(aligned) <= Number(best);
|
|
43
|
+
return crosses ? aligned : best;
|
|
44
|
+
}
|
|
45
|
+
function belowMinimum(size, price, p) {
|
|
46
|
+
if (p.minSize !== null && Number(size) < Number(p.minSize))
|
|
47
|
+
return `size ${p.minSize}`;
|
|
48
|
+
if (p.minNotional !== null && Number(size) * price < Number(p.minNotional))
|
|
49
|
+
return `order value $${p.minNotional}`;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The error a split run that did not fill completely must surface (never ok:true): ORDER_PENDING
|
|
54
|
+
* when a slice's outcome is unknown, ORDER_REJECTED when nothing filled and every slice is
|
|
55
|
+
* definite, ORDER_PARTIAL when only part filled. null = complete.
|
|
56
|
+
*/
|
|
57
|
+
export function splitIncompleteError(venue, r) {
|
|
58
|
+
if (r.status === "complete")
|
|
59
|
+
return null;
|
|
60
|
+
const what = `${venue} split ${r.side} $${r.requestedUsd} ${r.symbol}`;
|
|
61
|
+
const details = { exchange: venue, result: r };
|
|
62
|
+
const check = `perp account positions -e ${venue}`;
|
|
63
|
+
if (r.status === "unknown") {
|
|
64
|
+
return new PerpError("ORDER_PENDING", `${what}: the outcome of slice ${r.slices.findIndex(s => s.status === "unknown") + 1} is unknown; ${r.filledSize} of ${r.requestedSize} verified filled`, {
|
|
65
|
+
...details, remediation: `Do NOT resubmit — the slice may have executed. Check the position: ${check}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (r.status === "failed") {
|
|
69
|
+
const why = r.slices.map(s => s.error).filter(e => e !== null).join("; ");
|
|
70
|
+
return new PerpError("ORDER_REJECTED", `${what}: nothing filled${why !== "" ? ` — ${why}` : r.slices.length === 0 ? " — no slice was sent" : ""}`, details);
|
|
71
|
+
}
|
|
72
|
+
return new PerpError("ORDER_PARTIAL", `${what}: ${r.filledSize} of ${r.requestedSize} filled`, {
|
|
73
|
+
...details, remediation: `The filled part is open. Check the position: ${check}, then send the remainder as a new order if still wanted.`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Execute a split order: lot-sized slices of the total, IOC limits, verified fills.
|
|
25
78
|
*/
|
|
26
79
|
export async function runSplitOrder(adapter, params, log = console.log) {
|
|
27
|
-
const { symbol, side, totalSizeUsd, maxSlippagePct = 0.3, maxSlices = 10, delayMs = 1000, minSliceUsd = 100, } = params;
|
|
80
|
+
const { symbol, side, totalSizeUsd, stepSize, tickSize, maxSlippagePct = 0.3, maxSlices = 10, delayMs = 1000, minSliceUsd = 100, } = params;
|
|
28
81
|
const startedAt = Date.now();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
82
|
+
if (!(totalSizeUsd > 0) || !(minSliceUsd > 0) || !Number.isInteger(maxSlices) || maxSlices < 1 || !(maxSlippagePct > 0)) {
|
|
83
|
+
throw new PerpError("INVALID_PARAMS", `split ${symbol}: usd, --min-slice and --max-slippage must be > 0 and --max-slices a whole number ≥ 1`);
|
|
84
|
+
}
|
|
85
|
+
// ── Plan (nothing is sent before every slice passes) ──
|
|
86
|
+
const openingBook = await adapter.getOrderbook(symbol);
|
|
87
|
+
const openingLevels = side === "buy" ? openingBook.asks : openingBook.bids;
|
|
88
|
+
if (openingLevels.length === 0) {
|
|
89
|
+
throw new PerpError("PRICE_STALE", `${adapter.name} ${symbol}: no ${side === "buy" ? "asks" : "bids"} in the orderbook — nothing was sent`, { exchange: adapter.name });
|
|
90
|
+
}
|
|
91
|
+
const initialBestPrice = Number(openingLevels[0][0]);
|
|
92
|
+
if (!(initialBestPrice > 0)) {
|
|
93
|
+
throw new PerpError("PRICE_STALE", `${adapter.name} ${symbol}: unusable best price ${openingLevels[0][0]} — nothing was sent`, { exchange: adapter.name });
|
|
94
|
+
}
|
|
95
|
+
const sliceCount = Math.min(maxSlices, Math.floor(totalSizeUsd / minSliceUsd));
|
|
96
|
+
if (sliceCount < 1) {
|
|
97
|
+
throw new PerpError("SIZE_TOO_SMALL", `split ${symbol}: $${totalSizeUsd} is below --min-slice $${minSliceUsd}; nothing was sent`, {
|
|
98
|
+
exchange: adapter.name, remediation: "Lower --min-slice or use a larger total",
|
|
99
|
+
});
|
|
41
100
|
}
|
|
42
|
-
|
|
43
|
-
|
|
101
|
+
const totalSize = floorToStep(totalSizeUsd / initialBestPrice, stepSize);
|
|
102
|
+
const totalUnits = toUnitsExact(totalSize, stepSize, "size");
|
|
103
|
+
const smallest = unitsToString(totalUnits / BigInt(sliceCount), stepSize);
|
|
104
|
+
const under = totalUnits / BigInt(sliceCount) <= 0n ? `lot ${stepSize}` : belowMinimum(smallest, initialBestPrice, params);
|
|
105
|
+
if (under !== null) {
|
|
106
|
+
throw new PerpError("SIZE_TOO_SMALL", `${adapter.name} ${symbol}: $${totalSizeUsd} in ${sliceCount} slice(s) gives a slice of ${smallest} (≈ $${(Number(smallest) * initialBestPrice).toFixed(2)} @ ${openingLevels[0][0]}), below the venue minimum ${under}; nothing was sent`, {
|
|
107
|
+
exchange: adapter.name, slices: sliceCount, sliceSize: smallest, stepSize, minSize: params.minSize, minNotional: params.minNotional,
|
|
108
|
+
remediation: "Use fewer slices (--max-slices) or a larger total so that every slice meets the venue minimum",
|
|
109
|
+
});
|
|
44
110
|
}
|
|
45
|
-
log(`[SPLIT] ${side.toUpperCase()} $${totalSizeUsd
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
111
|
+
log(`[SPLIT] ${side.toUpperCase()} $${totalSizeUsd} of ${symbol} = ${totalSize} in ${sliceCount} slice(s) | max slippage: ${maxSlippagePct}%`);
|
|
112
|
+
const slices = [];
|
|
113
|
+
let remainingUnits = totalUnits;
|
|
114
|
+
let filledUnits = 0n;
|
|
115
|
+
let stopped = false;
|
|
116
|
+
for (let i = 0; i < sliceCount && remainingUnits > 0n && !stopped; i++) {
|
|
117
|
+
// 1. Fresh orderbook
|
|
118
|
+
const book = i === 0 ? openingBook : await adapter.getOrderbook(symbol);
|
|
49
119
|
const levels = side === "buy" ? book.asks : book.bids;
|
|
50
120
|
if (levels.length === 0) {
|
|
51
121
|
log(`[SPLIT] No ${side === "buy" ? "asks" : "bids"} in orderbook, stopping`);
|
|
52
122
|
break;
|
|
53
123
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
log(`[SPLIT]
|
|
124
|
+
const best = levels[0][0];
|
|
125
|
+
// 2. Slice: an even share of what remains, in whole lots (the last slice takes the rest)
|
|
126
|
+
const slicesLeft = BigInt(sliceCount - i);
|
|
127
|
+
const sliceUnits = slicesLeft === 1n ? remainingUnits : remainingUnits / slicesLeft;
|
|
128
|
+
const size = unitsToString(sliceUnits, stepSize);
|
|
129
|
+
const short = sliceUnits <= 0n ? `lot ${stepSize}` : belowMinimum(size, Number(best), params);
|
|
130
|
+
if (short !== null) {
|
|
131
|
+
log(`[SPLIT] Slice ${i + 1}: ${size} is below the venue minimum ${short}, stopping (not sent)`);
|
|
62
132
|
break;
|
|
63
133
|
}
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
? bestPrice * (1 + maxSlippagePct / 100)
|
|
71
|
-
: bestPrice * (1 - maxSlippagePct / 100);
|
|
72
|
-
const decimals = pricePrecision(levels[0][0]);
|
|
73
|
-
const formattedPrice = limitPrice.toFixed(decimals);
|
|
74
|
-
const formattedSize = sliceSize.toFixed(6);
|
|
75
|
-
log(`[SPLIT] Slice ${i + 1}: ${formattedSize} @ limit $${formattedPrice} (~$${sliceNotional.toFixed(0)}) | depth: $${check.depthUsd.toFixed(0)}`);
|
|
76
|
-
// 4. Execute IOC limit order
|
|
134
|
+
// 3. IOC limit at the worst acceptable price, fill verified
|
|
135
|
+
const raw = sliceLimitPrice(best, side, maxSlippagePct, tickSize);
|
|
136
|
+
// No published tick (HL): the venue grid, aligned toward the best price (never past the tolerance).
|
|
137
|
+
const price = tickSize === null ? await alignComputedLimitPrice(adapter, symbol, side === "buy" ? "buy" : "sell", raw) : raw;
|
|
138
|
+
const depth = computeExecutableSize(levels, Number(size) * Number(best), maxSlippagePct);
|
|
139
|
+
log(`[SPLIT] Slice ${i + 1}/${sliceCount}: ${size} @ limit $${price} | depth within ${maxSlippagePct}%: $${depth.depthUsd.toFixed(0)}`);
|
|
77
140
|
let slice;
|
|
78
141
|
try {
|
|
79
|
-
await adapter.
|
|
142
|
+
const fill = await placePerpVerified(adapter, adapter.name, symbol, side, size, {
|
|
143
|
+
stepSize,
|
|
144
|
+
place: () => adapter.limitOrder(symbol, side, price, size, { tif: "IOC" }),
|
|
145
|
+
});
|
|
146
|
+
const filled = floorToStep(fill.filledSize, stepSize);
|
|
147
|
+
const got = toUnitsExact(filled, stepSize, "filled size");
|
|
148
|
+
filledUnits += got;
|
|
149
|
+
remainingUnits -= got;
|
|
80
150
|
slice = {
|
|
81
|
-
index: i,
|
|
82
|
-
|
|
83
|
-
price: formattedPrice,
|
|
84
|
-
notionalUsd: sliceNotional,
|
|
85
|
-
slippagePct: check.slippagePct,
|
|
86
|
-
method: "limit_ioc",
|
|
87
|
-
status: "filled",
|
|
151
|
+
index: i, size, price, filledSize: filled, avgFillPrice: reportedAvgFillPrice(fill.raw),
|
|
152
|
+
method: "limit_ioc", status: got >= sliceUnits ? "filled" : "partial", error: null,
|
|
88
153
|
};
|
|
89
|
-
totalFilledSize += sliceSize;
|
|
90
|
-
totalFilledNotional += sliceNotional;
|
|
91
|
-
remainingUsd -= sliceNotional;
|
|
92
|
-
logExecution({
|
|
93
|
-
type: "limit_order", exchange: adapter.name, symbol,
|
|
94
|
-
side, size: formattedSize, price: formattedPrice,
|
|
95
|
-
notional: sliceNotional, status: "success", dryRun: false,
|
|
96
|
-
meta: { action: "split-order", slice: i + 1, totalSlices: maxSlices },
|
|
97
|
-
});
|
|
98
154
|
}
|
|
99
155
|
catch (err) {
|
|
100
|
-
|
|
156
|
+
const definite = isDefiniteOrderFailure(err);
|
|
157
|
+
const msg = extractErrorMessage(err);
|
|
158
|
+
log(`[SPLIT] Slice ${i + 1} ${definite ? "refused" : "outcome UNKNOWN"}: ${msg}`);
|
|
101
159
|
slice = {
|
|
102
|
-
index: i, size:
|
|
103
|
-
|
|
160
|
+
index: i, size, price, filledSize: definite ? "0" : null, avgFillPrice: null,
|
|
161
|
+
method: "limit_ioc", status: definite ? "rejected" : "unknown", error: msg,
|
|
104
162
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
notional: 0, status: "failed", dryRun: false,
|
|
109
|
-
error: err instanceof Error ? err.message : String(err),
|
|
110
|
-
meta: { action: "split-order", slice: i + 1, totalSlices: maxSlices },
|
|
111
|
-
});
|
|
163
|
+
// Unknown outcome: the slice may have executed — never send more on top of it.
|
|
164
|
+
if (!definite)
|
|
165
|
+
stopped = true;
|
|
112
166
|
}
|
|
113
167
|
slices.push(slice);
|
|
114
|
-
|
|
115
|
-
|
|
168
|
+
logExecution({
|
|
169
|
+
type: "limit_order", exchange: adapter.name, symbol, side, size, price,
|
|
170
|
+
status: slice.status === "unknown" ? "unverified" : slice.status === "rejected" ? "failed" : "success",
|
|
171
|
+
dryRun: false, ...(slice.error !== null ? { error: slice.error } : {}),
|
|
172
|
+
meta: { action: "split-order", slice: i + 1, totalSlices: sliceCount, filledSize: slice.filledSize, avgFillPrice: slice.avgFillPrice },
|
|
173
|
+
});
|
|
174
|
+
// 4. Let the book refresh
|
|
175
|
+
if (!stopped && remainingUnits > 0n && i < sliceCount - 1) {
|
|
116
176
|
await new Promise(r => setTimeout(r, delayMs));
|
|
117
177
|
}
|
|
118
178
|
}
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
179
|
+
// Average = the venue-reported fill prices, size-weighted; unknown when any filled slice has none.
|
|
180
|
+
const filledSlices = slices.filter(s => s.filledSize !== null && Number(s.filledSize) > 0);
|
|
181
|
+
const priced = filledSlices.length > 0 && filledSlices.every(s => s.avgFillPrice !== null);
|
|
182
|
+
const filledSize = unitsToString(filledUnits, stepSize);
|
|
183
|
+
const filledUsd = priced ? filledSlices.reduce((sum, s) => sum + Number(s.filledSize) * Number(s.avgFillPrice), 0) : filledUnits === 0n ? 0 : null;
|
|
184
|
+
const avgNum = priced && filledUsd !== null ? filledUsd / Number(filledSize) : null;
|
|
185
|
+
const avgPrice = avgNum !== null ? formatComputedPrice(avgNum, tickSize) : null;
|
|
186
|
+
const totalSlippagePct = avgNum !== null ? Math.abs((avgNum - initialBestPrice) / initialBestPrice) * 100 : null;
|
|
187
|
+
const status = slices.some(s => s.status === "unknown") ? "unknown"
|
|
188
|
+
: filledUnits >= totalUnits ? "complete"
|
|
189
|
+
: filledUnits > 0n ? "partial"
|
|
190
|
+
: "failed";
|
|
191
|
+
log(`[SPLIT] Done: ${slices.length} slice(s) | filled ${filledSize}/${totalSize} | avg price: ${avgPrice !== null ? `$${avgPrice}` : "not reported"}`);
|
|
127
192
|
return {
|
|
128
193
|
symbol, side,
|
|
129
194
|
requestedUsd: totalSizeUsd,
|
|
130
|
-
|
|
131
|
-
filledSize
|
|
195
|
+
requestedSize: totalSize,
|
|
196
|
+
filledSize,
|
|
197
|
+
filledUsd,
|
|
132
198
|
avgPrice,
|
|
133
|
-
totalSlippagePct
|
|
199
|
+
totalSlippagePct,
|
|
134
200
|
slices,
|
|
135
201
|
status,
|
|
136
202
|
runtime: (Date.now() - startedAt) / 1000,
|
|
@@ -7,7 +7,8 @@ export interface TrailingStopParams {
|
|
|
7
7
|
}
|
|
8
8
|
export interface TrailingStopResult {
|
|
9
9
|
triggered: boolean;
|
|
10
|
-
|
|
10
|
+
/** position_gone = the position was closed / flipped before the trigger fired (nothing sent). */
|
|
11
|
+
reason: "triggered" | "cancelled" | "no_position" | "position_gone";
|
|
11
12
|
peakPrice?: number;
|
|
12
13
|
triggerPrice?: number;
|
|
13
14
|
changePct?: number;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { assertCanClosePositions } from "../exchanges/trading-v2.js";
|
|
1
2
|
import { updateJobState } from "../jobs.js";
|
|
3
|
+
import { invalidateCache } from "../cache.js";
|
|
2
4
|
import { logExecution } from "../execution-log.js";
|
|
5
|
+
import { PerpError } from "../errors.js";
|
|
6
|
+
import { closePerpVerified } from "../bot/perp-order.js";
|
|
7
|
+
import { isPositionGone } from "../bot/strategies/funding-arb-utils.js";
|
|
3
8
|
export async function runTrailingStop(adapter, params, jobId, log = console.log) {
|
|
4
9
|
const { symbol, trailPct } = params;
|
|
5
10
|
const intervalMs = (params.intervalSec ?? 5) * 1000;
|
|
6
11
|
const activationPrice = params.activationPrice;
|
|
7
12
|
const startedAt = Date.now();
|
|
13
|
+
// Unattended: the close fires later, with nobody watching — refused up front on a gated venue.
|
|
14
|
+
assertCanClosePositions(adapter, `Trailing stop on ${adapter.name}`);
|
|
8
15
|
// Auto-detect position side
|
|
9
16
|
const positions = await adapter.getPositions();
|
|
10
17
|
const pos = positions.find(p => {
|
|
@@ -19,6 +26,39 @@ export async function runTrailingStop(adapter, params, jobId, log = console.log)
|
|
|
19
26
|
const positionSide = pos.side;
|
|
20
27
|
const closeSide = positionSide === "long" ? "sell" : "buy";
|
|
21
28
|
const posSize = pos.size;
|
|
29
|
+
/**
|
|
30
|
+
* At trigger time: re-read the position (it may have shrunk, grown, flipped or closed since
|
|
31
|
+
* start) and close exactly the live size, reduce-only and verified (closePerpVerified: one
|
|
32
|
+
* residual close). Returns the size closed, or null when the position is gone (nothing sent /
|
|
33
|
+
* the venue rejected a reduce-only close of a flat position). An unknown outcome (ORDER_PENDING)
|
|
34
|
+
* or a residual propagates as an error — never resubmitted.
|
|
35
|
+
*/
|
|
36
|
+
const closeLive = async () => {
|
|
37
|
+
invalidateCache("acct"); // size the reduce-only close from a fresh read, never a cached position
|
|
38
|
+
const live = (await adapter.getPositions()).find(p => {
|
|
39
|
+
const c = p.symbol.toUpperCase();
|
|
40
|
+
const t = symbol.toUpperCase();
|
|
41
|
+
return (c === t || c === `${t}-PERP` || c.replace(/-PERP$/, "") === t) && p.side === positionSide;
|
|
42
|
+
});
|
|
43
|
+
const liveSize = live === undefined ? null : live.size.replace(/^-/, "");
|
|
44
|
+
if (liveSize === null || !(Number(liveSize) > 0))
|
|
45
|
+
return null;
|
|
46
|
+
log(`[TRAIL] Closing ${positionSide} ${liveSize} ${symbol} (live size, reduce-only)...`);
|
|
47
|
+
try {
|
|
48
|
+
const c = await closePerpVerified(adapter, adapter.name, symbol, closeSide, liveSize);
|
|
49
|
+
if (c.residual > 0) {
|
|
50
|
+
throw new PerpError("ORDER_PARTIAL", `trailing stop closed ${c.closed} of ${liveSize} ${symbol}; ${c.residual} still open${c.note !== null ? ` (${c.note})` : ""}`, {
|
|
51
|
+
exchange: adapter.name, remediation: `Do NOT rerun blindly — check the ${symbol} position on ${adapter.name} and close the rest by hand`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (isPositionGone(e))
|
|
57
|
+
return null;
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
return liveSize;
|
|
61
|
+
};
|
|
22
62
|
log(`[TRAIL] ${symbol} ${positionSide} ${posSize} | Trail: ${trailPct}%${activationPrice ? ` | Activation: $${activationPrice}` : ""}`);
|
|
23
63
|
let peakPrice = 0;
|
|
24
64
|
let activated = !activationPrice;
|
|
@@ -69,11 +109,14 @@ export async function runTrailingStop(adapter, params, jobId, log = console.log)
|
|
|
69
109
|
log(`[TRAIL] $${currentPrice.toFixed(2)} | Peak: $${peakPrice.toFixed(2)} | Drop: ${dropPct.toFixed(2)}%`);
|
|
70
110
|
if (dropPct >= trailPct) {
|
|
71
111
|
log(`[TRAIL] TRIGGERED! Price dropped ${dropPct.toFixed(2)}% from peak $${peakPrice.toFixed(2)}`);
|
|
72
|
-
|
|
73
|
-
|
|
112
|
+
const closedSize = await closeLive();
|
|
113
|
+
if (closedSize === null) {
|
|
114
|
+
log(`[TRAIL] ${symbol} ${positionSide} position is gone — nothing to close. Exiting.`);
|
|
115
|
+
return { triggered: false, reason: "position_gone", peakPrice, positionSide, runtime: (Date.now() - startedAt) / 1000 };
|
|
116
|
+
}
|
|
74
117
|
logExecution({
|
|
75
118
|
type: "market_order", exchange: adapter.name, symbol,
|
|
76
|
-
side: closeSide, size:
|
|
119
|
+
side: closeSide, size: closedSize, status: "success", dryRun: false,
|
|
77
120
|
meta: { action: "trailing-stop", trailPct, peakPrice, triggerPrice: currentPrice },
|
|
78
121
|
});
|
|
79
122
|
return {
|
|
@@ -91,11 +134,14 @@ export async function runTrailingStop(adapter, params, jobId, log = console.log)
|
|
|
91
134
|
log(`[TRAIL] $${currentPrice.toFixed(2)} | Trough: $${peakPrice.toFixed(2)} | Rise: ${risePct.toFixed(2)}%`);
|
|
92
135
|
if (risePct >= trailPct) {
|
|
93
136
|
log(`[TRAIL] TRIGGERED! Price rose ${risePct.toFixed(2)}% from trough $${peakPrice.toFixed(2)}`);
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
const closedSize = await closeLive();
|
|
138
|
+
if (closedSize === null) {
|
|
139
|
+
log(`[TRAIL] ${symbol} ${positionSide} position is gone — nothing to close. Exiting.`);
|
|
140
|
+
return { triggered: false, reason: "position_gone", peakPrice, positionSide, runtime: (Date.now() - startedAt) / 1000 };
|
|
141
|
+
}
|
|
96
142
|
logExecution({
|
|
97
143
|
type: "market_order", exchange: adapter.name, symbol,
|
|
98
|
-
side: closeSide, size:
|
|
144
|
+
side: closeSide, size: closedSize, status: "success", dryRun: false,
|
|
99
145
|
meta: { action: "trailing-stop", trailPct, troughPrice: peakPrice, triggerPrice: currentPrice },
|
|
100
146
|
});
|
|
101
147
|
return {
|
|
@@ -6,13 +6,18 @@ export interface TWAPParams {
|
|
|
6
6
|
durationSec: number;
|
|
7
7
|
slices?: number;
|
|
8
8
|
maxSlippage?: number;
|
|
9
|
+
reduceOnly?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface TWAPState {
|
|
11
12
|
filled: number;
|
|
12
13
|
remaining: number;
|
|
13
14
|
slicesDone: number;
|
|
14
15
|
totalSlices: number;
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Size-weighted average of the venue-reported FILL prices of the slices; null while any
|
|
18
|
+
* filled slice has no reported fill price (never the limit / slippage-protection price).
|
|
19
|
+
*/
|
|
20
|
+
avgPrice: number | null;
|
|
16
21
|
errors: number;
|
|
17
22
|
startedAt: number;
|
|
18
23
|
lastSliceAt: number;
|
package/dist/strategies/twap.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { assertCanClosePositions } from "../exchanges/trading-v2.js";
|
|
2
|
+
import { resolveAdapterSlippage } from "../exchanges/slippage.js";
|
|
1
3
|
import { updateJobState } from "../jobs.js";
|
|
4
|
+
import { reportedAvgFillPrice } from "../exchanges/ioc-outcome.js";
|
|
5
|
+
import { filledOf } from "../bot/fill-size.js";
|
|
6
|
+
import { isOutcomeUnknown } from "../bot/perp-order.js";
|
|
7
|
+
import { pendingStop } from "./pending-stop.js";
|
|
8
|
+
import { formatComputedPrice } from "../exchanges/precision.js";
|
|
2
9
|
export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
10
|
+
assertCanClosePositions(adapter, `TWAP on ${adapter.name}`);
|
|
3
11
|
const totalSlices = params.slices || Math.max(Math.floor(params.durationSec / 30), 2);
|
|
4
12
|
const sliceSize = params.totalSize / totalSlices;
|
|
5
13
|
const intervalMs = (params.durationSec * 1000) / totalSlices;
|
|
@@ -8,7 +16,7 @@ export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
|
8
16
|
remaining: params.totalSize,
|
|
9
17
|
slicesDone: 0,
|
|
10
18
|
totalSlices,
|
|
11
|
-
avgPrice:
|
|
19
|
+
avgPrice: null,
|
|
12
20
|
errors: 0,
|
|
13
21
|
startedAt: Date.now(),
|
|
14
22
|
lastSliceAt: 0,
|
|
@@ -17,6 +25,9 @@ export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
|
17
25
|
log(`[TWAP] ${totalSlices} slices, ${sliceSize.toFixed(6)} per slice, ${(intervalMs / 1000).toFixed(1)}s interval`);
|
|
18
26
|
log(`[TWAP] Exchange: ${adapter.name}`);
|
|
19
27
|
let totalCost = 0;
|
|
28
|
+
let everySlicePriced = true;
|
|
29
|
+
/** Set when a slice's outcome / fill is unknown: the run stops and surfaces ORDER_PENDING. */
|
|
30
|
+
let pending = null;
|
|
20
31
|
for (let i = 0; i < totalSlices; i++) {
|
|
21
32
|
if (i > 0) {
|
|
22
33
|
await sleep(intervalMs);
|
|
@@ -28,19 +39,26 @@ export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
|
28
39
|
break;
|
|
29
40
|
try {
|
|
30
41
|
log(`[TWAP] Slice ${i + 1}/${totalSlices}: ${params.side} ${thisSlice.toFixed(6)} ${params.symbol}...`);
|
|
31
|
-
const result = await adapter.marketOrder(params.symbol, params.side, String(thisSlice));
|
|
42
|
+
const result = await adapter.marketOrder(params.symbol, params.side, String(thisSlice), { reduceOnly: params.reduceOnly === true, slippage: resolveAdapterSlippage(adapter, null) });
|
|
43
|
+
// The venue-reported fill (IOC outcome / OrderResult.filledSize) — never the slice requested.
|
|
44
|
+
const sliceFilled = filledOf(adapter, result);
|
|
45
|
+
if (sliceFilled === null) {
|
|
46
|
+
pending = pendingStop("twap", adapter, params.symbol, `TWAP slice ${i + 1}/${totalSlices} ${params.side} ${thisSlice.toFixed(6)} ${params.symbol} on ${adapter.name}: accepted but the venue reported no fill size`, log);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
32
49
|
state.slicesDone++;
|
|
33
|
-
state.filled +=
|
|
50
|
+
state.filled += sliceFilled;
|
|
34
51
|
state.remaining = params.totalSize - state.filled;
|
|
35
52
|
state.lastSliceAt = Date.now();
|
|
36
|
-
//
|
|
37
|
-
const fillPrice =
|
|
38
|
-
if (fillPrice
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// The venue-reported fill price only: the result's `price` is the protection price that was sent.
|
|
54
|
+
const fillPrice = reportedAvgFillPrice(result);
|
|
55
|
+
if (fillPrice === null)
|
|
56
|
+
everySlicePriced = false;
|
|
57
|
+
else
|
|
58
|
+
totalCost += sliceFilled * Number(fillPrice);
|
|
59
|
+
state.avgPrice = everySlicePriced && state.filled > 0 ? Number(formatComputedPrice(totalCost / state.filled)) : null;
|
|
42
60
|
const pct = ((state.filled / params.totalSize) * 100).toFixed(1);
|
|
43
|
-
log(`[TWAP] Filled ${state.filled.toFixed(6)}/${params.totalSize} (${pct}%)${state.avgPrice
|
|
61
|
+
log(`[TWAP] Filled ${state.filled.toFixed(6)}/${params.totalSize} (${pct}%)${state.avgPrice !== null ? ` avg $${state.avgPrice}` : ""}`);
|
|
44
62
|
// Update job state file if running as background job
|
|
45
63
|
if (jobId) {
|
|
46
64
|
updateJobState(jobId, {
|
|
@@ -56,8 +74,13 @@ export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
76
|
catch (err) {
|
|
59
|
-
state.errors++;
|
|
60
77
|
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
if (isOutcomeUnknown(err)) {
|
|
79
|
+
// Unknown outcome: never resubmitted — the run stops and the symbol is quarantined.
|
|
80
|
+
pending = pendingStop("twap", adapter, params.symbol, `TWAP slice ${i + 1}/${totalSlices} ${params.side} ${thisSlice.toFixed(6)} ${params.symbol} on ${adapter.name}: outcome unknown (${msg})`, log);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
state.errors++;
|
|
61
84
|
log(`[TWAP] Slice ${i + 1} error: ${msg}`);
|
|
62
85
|
// Continue — don't abort the whole TWAP for one failed slice
|
|
63
86
|
if (state.errors > totalSlices * 0.5) {
|
|
@@ -68,6 +91,11 @@ export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
|
68
91
|
}
|
|
69
92
|
const elapsed = ((Date.now() - state.startedAt) / 1000).toFixed(1);
|
|
70
93
|
log(`[TWAP] Complete. Filled ${state.filled.toFixed(6)} in ${elapsed}s, ${state.errors} errors.`);
|
|
94
|
+
if (pending !== null) {
|
|
95
|
+
if (jobId)
|
|
96
|
+
updateJobState(jobId, { status: "error", result: { ...state, error: pending.message } });
|
|
97
|
+
throw pending;
|
|
98
|
+
}
|
|
71
99
|
if (jobId) {
|
|
72
100
|
updateJobState(jobId, { status: "done", result: { ...state } });
|
|
73
101
|
}
|