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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Filled base size the venue reported for a placed order: the adapter's venue-final IOC / market
|
|
4
|
+
* outcome (iocOutcomeOf), else a TradingV2 OrderResult's `filledSize`. null = not reported (a
|
|
5
|
+
* resting order, or an unrecognized result) — the caller does not count it as a fill.
|
|
6
|
+
*/
|
|
7
|
+
export declare function filledOf(adapter: ExchangeAdapter, raw: unknown): number | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isIocOutcomeCapable } from "../exchanges/ioc-outcome.js";
|
|
2
|
+
/**
|
|
3
|
+
* Filled base size the venue reported for a placed order: the adapter's venue-final IOC / market
|
|
4
|
+
* outcome (iocOutcomeOf), else a TradingV2 OrderResult's `filledSize`. null = not reported (a
|
|
5
|
+
* resting order, or an unrecognized result) — the caller does not count it as a fill.
|
|
6
|
+
*/
|
|
7
|
+
export function filledOf(adapter, raw) {
|
|
8
|
+
const outcome = isIocOutcomeCapable(adapter) ? adapter.iocOutcomeOf(raw) : null;
|
|
9
|
+
if (outcome !== null)
|
|
10
|
+
return Number(outcome.filledSize);
|
|
11
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
12
|
+
return null;
|
|
13
|
+
const r = raw;
|
|
14
|
+
if (typeof r.status !== "string" || typeof r.filledSize !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const n = Number(r.filledSize);
|
|
17
|
+
return Number.isFinite(n) ? n : null;
|
|
18
|
+
}
|
package/dist/bot/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "
|
|
|
2
2
|
export type { ConditionType, Condition, RiskConfig, GridStrategyParams, DCAStrategyParams, FundingArbStrategyParams, GenericStrategyParams, StrategyParams, BotConfig, } from "./config.js";
|
|
3
3
|
export { runBot } from "./engine.js";
|
|
4
4
|
export type { BotLog, BotOutputMode } from "./engine.js";
|
|
5
|
-
export { PRESETS, getPreset, getPresetsByStrategy } from "./presets.js";
|
|
5
|
+
export { PRESETS, getPreset, getPresetsByStrategy, withPresetSize } from "./presets.js";
|
|
6
6
|
export type { Preset } from "./presets.js";
|
|
7
7
|
export { evaluateCondition, evaluateAllConditions, getMarketSnapshot, calculateRSI } from "./conditions.js";
|
|
8
8
|
export type { MarketSnapshot } from "./conditions.js";
|
package/dist/bot/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// bot/ barrel — public API
|
|
2
2
|
export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "./config.js";
|
|
3
3
|
export { runBot } from "./engine.js";
|
|
4
|
-
export { PRESETS, getPreset, getPresetsByStrategy } from "./presets.js";
|
|
4
|
+
export { PRESETS, getPreset, getPresetsByStrategy, withPresetSize } from "./presets.js";
|
|
5
5
|
export { evaluateCondition, evaluateAllConditions, getMarketSnapshot, calculateRSI } from "./conditions.js";
|
|
6
6
|
export * from "./indicators.js";
|
|
7
7
|
export * from "./strategy-types.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified perp market order for bot / arb legs (design §12.3).
|
|
3
|
+
*
|
|
4
|
+
* A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
|
|
5
|
+
* when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
|
|
6
|
+
* transport failure after the request leaves the outcome unknown. Every bot perp leg
|
|
7
|
+
* that hedges (or un-hedges) another leg goes through placePerpVerified():
|
|
8
|
+
*
|
|
9
|
+
* - the position on the venue is read BEFORE the order (failure → ORDER_REJECTED,
|
|
10
|
+
* nothing sent);
|
|
11
|
+
* - the order call throws a TYPED definite rejection (isDefiniteOrderFailure) → rethrown
|
|
12
|
+
* unchanged;
|
|
13
|
+
* - otherwise — a resolved call, or ANY other thrown value (untyped Error, transport
|
|
14
|
+
* failure, HL "HL exchange API error (5xx)", Pacifica "POST … failed (5xx)", any
|
|
15
|
+
* non-definite PerpError) — the fill is MEASURED as the position delta (uncached
|
|
16
|
+
* reads, polled): moved ≥ 80% of the size (or a reduce-only order flattened the
|
|
17
|
+
* position) → filled; no such movement, or an unreadable position → ORDER_PENDING
|
|
18
|
+
* (do NOT resubmit, the caller quarantines). An untyped error is never read as a
|
|
19
|
+
* rejection. The one measured exception: a reduce-only order that threw a plain venue
|
|
20
|
+
* message while the position was flat before AND after cannot have done anything →
|
|
21
|
+
* ORDER_REJECTED carrying the venue message and details.reduceOnlyFlat (isPositionGone()).
|
|
22
|
+
* Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
|
|
23
|
+
* the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
|
|
24
|
+
*
|
|
25
|
+
* Venue-final IOC outcome (design §13.10 #4): when the adapter is IocOutcomeCapable and
|
|
26
|
+
* iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
|
|
27
|
+
* (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
|
|
28
|
+
* step, taken from the precision of the order size / reported fill):
|
|
29
|
+
* - final 0 and the position did not move → ORDER_REJECTED (definite, nothing filled);
|
|
30
|
+
* the position DID move → the measured delta is returned (discrepancy logged to stderr);
|
|
31
|
+
* - final 0 < filled ≤ size and the delta agrees → filledSize = the venue-reported size
|
|
32
|
+
* (a final partial fill — callers size the 2nd leg / trim / residual-close from it);
|
|
33
|
+
* the delta never agrees (or the position is unreadable) → ORDER_PENDING.
|
|
34
|
+
* No outcome (adapter not capable, or iocOutcomeOf → null) → the measurement path above.
|
|
35
|
+
*/
|
|
36
|
+
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
37
|
+
import { type ResolvedSlippage } from "../exchanges/slippage.js";
|
|
38
|
+
export interface PerpFill {
|
|
39
|
+
/** Base units filled, > 0: the measured position delta, or the venue-final IOC fill cross-checked against it. */
|
|
40
|
+
filledSize: number;
|
|
41
|
+
/** The adapter's raw result (null when the call threw but the position moved). */
|
|
42
|
+
raw: unknown;
|
|
43
|
+
}
|
|
44
|
+
/** Net signed position (long > 0, short < 0) for `symbol`, read uncached. Throws on a bad read. */
|
|
45
|
+
export declare function signedPerpPosition(adapter: ExchangeAdapter, symbol: string): Promise<number>;
|
|
46
|
+
export declare function placePerpVerified(adapter: ExchangeAdapter, venue: string, symbol: string, side: "buy" | "sell", size: string, opts?: {
|
|
47
|
+
reduceOnly?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Sends the order instead of adapter.marketOrder (e.g. an IOC limit from smartOrder).
|
|
50
|
+
* It must place exactly `side` `size` `symbol` (reduce-only when opts.reduceOnly);
|
|
51
|
+
* the fill is measured the same way.
|
|
52
|
+
*/
|
|
53
|
+
place?: () => Promise<unknown>;
|
|
54
|
+
/** Slippage bound for adapter.marketOrder; omitted → resolveSlippage(venue, null) (bot/arb paths). */
|
|
55
|
+
slippage?: ResolvedSlippage;
|
|
56
|
+
/** Market lot step when known: fill comparisons tolerate half a lot (else float-noise epsilon). */
|
|
57
|
+
stepSize?: string | null;
|
|
58
|
+
}): Promise<PerpFill>;
|
|
59
|
+
/** Lot size from a definite lot-size rejection (placePerpVerified ORDER_REJECTED details.lotSize), else null. */
|
|
60
|
+
export declare function rejectedLotSize(e: unknown): number | null;
|
|
61
|
+
/** true when `e` says the order outcome is unknown (quarantine, never retry / roll back blindly). */
|
|
62
|
+
export declare function isOutcomeUnknown(e: unknown): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Second-leg residual correction (design §12.3): the second leg is sized to the first
|
|
65
|
+
* leg's VERIFIED fill but may itself fill less (≥ 80% measured, or any venue-final partial fill). Reduce the first leg by exactly
|
|
66
|
+
* this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
|
|
67
|
+
* verified, never the venue's whole position. `firstSide` is the side the first leg
|
|
68
|
+
* opened with. Returns the excess closed ("0" when the legs already match). Throws when
|
|
69
|
+
* the correction was rejected or is unverified: the residual is unhedged → the caller
|
|
70
|
+
* quarantines.
|
|
71
|
+
*/
|
|
72
|
+
export declare function trimFirstLegToHedge(adapter: ExchangeAdapter, venue: string, symbol: string, firstSide: "buy" | "sell", firstFilled: number, secondFilled: number, decimals: number): Promise<string>;
|
|
73
|
+
export interface PerpCloseResult {
|
|
74
|
+
/** Base units this call closed (measured, both orders). */
|
|
75
|
+
closed: number;
|
|
76
|
+
/** Base units of the requested close still open on the venue (0 = done). > 0 → the caller quarantines. */
|
|
77
|
+
residual: number;
|
|
78
|
+
/** Why a residual remains (residual close failed / position unreadable), or null. */
|
|
79
|
+
note: string | null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Verified reduce-only close of `size` with residual handling (design §12.3, §13.10 #4). placePerpVerified
|
|
83
|
+
* accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
|
|
84
|
+
* not leave the rest silently open, so:
|
|
85
|
+
* 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
|
|
86
|
+
* 2. if it filled less than `size`, re-read the position: the residual is
|
|
87
|
+
* min(size − closed, open size in the closed direction), floored to `decimals`;
|
|
88
|
+
* 3. residual > 0 → ONE more verified reduce-only MARKET close of the residual;
|
|
89
|
+
* 4. re-read; whatever is still open is returned as `residual` (never retried again).
|
|
90
|
+
* Errors of the FIRST close propagate unchanged (definite → nothing happened; ORDER_PENDING →
|
|
91
|
+
* unknown). Once the first close moved the position, a failed / unknown residual close or an
|
|
92
|
+
* unreadable position is reported as `residual` (> 0) + `note` — the caller quarantines.
|
|
93
|
+
*/
|
|
94
|
+
export declare function closePerpVerified(adapter: ExchangeAdapter, venue: string, symbol: string, side: "buy" | "sell", size: string, opts?: {
|
|
95
|
+
place?: () => Promise<unknown>;
|
|
96
|
+
decimals?: number;
|
|
97
|
+
slippage?: ResolvedSlippage;
|
|
98
|
+
stepSize?: string | null;
|
|
99
|
+
}): Promise<PerpCloseResult>;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified perp market order for bot / arb legs (design §12.3).
|
|
3
|
+
*
|
|
4
|
+
* A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
|
|
5
|
+
* when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
|
|
6
|
+
* transport failure after the request leaves the outcome unknown. Every bot perp leg
|
|
7
|
+
* that hedges (or un-hedges) another leg goes through placePerpVerified():
|
|
8
|
+
*
|
|
9
|
+
* - the position on the venue is read BEFORE the order (failure → ORDER_REJECTED,
|
|
10
|
+
* nothing sent);
|
|
11
|
+
* - the order call throws a TYPED definite rejection (isDefiniteOrderFailure) → rethrown
|
|
12
|
+
* unchanged;
|
|
13
|
+
* - otherwise — a resolved call, or ANY other thrown value (untyped Error, transport
|
|
14
|
+
* failure, HL "HL exchange API error (5xx)", Pacifica "POST … failed (5xx)", any
|
|
15
|
+
* non-definite PerpError) — the fill is MEASURED as the position delta (uncached
|
|
16
|
+
* reads, polled): moved ≥ 80% of the size (or a reduce-only order flattened the
|
|
17
|
+
* position) → filled; no such movement, or an unreadable position → ORDER_PENDING
|
|
18
|
+
* (do NOT resubmit, the caller quarantines). An untyped error is never read as a
|
|
19
|
+
* rejection. The one measured exception: a reduce-only order that threw a plain venue
|
|
20
|
+
* message while the position was flat before AND after cannot have done anything →
|
|
21
|
+
* ORDER_REJECTED carrying the venue message and details.reduceOnlyFlat (isPositionGone()).
|
|
22
|
+
* Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
|
|
23
|
+
* the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
|
|
24
|
+
*
|
|
25
|
+
* Venue-final IOC outcome (design §13.10 #4): when the adapter is IocOutcomeCapable and
|
|
26
|
+
* iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
|
|
27
|
+
* (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
|
|
28
|
+
* step, taken from the precision of the order size / reported fill):
|
|
29
|
+
* - final 0 and the position did not move → ORDER_REJECTED (definite, nothing filled);
|
|
30
|
+
* the position DID move → the measured delta is returned (discrepancy logged to stderr);
|
|
31
|
+
* - final 0 < filled ≤ size and the delta agrees → filledSize = the venue-reported size
|
|
32
|
+
* (a final partial fill — callers size the 2nd leg / trim / residual-close from it);
|
|
33
|
+
* the delta never agrees (or the position is unreadable) → ORDER_PENDING.
|
|
34
|
+
* No outcome (adapter not capable, or iocOutcomeOf → null) → the measurement path above.
|
|
35
|
+
*/
|
|
36
|
+
import { invalidateCache } from "../cache.js";
|
|
37
|
+
import { resolveAdapterSlippage } from "../exchanges/slippage.js";
|
|
38
|
+
import { PerpError, extractErrorMessage, isDefiniteOrderFailure, isTransportFailure, isUnknownOutcomeServerError, orderNotSent, orderOutcomeUnknown } from "../errors.js";
|
|
39
|
+
import { fillSizeString, sizeDecimalsOf } from "../exchanges/spot-result.js";
|
|
40
|
+
import { iocLotTolerance, isIocOutcomeCapable } from "../exchanges/ioc-outcome.js";
|
|
41
|
+
import { matchSymbol } from "./strategies/funding-arb-utils.js";
|
|
42
|
+
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
43
|
+
/** Share of the requested size the position must move by to count as filled (measured path only — not for venue-final outcomes). */
|
|
44
|
+
const MIN_FILL_RATIO = 0.8;
|
|
45
|
+
/**
|
|
46
|
+
* Position polls after the order (ms between reads; ~10 s in total). Venue account reads can
|
|
47
|
+
* lag the fill (Lighter testnet: /account showed the position a few seconds after the tx
|
|
48
|
+
* executed), so the window is bounded but long enough; a match ends the polling early.
|
|
49
|
+
*/
|
|
50
|
+
const POLL_DELAYS_MS = [0, 1000, 1500, 2000, 2500, 3000];
|
|
51
|
+
/**
|
|
52
|
+
* HIP-3 dex coin key ("xyz:TSLA"): the order symbol and the position's coin go through
|
|
53
|
+
* the SAME resolution the HL adapter uses for dex markets (resolveSymbol + dex prefix),
|
|
54
|
+
* so "TSLA", "xyz:TSLA" and "XYZ:TSLA" all name one position.
|
|
55
|
+
*/
|
|
56
|
+
function hlDexCoinKey(adapter, sym) {
|
|
57
|
+
const resolved = adapter.resolveSymbol(sym);
|
|
58
|
+
return (resolved.includes(":") ? resolved : `${adapter.dex}:${resolved}`).toUpperCase();
|
|
59
|
+
}
|
|
60
|
+
/** Net signed position (long > 0, short < 0) for `symbol`, read uncached. Throws on a bad read. */
|
|
61
|
+
export async function signedPerpPosition(adapter, symbol) {
|
|
62
|
+
invalidateCache("acct");
|
|
63
|
+
let matches = (posSym) => matchSymbol(posSym, symbol);
|
|
64
|
+
if (adapter instanceof HyperliquidAdapter && adapter.dex !== "") {
|
|
65
|
+
const target = hlDexCoinKey(adapter, symbol);
|
|
66
|
+
matches = (posSym) => hlDexCoinKey(adapter, posSym) === target;
|
|
67
|
+
}
|
|
68
|
+
let net = 0;
|
|
69
|
+
for (const p of await adapter.getPositions()) {
|
|
70
|
+
if (!matches(p.symbol))
|
|
71
|
+
continue;
|
|
72
|
+
const n = Math.abs(Number(p.size));
|
|
73
|
+
if (!Number.isFinite(n))
|
|
74
|
+
throw new PerpError("EXCHANGE_ERROR", `${adapter.name} position size for ${p.symbol} is not a number: ${p.size}`);
|
|
75
|
+
net += p.side === "long" ? n : -n;
|
|
76
|
+
}
|
|
77
|
+
return net;
|
|
78
|
+
}
|
|
79
|
+
/** First HL-style per-order status in a raw result, or null. */
|
|
80
|
+
function firstVenueStatus(raw) {
|
|
81
|
+
const statuses = raw?.response?.data?.statuses;
|
|
82
|
+
return Array.isArray(statuses) && statuses.length > 0 ? statuses[0] : null;
|
|
83
|
+
}
|
|
84
|
+
/** HL-style per-order status in a raw result: error → definite rejection; 0 fill → definite. */
|
|
85
|
+
function checkVenueStatus(venue, what, raw) {
|
|
86
|
+
const st = firstVenueStatus(raw);
|
|
87
|
+
if (st === null)
|
|
88
|
+
return;
|
|
89
|
+
if (st.error)
|
|
90
|
+
throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${String(st.error)}`, { exchange: venue });
|
|
91
|
+
const filled = st.filled;
|
|
92
|
+
if (st.resting === undefined && filled !== undefined && Number(filled.totalSz ?? 0) === 0) {
|
|
93
|
+
throw new PerpError("ORDER_REJECTED", `${venue} ${what}: 0 fill`, { exchange: venue });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function placePerpVerified(adapter, venue, symbol, side, size, opts = {}) {
|
|
97
|
+
const what = `perp ${side} ${size} ${symbol}${opts.reduceOnly ? " (reduce-only)" : ""}`;
|
|
98
|
+
const requested = Number(size);
|
|
99
|
+
if (!(requested > 0))
|
|
100
|
+
throw new PerpError("INVALID_PARAMS", `${what}: size must be positive`);
|
|
101
|
+
let before;
|
|
102
|
+
try {
|
|
103
|
+
before = await signedPerpPosition(adapter, symbol);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
throw orderNotSent(venue, `${what} (pre-order position read failed)`, e);
|
|
107
|
+
}
|
|
108
|
+
/** Base units moved in the order's direction (null when no fill is observable yet) and the last read position. */
|
|
109
|
+
const measure = async () => {
|
|
110
|
+
let last = before;
|
|
111
|
+
for (const delay of POLL_DELAYS_MS) {
|
|
112
|
+
if (delay > 0)
|
|
113
|
+
await new Promise(r => setTimeout(r, delay));
|
|
114
|
+
last = await signedPerpPosition(adapter, symbol);
|
|
115
|
+
const moved = side === "buy" ? last - before : before - last;
|
|
116
|
+
if (moved >= requested * MIN_FILL_RATIO)
|
|
117
|
+
return { moved: Math.min(moved, requested), last };
|
|
118
|
+
if (opts.reduceOnly && moved > 0 && Math.abs(last) < requested * 1e-6)
|
|
119
|
+
return { moved, last }; // flattened a smaller position
|
|
120
|
+
}
|
|
121
|
+
return { moved: null, last };
|
|
122
|
+
};
|
|
123
|
+
let raw;
|
|
124
|
+
try {
|
|
125
|
+
raw = opts.place !== undefined
|
|
126
|
+
? await opts.place()
|
|
127
|
+
: await adapter.marketOrder(symbol, side, size, { reduceOnly: opts.reduceOnly === true, slippage: opts.slippage ?? resolveAdapterSlippage(adapter, null) });
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
if (isDefiniteOrderFailure(e)) {
|
|
131
|
+
if (opts.reduceOnly === true && e instanceof PerpError && e.structured.code === "POSITION_NOT_FOUND") {
|
|
132
|
+
// The adapter read the position, found none and sent nothing (QFEX; details.sent === false):
|
|
133
|
+
// the same typed "nothing to reduce" as below (isPositionGone()) — no poll, no quarantine.
|
|
134
|
+
throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${e.message} (no ${symbol} position to reduce)`, { exchange: venue, reduceOnlyFlat: true });
|
|
135
|
+
}
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
// Not a typed rejection → the order may have been placed: measure, never assume.
|
|
139
|
+
let m;
|
|
140
|
+
try {
|
|
141
|
+
m = await measure();
|
|
142
|
+
}
|
|
143
|
+
catch (readErr) {
|
|
144
|
+
throw orderOutcomeUnknown(venue, what, e, `Do NOT resubmit — check the ${venue} ${symbol} position (position read also failed: ${readErr instanceof Error ? readErr.message : String(readErr)})`);
|
|
145
|
+
}
|
|
146
|
+
if (m.moved !== null)
|
|
147
|
+
return { filledSize: m.moved, raw: null };
|
|
148
|
+
const postSend = e instanceof PerpError || isTransportFailure(e) || isUnknownOutcomeServerError(e);
|
|
149
|
+
const flat = (n) => Math.abs(n) < requested * 1e-6;
|
|
150
|
+
const lot = !postSend ? /not a multiple of lot size (\d+(?:\.\d+)?)/.exec(extractErrorMessage(e)) : null;
|
|
151
|
+
if (lot !== null && flat(m.last - before)) {
|
|
152
|
+
// Venue lot-size validation rejection (e.g. Pacifica 4xx) and the position did not move: not placed.
|
|
153
|
+
throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${extractErrorMessage(e)}`, { exchange: venue, lotSize: Number(lot[1]) });
|
|
154
|
+
}
|
|
155
|
+
if (opts.reduceOnly && !postSend && flat(before) && flat(m.last)) {
|
|
156
|
+
// Reduce-only cannot open a position: flat before and after = nothing happened (position already gone).
|
|
157
|
+
throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${extractErrorMessage(e)} (no ${symbol} position to reduce)`, { exchange: venue, reduceOnlyFlat: true });
|
|
158
|
+
}
|
|
159
|
+
throw orderOutcomeUnknown(venue, what, e, `Do NOT resubmit — the order may still execute. Check the ${venue} ${symbol} position`);
|
|
160
|
+
}
|
|
161
|
+
// Venue-final IOC outcome (§13.10 #4): the venue reported what this order filled; nothing more can fill.
|
|
162
|
+
const outcome = isIocOutcomeCapable(adapter) ? adapter.iocOutcomeOf(raw) : null;
|
|
163
|
+
if (outcome !== null) {
|
|
164
|
+
const reported = Number(outcome.filledSize);
|
|
165
|
+
if (!(Number.isFinite(reported) && reported >= 0)) {
|
|
166
|
+
throw orderOutcomeUnknown(venue, what, new Error(`venue reported an unusable final fill "${outcome.filledSize}"`));
|
|
167
|
+
}
|
|
168
|
+
const tol = iocLotTolerance(size, outcome.filledSize, opts.stepSize);
|
|
169
|
+
if (reported > requested + tol) {
|
|
170
|
+
throw orderOutcomeUnknown(venue, what, new Error(`venue reported a final fill of ${outcome.filledSize}, more than the ${size} ordered`));
|
|
171
|
+
}
|
|
172
|
+
let measured = 0;
|
|
173
|
+
let last = before;
|
|
174
|
+
try {
|
|
175
|
+
for (const delay of POLL_DELAYS_MS) {
|
|
176
|
+
if (delay > 0)
|
|
177
|
+
await new Promise(r => setTimeout(r, delay));
|
|
178
|
+
last = await signedPerpPosition(adapter, symbol);
|
|
179
|
+
measured = side === "buy" ? last - before : before - last;
|
|
180
|
+
// 0 reported: keep polling the whole schedule so a lagging fill is still seen.
|
|
181
|
+
if (reported > 0 ? Math.abs(measured - reported) <= tol : measured > tol)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (readErr) {
|
|
186
|
+
throw orderOutcomeUnknown(venue, what, new Error(`venue reported a final fill of ${outcome.filledSize} but the position could not be read: ${readErr instanceof Error ? readErr.message : String(readErr)}`));
|
|
187
|
+
}
|
|
188
|
+
if (reported === 0) {
|
|
189
|
+
if (!(measured > tol)) {
|
|
190
|
+
const venueError = firstVenueStatus(raw)?.error;
|
|
191
|
+
// Reduce-only with the position flat before and after: nothing to reduce (isPositionGone()).
|
|
192
|
+
const flat = (n) => Math.abs(n) < requested * 1e-6;
|
|
193
|
+
const gone = opts.reduceOnly === true && flat(before) && flat(last);
|
|
194
|
+
throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${venueError ? String(venueError) : "IOC filled nothing"} (venue-final, position unchanged)${gone ? ` (no ${symbol} position to reduce)` : ""}`, { exchange: venue, ...(gone ? { reduceOnlyFlat: true } : {}) });
|
|
195
|
+
}
|
|
196
|
+
process.stderr.write(`[warn] ${venue} ${what}: venue reported a final fill of 0 but the position moved ${measured} — using the measured fill\n`);
|
|
197
|
+
return { filledSize: Math.min(measured, requested), raw };
|
|
198
|
+
}
|
|
199
|
+
if (Math.abs(measured - reported) > tol) {
|
|
200
|
+
throw orderOutcomeUnknown(venue, what, new Error(`venue reported a final fill of ${outcome.filledSize} but the ${symbol} position moved ${measured} (before ${before})`), `Do NOT resubmit — check the ${venue} ${symbol} position`);
|
|
201
|
+
}
|
|
202
|
+
return { filledSize: Math.min(reported, requested), raw };
|
|
203
|
+
}
|
|
204
|
+
checkVenueStatus(venue, what, raw);
|
|
205
|
+
let moved;
|
|
206
|
+
try {
|
|
207
|
+
moved = (await measure()).moved;
|
|
208
|
+
}
|
|
209
|
+
catch (readErr) {
|
|
210
|
+
throw orderOutcomeUnknown(venue, what, new Error(`order acknowledged but the position could not be read: ${readErr instanceof Error ? readErr.message : String(readErr)}`));
|
|
211
|
+
}
|
|
212
|
+
if (moved === null) {
|
|
213
|
+
throw orderOutcomeUnknown(venue, what, new Error(`order acknowledged but the ${symbol} position did not move (before ${before})`), `Do NOT resubmit — the order may be unconfirmed. Check the ${venue} ${symbol} position`);
|
|
214
|
+
}
|
|
215
|
+
return { filledSize: moved, raw };
|
|
216
|
+
}
|
|
217
|
+
/** Lot size from a definite lot-size rejection (placePerpVerified ORDER_REJECTED details.lotSize), else null. */
|
|
218
|
+
export function rejectedLotSize(e) {
|
|
219
|
+
if (!(e instanceof PerpError) || e.structured.code !== "ORDER_REJECTED")
|
|
220
|
+
return null;
|
|
221
|
+
const lot = e.structured.details?.lotSize;
|
|
222
|
+
return typeof lot === "number" && lot > 0 ? lot : null;
|
|
223
|
+
}
|
|
224
|
+
/** true when `e` says the order outcome is unknown (quarantine, never retry / roll back blindly). */
|
|
225
|
+
export function isOutcomeUnknown(e) {
|
|
226
|
+
return e instanceof PerpError && e.structured.code === "ORDER_PENDING";
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Second-leg residual correction (design §12.3): the second leg is sized to the first
|
|
230
|
+
* leg's VERIFIED fill but may itself fill less (≥ 80% measured, or any venue-final partial fill). Reduce the first leg by exactly
|
|
231
|
+
* this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
|
|
232
|
+
* verified, never the venue's whole position. `firstSide` is the side the first leg
|
|
233
|
+
* opened with. Returns the excess closed ("0" when the legs already match). Throws when
|
|
234
|
+
* the correction was rejected or is unverified: the residual is unhedged → the caller
|
|
235
|
+
* quarantines.
|
|
236
|
+
*/
|
|
237
|
+
export async function trimFirstLegToHedge(adapter, venue, symbol, firstSide, firstFilled, secondFilled, decimals) {
|
|
238
|
+
const excess = fillSizeString(firstFilled - secondFilled, decimals);
|
|
239
|
+
if (!(Number(excess) > 0))
|
|
240
|
+
return "0";
|
|
241
|
+
const c = await closePerpVerified(adapter, venue, symbol, firstSide === "buy" ? "sell" : "buy", excess, { decimals });
|
|
242
|
+
if (c.residual > 0) {
|
|
243
|
+
throw new PerpError("ORDER_PARTIAL", `${venue} trim of ${excess} ${symbol} closed ${c.closed}; ${c.residual} left unhedged${c.note !== null ? ` (${c.note})` : ""}`, { exchange: venue });
|
|
244
|
+
}
|
|
245
|
+
return excess;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Verified reduce-only close of `size` with residual handling (design §12.3, §13.10 #4). placePerpVerified
|
|
249
|
+
* accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
|
|
250
|
+
* not leave the rest silently open, so:
|
|
251
|
+
* 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
|
|
252
|
+
* 2. if it filled less than `size`, re-read the position: the residual is
|
|
253
|
+
* min(size − closed, open size in the closed direction), floored to `decimals`;
|
|
254
|
+
* 3. residual > 0 → ONE more verified reduce-only MARKET close of the residual;
|
|
255
|
+
* 4. re-read; whatever is still open is returned as `residual` (never retried again).
|
|
256
|
+
* Errors of the FIRST close propagate unchanged (definite → nothing happened; ORDER_PENDING →
|
|
257
|
+
* unknown). Once the first close moved the position, a failed / unknown residual close or an
|
|
258
|
+
* unreadable position is reported as `residual` (> 0) + `note` — the caller quarantines.
|
|
259
|
+
*/
|
|
260
|
+
export async function closePerpVerified(adapter, venue, symbol, side, size, opts = {}) {
|
|
261
|
+
const requested = Number(size);
|
|
262
|
+
const dec = opts.decimals ?? sizeDecimalsOf(size);
|
|
263
|
+
const slip = { ...(opts.slippage !== undefined ? { slippage: opts.slippage } : {}), ...(opts.stepSize !== undefined ? { stepSize: opts.stepSize } : {}) };
|
|
264
|
+
const first = await placePerpVerified(adapter, venue, symbol, side, size, { reduceOnly: true, ...slip, ...(opts.place !== undefined ? { place: opts.place } : {}) });
|
|
265
|
+
let closed = first.filledSize;
|
|
266
|
+
/** Residual of this close still open on the venue (closing a long = sell → the long part). */
|
|
267
|
+
const openResidual = async () => {
|
|
268
|
+
const pos = await signedPerpPosition(adapter, symbol);
|
|
269
|
+
const openInDir = side === "sell" ? Math.max(pos, 0) : Math.max(-pos, 0);
|
|
270
|
+
return Number(fillSizeString(Math.min(requested - closed, openInDir), dec));
|
|
271
|
+
};
|
|
272
|
+
if (closed >= requested * (1 - 1e-6))
|
|
273
|
+
return { closed, residual: 0, note: null };
|
|
274
|
+
let residual;
|
|
275
|
+
try {
|
|
276
|
+
residual = await openResidual();
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
return { closed, residual: Number(fillSizeString(requested - closed, dec)), note: `position unreadable after a partial close: ${extractErrorMessage(e)}` };
|
|
280
|
+
}
|
|
281
|
+
if (!(residual > 0))
|
|
282
|
+
return { closed, residual: 0, note: null };
|
|
283
|
+
const resStr = fillSizeString(residual, dec);
|
|
284
|
+
try {
|
|
285
|
+
closed += (await placePerpVerified(adapter, venue, symbol, side, resStr, { reduceOnly: true, ...slip })).filledSize;
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
return { closed, residual, note: `residual close of ${resStr} ${isOutcomeUnknown(e) ? "unconfirmed" : "failed"}: ${extractErrorMessage(e)}` };
|
|
289
|
+
}
|
|
290
|
+
if (closed >= requested * (1 - 1e-6))
|
|
291
|
+
return { closed, residual: 0, note: null };
|
|
292
|
+
try {
|
|
293
|
+
residual = await openResidual();
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
return { closed, residual: Number(fillSizeString(requested - closed, dec)), note: `position unreadable after the residual close: ${extractErrorMessage(e)}` };
|
|
297
|
+
}
|
|
298
|
+
return { closed, residual, note: residual > 0 ? `residual close of ${resStr} filled partially` : null };
|
|
299
|
+
}
|
package/dist/bot/presets.d.ts
CHANGED
|
@@ -7,5 +7,12 @@ export interface Preset {
|
|
|
7
7
|
buildConfig: (exchange: string, symbol: string) => BotConfig;
|
|
8
8
|
}
|
|
9
9
|
export declare const PRESETS: Preset[];
|
|
10
|
+
/**
|
|
11
|
+
* `perp strategy preset --size`: replaces the preset's size field — grid `size` (total base
|
|
12
|
+
* size, split across the grids), DCA `amount` (base size per order), arb `size_usd` ($ per leg).
|
|
13
|
+
*/
|
|
14
|
+
export declare function withPresetSize<T extends {
|
|
15
|
+
strategy: unknown;
|
|
16
|
+
}>(config: T, size: string): T;
|
|
10
17
|
export declare function getPreset(name: string): Preset | undefined;
|
|
11
18
|
export declare function getPresetsByStrategy(strategy: string): Preset[];
|
package/dist/bot/presets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PerpError } from "../errors.js";
|
|
1
2
|
const DEFAULT_RISK = {
|
|
2
3
|
max_position_usd: 1000,
|
|
3
4
|
max_daily_loss: 100,
|
|
@@ -289,6 +290,22 @@ export const PRESETS = [
|
|
|
289
290
|
arbStandard,
|
|
290
291
|
arbAggressive,
|
|
291
292
|
];
|
|
293
|
+
/**
|
|
294
|
+
* `perp strategy preset --size`: replaces the preset's size field — grid `size` (total base
|
|
295
|
+
* size, split across the grids), DCA `amount` (base size per order), arb `size_usd` ($ per leg).
|
|
296
|
+
*/
|
|
297
|
+
export function withPresetSize(config, size) {
|
|
298
|
+
const n = Number(size);
|
|
299
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
300
|
+
throw new PerpError("INVALID_PARAMS", `--size must be a positive number (got ${JSON.stringify(size)})`);
|
|
301
|
+
}
|
|
302
|
+
const s = config.strategy;
|
|
303
|
+
const field = "size_usd" in s ? "size_usd" : "amount" in s ? "amount" : "size" in s ? "size" : null;
|
|
304
|
+
if (field === null) {
|
|
305
|
+
throw new PerpError("INVALID_PARAMS", `preset strategy ${String(s.type)} has no size field to override`);
|
|
306
|
+
}
|
|
307
|
+
return { ...config, strategy: { ...s, [field]: n } };
|
|
308
|
+
}
|
|
292
309
|
export function getPreset(name) {
|
|
293
310
|
return PRESETS.find(p => p.name === name);
|
|
294
311
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position quarantine (design §12.3) — GLOBAL per symbol.
|
|
3
|
+
*
|
|
4
|
+
* A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
|
|
5
|
+
* confirmed, a close whose post-trade verification failed, or a position left unhedged
|
|
6
|
+
* by a failed rollback must NOT be retried automatically: a retry can double-buy,
|
|
7
|
+
* double-sell or open a naked perp. Such a symbol is quarantined here, persisted in
|
|
8
|
+
* ~/.perp/bot-quarantine.json, and skipped by EVERY automated path — the bot strategies
|
|
9
|
+
* (funding-arb, funding-arb-v2, spot-perp-arb, funding-auto), `perp arb exec` (perp-perp
|
|
10
|
+
* and spot:<venue>) and the `perp arb auto` / `arb dex-auto` daemons — no entry, no close,
|
|
11
|
+
* no rotation — until a human clears it (`perp arb close` refuses it without --force):
|
|
12
|
+
*
|
|
13
|
+
* perp strategy quarantine # list
|
|
14
|
+
* perp strategy quarantine-clear <symbol> [--strategy <source>]
|
|
15
|
+
*
|
|
16
|
+
* Entries are stored per source ("<source>:<KEY>", source = strategy name or CLI path)
|
|
17
|
+
* but CHECKED by the canonical key alone (quarantineKey: HIP-3 dex prefix and "-PERP"
|
|
18
|
+
* stripped, DENOMINATION_ALIASES applied, uppercased — "xyz:TSLA" ≡ "TSLA", "KPEPE" ≡
|
|
19
|
+
* "1000PEPE"): one venue's unknown outcome blocks the underlying for all strategies,
|
|
20
|
+
* venues and processes. `entry.symbol` keeps the symbol as the source reported it. The file is re-read on every check, so a clear
|
|
21
|
+
* from the CLI takes effect in running bots on their next tick (no restart). Only an
|
|
22
|
+
* entry whose persist failed is held in process memory (logged CRITICAL); it holds
|
|
23
|
+
* until that process stops.
|
|
24
|
+
*
|
|
25
|
+
* Network scope: an entry records the network it was set on (`network`); a process that called
|
|
26
|
+
* setQuarantineNetwork(n) sees only entries of n plus entries of unknown network (null — set by
|
|
27
|
+
* a process that never declared its network: they apply to EVERY network, fail closed). Entries
|
|
28
|
+
* written before network scoping have no tag and are MAINNET (like the arb state's legacy file).
|
|
29
|
+
* A process that never declared its network sees every entry (fail closed). Mainnet / unknown
|
|
30
|
+
* entries keep the legacy key "<source>:<KEY>"; testnet ones are keyed "testnet:<source>:<KEY>".
|
|
31
|
+
* Every load → modify → save of the file holds the cross-process lock (withFileLock).
|
|
32
|
+
*/
|
|
33
|
+
import type { Network } from "../exchanges/venue-types.js";
|
|
34
|
+
export declare const QUARANTINE_FILE: string;
|
|
35
|
+
export interface QuarantineEntry {
|
|
36
|
+
/** Who quarantined it: a strategy name or a CLI path ("arb-exec"). */
|
|
37
|
+
strategy: string;
|
|
38
|
+
/** The symbol as the source reported it (display); matched via quarantineKey(). */
|
|
39
|
+
symbol: string;
|
|
40
|
+
/** What happened (the CRITICAL log line). */
|
|
41
|
+
reason: string;
|
|
42
|
+
/** What the human must check before clearing. */
|
|
43
|
+
remediation: string;
|
|
44
|
+
time: string;
|
|
45
|
+
/** Network the entry was set on; null = unknown (applies to every network). Legacy untagged entries load as "mainnet". */
|
|
46
|
+
network: Network | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Canonical quarantine key for a symbol — the ONLY way a symbol is compared here (set,
|
|
50
|
+
* check, clear): uppercased, HIP-3 dex prefix ("xyz:") and "-PERP" suffix stripped,
|
|
51
|
+
* DENOMINATION_ALIASES applied (KPEPE → 1000PEPE, UBTC → BTC).
|
|
52
|
+
*/
|
|
53
|
+
export declare function quarantineKey(symbol: string): string;
|
|
54
|
+
/** Declare the network this process trades on: entries are set on it and checked against it. */
|
|
55
|
+
export declare function setQuarantineNetwork(network: Network): void;
|
|
56
|
+
/** Test hook: back to "network not declared". */
|
|
57
|
+
export declare function resetQuarantineNetwork(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Quarantine `symbol` (for every strategy / CLI path) and log CRITICAL with the
|
|
60
|
+
* remediation. `source` records who did it. If persisting fails the entry is held in
|
|
61
|
+
* this process's memory and the failure is logged loudly.
|
|
62
|
+
*/
|
|
63
|
+
export declare function quarantineSymbol(source: string, symbol: string, reason: string, remediation: string, log: (msg: string) => void): void;
|
|
64
|
+
/**
|
|
65
|
+
* The quarantine entry blocking `symbol` (any source), or null. Re-reads the file on
|
|
66
|
+
* every call. An unreadable quarantine file fails CLOSED (a synthetic entry, logged) —
|
|
67
|
+
* never "not quarantined".
|
|
68
|
+
*/
|
|
69
|
+
export declare function quarantineEntryFor(symbol: string, log: (msg: string) => void): QuarantineEntry | null;
|
|
70
|
+
/** true when `symbol` is quarantined by any strategy / CLI path (see quarantineEntryFor). */
|
|
71
|
+
export declare function isQuarantined(symbol: string, log: (msg: string) => void): boolean;
|
|
72
|
+
/** Entries that apply on this process's network (every entry when it never declared one). */
|
|
73
|
+
export declare function listQuarantine(): QuarantineEntry[];
|
|
74
|
+
/** Remove entries (that apply on this process's network) whose quarantineKey matches symbol's (one source, or every source when null). Returns the removed entries. */
|
|
75
|
+
export declare function clearQuarantine(symbol: string, strategy: string | null): QuarantineEntry[];
|