perp-cli 0.14.3 → 0.16.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.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +138 -405
- package/README.zh.md +243 -0
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +85 -15
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +12 -5
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
- package/dist/bot/strategies/funding-arb-utils.js +53 -20
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +3 -3
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +126 -6
- package/dist/commands/account.js +168 -24
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +23 -81
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +175 -97
- package/dist/commands/arb.d.ts +20 -0
- package/dist/commands/arb.js +107 -163
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +395 -100
- package/dist/commands/options.d.ts +14 -0
- package/dist/commands/options.js +151 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +455 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +83 -55
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +67 -62
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +3 -3
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +35 -3
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +16 -12
- package/dist/exchanges/arcus/adapter.js +45 -18
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +30 -10
- package/dist/exchanges/aster.js +153 -53
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +8 -2
- package/dist/exchanges/canonical-symbols.js +46 -2
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +73 -0
- package/dist/exchanges/derive/options.js +379 -0
- package/dist/exchanges/extended/adapter.d.ts +96 -0
- package/dist/exchanges/extended/adapter.js +241 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +133 -0
- package/dist/exchanges/extended/public.js +436 -0
- package/dist/exchanges/grvt/adapter.d.ts +100 -0
- package/dist/exchanges/grvt/adapter.js +283 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
- package/dist/exchanges/hyperliquid-spot.js +24 -16
- package/dist/exchanges/hyperliquid.d.ts +199 -22
- package/dist/exchanges/hyperliquid.js +550 -202
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +157 -13
- package/dist/exchanges/interface.js +47 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +67 -12
- package/dist/exchanges/lighter-public.js +250 -82
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +10 -5
- package/dist/exchanges/lighter-spot.js +105 -97
- package/dist/exchanges/lighter.d.ts +80 -21
- package/dist/exchanges/lighter.js +312 -105
- package/dist/exchanges/ondo/adapter.d.ts +38 -13
- package/dist/exchanges/ondo/adapter.js +86 -27
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +35 -3
- package/dist/exchanges/ondo/public.js +75 -32
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +8 -2
- package/dist/exchanges/pacifica-spot.js +26 -18
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +130 -0
- package/dist/exchanges/pacifica-vaults.js +277 -0
- package/dist/exchanges/pacifica.d.ts +108 -10
- package/dist/exchanges/pacifica.js +428 -171
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +69 -13
- package/dist/exchanges/qfex/adapter.js +102 -17
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +65 -12
- package/dist/exchanges/qfex/public.js +131 -25
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +160 -0
- package/dist/exchanges/risex/adapter.js +842 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +8 -8
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +61 -0
- package/dist/exchanges/txflow/adapter.js +150 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +155 -0
- package/dist/exchanges/txflow/public.d.ts +87 -0
- package/dist/exchanges/txflow/public.js +355 -0
- package/dist/exchanges/unknown-symbol.d.ts +34 -0
- package/dist/exchanges/unknown-symbol.js +56 -0
- package/dist/exchanges/variational/adapter.d.ts +67 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +52 -10
- package/dist/exchanges/venue-types.js +82 -11
- package/dist/exchanges/venue.d.ts +150 -11
- package/dist/exchanges/venue.js +126 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +19 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +20 -8
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +18 -6
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +15 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +131 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +6 -4
- package/dist/funding/legacy-public.js +42 -41
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +95 -6
- package/dist/funding/rates.js +99 -19
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +64 -0
- package/dist/mcp-server.js +211 -68
- package/dist/pacifica/client.d.ts +69 -5
- package/dist/pacifica/client.js +181 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +36 -63
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +980 -0
- package/dist/ws/index.js +4460 -0
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +996 -0
- package/dist/ws/node.js +4554 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +71 -10
- package/skills/perp-cli/SKILL.md +153 -36
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +22 -12
- package/skills/perp-cli/references/commands.md +143 -17
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +523 -0
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { assertCandleInterval } from "./interface.js";
|
|
2
|
+
import { isLighterMarketLive } from "../ws/lighter-market.js";
|
|
1
3
|
import { alignProtectionPrice, floorSize, formatComputedPrice, mulDecimal, slippagePrice, toUnitsExact, unitsToString } from "./precision.js";
|
|
2
4
|
import { unsupported } from "./trading-v2.js";
|
|
3
5
|
import { LocalEvmSigner } from "../signer/index.js";
|
|
4
6
|
import { isExpired } from "../agent-wallet/expiry.js";
|
|
5
|
-
import { PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure, unknownActionRemediation, parsePositiveDecimal, DEFINITE_ORDER_FAILURE_CODES } from "../errors.js";
|
|
7
|
+
import { PerpError, classifyError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure, unknownActionRemediation, parsePositiveDecimal, DEFINITE_ORDER_FAILURE_CODES, noAccountKeyError } from "../errors.js";
|
|
6
8
|
import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
|
|
7
9
|
import { lighterFamilyNetworks, LIGHTER_RH_RATE_LIMIT, LIGHTER_RH_RESERVED_KEY_INDEXES, LIGHTER_GTT_EXPIRY_MS, LIGHTER_CLIENT_ORDER_INDEX_MAX, LIGHTER_REFERENCE_PRICE_SOURCE, } from "./venues/lighter-family.js";
|
|
8
10
|
import { acquireRate, rateLimitedError } from "./rate-limiter.js";
|
|
9
|
-
import { lighterWafChallenge } from "./lighter-public.js";
|
|
11
|
+
import { fetchLighterFundingStats, lighterFundingStatsWarning, LIGHTER_BOOK_ORDERS_LIMIT, lighterBook, lighterWafChallenge } from "./lighter-public.js";
|
|
10
12
|
import { fetchAndCache, invalidateCache, setCached, withCache, TTL_ACCOUNT } from "../cache.js";
|
|
11
13
|
import { LighterWasmSigner } from "./lighter-signer.js";
|
|
12
14
|
/** Lighter trigger orderType enum (matches WASM signer). */
|
|
@@ -35,12 +37,35 @@ export function classifyTriggerOrderType(side, triggerPrice, markPrice, isMarket
|
|
|
35
37
|
}
|
|
36
38
|
/** Lighter order time-in-force enum (lighter-go / SDK `TimeInForce`): 0 IOC, 1 GTT, 2 post-only. */
|
|
37
39
|
export const LIGHTER_TIF = { IOC: 0, GTT: 1, POST_ONLY: 2 };
|
|
40
|
+
/**
|
|
41
|
+
* One GET /api/v1/fundings row → a funding history row (fraction per 1 h, signed).
|
|
42
|
+
* `rate` is percent per hour, unsigned; `direction` "long" = longs pay (positive), "short" = shorts pay.
|
|
43
|
+
*/
|
|
44
|
+
export function lighterFundingRow(venue, f) {
|
|
45
|
+
const x = f;
|
|
46
|
+
if (typeof x.timestamp !== "number" || !Number.isFinite(x.timestamp) || x.timestamp <= 0) {
|
|
47
|
+
throw new PerpError("EXCHANGE_ERROR", `${venue} /fundings: row without a numeric timestamp: ${JSON.stringify(f).slice(0, 160)}`, { exchange: venue });
|
|
48
|
+
}
|
|
49
|
+
if (typeof x.rate !== "string" || !/^\d+(\.\d+)?$/.test(x.rate)) {
|
|
50
|
+
throw new PerpError("EXCHANGE_ERROR", `${venue} /fundings: row without an unsigned decimal rate: ${JSON.stringify(f).slice(0, 160)}`, { exchange: venue });
|
|
51
|
+
}
|
|
52
|
+
if (x.direction !== "long" && x.direction !== "short") {
|
|
53
|
+
throw new PerpError("EXCHANGE_ERROR", `${venue} /fundings: unknown direction ${JSON.stringify(x.direction)}`, { exchange: venue });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
time: x.timestamp * 1000,
|
|
57
|
+
rate: mulDecimal(x.rate, x.direction === "long" ? "0.01" : "-0.01"),
|
|
58
|
+
price: null,
|
|
59
|
+
intervalHours: 1,
|
|
60
|
+
rateKind: "lastSettled",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
38
63
|
/** Decimal step for `decimals` digits ("1" for 0, "0.01" for 2). */
|
|
39
64
|
function lighterStep(decimals) {
|
|
40
65
|
return decimals <= 0 ? "1" : `0.${"0".repeat(decimals - 1)}1`;
|
|
41
66
|
}
|
|
42
67
|
/**
|
|
43
|
-
* CLI format rule for a Lighter client order id
|
|
68
|
+
* CLI format rule for a Lighter client order id: the venue's
|
|
44
69
|
* `clientOrderIndex` is numeric, so the id must be a plain decimal integer string
|
|
45
70
|
* (digits only, no sign / leading zero / exponent) in 1 ≤ n ≤ 2^48 − 1.
|
|
46
71
|
* Anything else → INVALID_PARAMS before anything is sent.
|
|
@@ -369,6 +394,65 @@ export function lighterRowCreatedMs(row) {
|
|
|
369
394
|
* leverage = floor(10000 / bps), which returns the integer leverage that was set. margin_mode:
|
|
370
395
|
* 0 cross, 1 isolated (the values updateLeverage signs). Anything else throws — never a guess.
|
|
371
396
|
*/
|
|
397
|
+
/**
|
|
398
|
+
* Balance of one Lighter-family account object (GET /account) — the one implementation behind
|
|
399
|
+
* LighterAdapter.getBalance() and the keyless `account balance --address`. Equity includes the
|
|
400
|
+
* spot collateral asset (USDC on Lighter, USDG on Lighter RH; separate from perp collateral).
|
|
401
|
+
*/
|
|
402
|
+
export function lighterBalanceOf(acct, venue, collateralSymbol) {
|
|
403
|
+
const totalAsset = parseFiniteVenueNumber(acct.total_asset_value, "total_asset_value", "lighter");
|
|
404
|
+
const available = parseFiniteVenueNumber(acct.available_balance, "available_balance", "lighter");
|
|
405
|
+
const collateral = parseFiniteVenueNumber(acct.collateral, "collateral", "lighter");
|
|
406
|
+
const unrealizedPnl = acct.positions?.reduce((sum, p) => sum + parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "lighter"), 0) ?? 0;
|
|
407
|
+
// Include spot collateral balance (USDC on Lighter, USDG on Lighter RH; separate from perp collateral)
|
|
408
|
+
// An account without that asset holds none of it (0); an unreadable assets list / balance throws.
|
|
409
|
+
let spotUsdc = 0;
|
|
410
|
+
if (!Array.isArray(acct.assets)) {
|
|
411
|
+
throw new PerpError("EXCHANGE_ERROR", `${venue} account has no assets list — spot ${collateralSymbol} balance unknown`, { exchange: venue });
|
|
412
|
+
}
|
|
413
|
+
const usdcAsset = acct.assets.find(a => a?.symbol === collateralSymbol);
|
|
414
|
+
if (usdcAsset !== undefined)
|
|
415
|
+
spotUsdc = parseFiniteVenueNumber(usdcAsset.balance, `assets.${collateralSymbol}.balance`, "lighter");
|
|
416
|
+
return {
|
|
417
|
+
equity: String(totalAsset + spotUsdc),
|
|
418
|
+
available: String(available),
|
|
419
|
+
marginUsed: String(Math.max(0, collateral - available)),
|
|
420
|
+
unrealizedPnl: String(unrealizedPnl),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Open positions of one Lighter account (`GET /account` `accounts[].positions[]`): the venue lists
|
|
425
|
+
* every market the account ever traded or configured, so rows with `position` 0 are dropped. The
|
|
426
|
+
* ONE mapper of the keyed `getPositions` and the public-address read; `priceDecimalsOf` formats
|
|
427
|
+
* the computed mark (position_value / size) to the market's price decimals when known.
|
|
428
|
+
*/
|
|
429
|
+
export function lighterPositionRows(positions, venue, priceDecimalsOf) {
|
|
430
|
+
return (positions ?? [])
|
|
431
|
+
.filter((p) => parseFiniteVenueNumber(p.position, "position", "lighter") !== 0)
|
|
432
|
+
.map((p) => {
|
|
433
|
+
const posSize = parseFiniteVenueNumber(p.position, "position", "lighter");
|
|
434
|
+
const positionValue = parseFiniteVenueNumber(p.position_value, "position_value", "lighter");
|
|
435
|
+
return {
|
|
436
|
+
symbol: String(p.symbol || `Market-${p.market_id}`),
|
|
437
|
+
side: (Number(p.sign) > 0 ? "long" : "short"),
|
|
438
|
+
size: String(Math.abs(posSize)),
|
|
439
|
+
entryPrice: String(p.avg_entry_price || "0"),
|
|
440
|
+
markPrice: (() => {
|
|
441
|
+
if (posSize === 0)
|
|
442
|
+
return "0";
|
|
443
|
+
const rawMark = positionValue / Math.abs(posSize);
|
|
444
|
+
const priceDec = priceDecimalsOf(String(p.symbol || ""));
|
|
445
|
+
return priceDec !== undefined ? rawMark.toFixed(priceDec) : formatComputedPrice(rawMark);
|
|
446
|
+
})(),
|
|
447
|
+
// The venue reports "0" when the position has no liquidation price (e.g. cross margin
|
|
448
|
+
// well covered): that is "none", not a price.
|
|
449
|
+
liquidationPrice: Number(p.liquidation_price) > 0 ? String(p.liquidation_price) : null,
|
|
450
|
+
unrealizedPnl: String(p.unrealized_pnl || "0"),
|
|
451
|
+
// The leverage CONFIGURED for the market (what `trade leverage` set) — not notional / equity.
|
|
452
|
+
...lighterPositionSettings(p, venue),
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
}
|
|
372
456
|
export function lighterPositionSettings(p, venue) {
|
|
373
457
|
const pct = typeof p.initial_margin_fraction === "string" && /^\d+(\.\d+)?$/.test(p.initial_margin_fraction) ? Number(p.initial_margin_fraction) : NaN;
|
|
374
458
|
const bps = Math.round(pct * 100);
|
|
@@ -409,8 +493,27 @@ export function lighterSelectAccount(accounts, accountIndex, venue) {
|
|
|
409
493
|
* window is unknown (SYMBOL_NOT_FOUND) until the entry expires.
|
|
410
494
|
*/
|
|
411
495
|
export const LIGHTER_MARKET_LIST_TTL_MS = 5 * 60_000;
|
|
496
|
+
/** GET /api/v1/candles resolutions, live-verified 2026-10-19 on lighter + lighter-rh mainnet and lighter testnet (3m / 2h / 6h / 8h / 1w / 1M / 2m: code 20001 "invalid param"). */
|
|
497
|
+
export const LIGHTER_CANDLE_INTERVALS = {
|
|
498
|
+
"1m": 60_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000,
|
|
499
|
+
"1h": 3_600_000, "4h": 14_400_000, "12h": 43_200_000, "1d": 86_400_000,
|
|
500
|
+
};
|
|
501
|
+
/** "Returns at most 500 candles per call" (apidocs.lighter.xyz/reference/candles). */
|
|
502
|
+
export const LIGHTER_CANDLES_PER_CALL = 500;
|
|
503
|
+
/** Max `limit` of GET /api/v1/recentTrades. */
|
|
504
|
+
export const LIGHTER_RECENT_TRADES_MAX = 100;
|
|
505
|
+
/** Requests one `market kline` may send (the venue allows 60 / minute on standard accounts). */
|
|
506
|
+
export const LIGHTER_KLINE_MAX_PAGES = 10;
|
|
507
|
+
/** A candle volume field: a finite number / numeric string → its string form; absent or non-numeric → null (never "" / "undefined"). */
|
|
508
|
+
function candleNumber(v) {
|
|
509
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
510
|
+
return String(v);
|
|
511
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)))
|
|
512
|
+
return v;
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
412
515
|
export class LighterAdapter {
|
|
413
|
-
/** GTC→GTT resting-order lifetime (28 days) for the execution record
|
|
516
|
+
/** GTC→GTT resting-order lifetime (28 days) for the execution record. */
|
|
414
517
|
static gttExpiryMs = LIGHTER_GTT_EXPIRY_MS;
|
|
415
518
|
/** Venue id ("lighter" | "lighter-rh"). */
|
|
416
519
|
name;
|
|
@@ -434,6 +537,8 @@ export class LighterAdapter {
|
|
|
434
537
|
_address;
|
|
435
538
|
_marketMap = new Map(); // symbol → marketIndex
|
|
436
539
|
_marketDecimals = new Map(); // symbol → decimals
|
|
540
|
+
/** symbol → live (isLighterMarketLive). Inactive markets stay in _marketMap for reads and closes. */
|
|
541
|
+
_marketLive = new Map();
|
|
437
542
|
_evmKey;
|
|
438
543
|
_apiKey;
|
|
439
544
|
_accountIndexInit;
|
|
@@ -679,7 +784,7 @@ export class LighterAdapter {
|
|
|
679
784
|
return { tier: "master" };
|
|
680
785
|
if (this._evmKey)
|
|
681
786
|
return { tier: "pk" };
|
|
682
|
-
// Lighter RH master key is OWS-only
|
|
787
|
+
// Lighter RH master key is OWS-only: no env-key remediation for it.
|
|
683
788
|
const remediation = this.venueId === "lighter"
|
|
684
789
|
? "Run one of: (a) perp wallet agent approve lighter --master <wallet>; (b) perp wallet generate && perp -e lighter --ows <wallet>; (c) export LIGHTER_PRIVATE_KEY=0x..."
|
|
685
790
|
: `Run one of: (a) perp wallet agent approve ${this.name} --master <wallet>; (b) perp wallet generate && perp -e ${this.name} --ows <wallet>`;
|
|
@@ -834,7 +939,8 @@ export class LighterAdapter {
|
|
|
834
939
|
* the 60 / minute budget.
|
|
835
940
|
*/
|
|
836
941
|
async _refreshMarketMap() {
|
|
837
|
-
|
|
942
|
+
// v2: rows carry `live` (entries cached before it lack the field).
|
|
943
|
+
const perps = await withCache(`mkt:${this.venueId}:${this.networkName}:orderBooks:v2`, LIGHTER_MARKET_LIST_TTL_MS, async () => {
|
|
838
944
|
const res = await this.restGet("/orderBooks", {});
|
|
839
945
|
if (!Array.isArray(res.order_books) || res.order_books.length === 0) {
|
|
840
946
|
throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBooks returned no order_books list — the market list is unknown`, {
|
|
@@ -851,19 +957,33 @@ export class LighterAdapter {
|
|
|
851
957
|
if (typeof d.symbol !== "string" || !Number.isInteger(d.market_id) || !Number.isInteger(d.supported_size_decimals) || !Number.isInteger(d.supported_price_decimals)) {
|
|
852
958
|
throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBooks: perp market row without symbol / market_id / decimals: ${JSON.stringify(d).slice(0, 160)}`, { exchange: this.name });
|
|
853
959
|
}
|
|
854
|
-
rows.push({ symbol: d.symbol.toUpperCase(), marketId: d.market_id, size: d.supported_size_decimals, price: d.supported_price_decimals });
|
|
960
|
+
rows.push({ symbol: d.symbol.toUpperCase(), marketId: d.market_id, size: d.supported_size_decimals, price: d.supported_price_decimals, live: isLighterMarketLive(d) });
|
|
855
961
|
}
|
|
856
962
|
return rows;
|
|
857
963
|
});
|
|
858
964
|
for (const m of perps) {
|
|
859
965
|
this._marketMap.set(m.symbol, m.marketId);
|
|
860
966
|
this._marketDecimals.set(m.symbol, { size: m.size, price: m.price });
|
|
967
|
+
(this._marketLive ??= new Map()).set(m.symbol, m.live);
|
|
861
968
|
}
|
|
862
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* An order that can open or add exposure (not reduce-only) on a market that is not live
|
|
972
|
+
* (`status` inactive / `is_frozen`) is refused before signing. Reduce-only orders and closes are
|
|
973
|
+
* still sent, so a position on a market that went inactive can be closed.
|
|
974
|
+
*/
|
|
975
|
+
assertOpenable(symbol, reduceOnly) {
|
|
976
|
+
if (reduceOnly || this._marketLive?.get(symbol.toUpperCase()) !== false)
|
|
977
|
+
return;
|
|
978
|
+
throw new PerpError("ORDER_REJECTED", `${this.name} market ${symbol.toUpperCase()} is inactive (status not "active" or frozen) — only reduce-only orders and closes are sent; nothing was signed`, {
|
|
979
|
+
exchange: this.name, marketInactive: true,
|
|
980
|
+
remediation: `Close or reduce with --reduce-only; list live markets: perp -e ${this.name} market list`,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
863
983
|
getMarketIndex(symbol) {
|
|
864
984
|
const idx = this._marketMap.get(symbol.toUpperCase());
|
|
865
985
|
if (idx === undefined)
|
|
866
|
-
throw new PerpError("SYMBOL_NOT_FOUND", `Unknown Lighter market: ${symbol}`, { exchange: this.name });
|
|
986
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Unknown Lighter market: ${symbol}`, { exchange: this.name, remediation: `List markets: perp -e ${this.name} market list` });
|
|
867
987
|
return idx;
|
|
868
988
|
}
|
|
869
989
|
/**
|
|
@@ -976,7 +1096,7 @@ export class LighterAdapter {
|
|
|
976
1096
|
return last;
|
|
977
1097
|
}
|
|
978
1098
|
/**
|
|
979
|
-
* Definite IOC outcome
|
|
1099
|
+
* Definite IOC outcome from the executed tx: poll /tx by hash. A taker order the
|
|
980
1100
|
* venue reports "filled" (event_info `to.st` 3, `is` = sent) filled the whole order. Any other
|
|
981
1101
|
* status (the remainder was cancelled — the venue zeroes `rs`, so is − rs is NOT a fill) →
|
|
982
1102
|
* the venue's own `filled_base_amount` from accountOrders by client order index. Zero fill →
|
|
@@ -1117,7 +1237,7 @@ export class LighterAdapter {
|
|
|
1117
1237
|
return null;
|
|
1118
1238
|
}
|
|
1119
1239
|
/**
|
|
1120
|
-
* Final outcome of a placed resting / post-only order (
|
|
1240
|
+
* Final outcome of a placed resting / post-only order (analogue for non-IOC): poll
|
|
1121
1241
|
* /tx (rejected → ORDER_REJECTED). Our order (`to`, matched by client order index) reported
|
|
1122
1242
|
* "filled" in event_info → filled = is (the whole order). Any other status — open, or a
|
|
1123
1243
|
* canceled-* whose remainder the venue zeroed (`rs` 0, so is − rs is NOT a fill) — or an
|
|
@@ -1196,28 +1316,44 @@ export class LighterAdapter {
|
|
|
1196
1316
|
}
|
|
1197
1317
|
return { baseAmount, priceTicks };
|
|
1198
1318
|
}
|
|
1199
|
-
|
|
1319
|
+
/** Settled / predicted hourly funding per market_id (WS market_stats snapshot, shared with the public directory). */
|
|
1320
|
+
fundingStats() {
|
|
1321
|
+
return fetchLighterFundingStats(this.venueId, this.networkName);
|
|
1322
|
+
}
|
|
1323
|
+
/** Why the last listing's fundingRate is null (the settled-rate snapshot failed), else null. */
|
|
1324
|
+
fundingWarning() {
|
|
1325
|
+
return lighterFundingStatsWarning(this.venueId, this.networkName);
|
|
1326
|
+
}
|
|
1327
|
+
async getMarkets(opts) {
|
|
1200
1328
|
if (this._marketsCache && Date.now() - this._marketsCacheTime < LighterAdapter.CACHE_TTL)
|
|
1201
1329
|
return this._marketsCache;
|
|
1330
|
+
const withFunding = opts?.funding !== false;
|
|
1202
1331
|
const markets = [];
|
|
1203
1332
|
// Ensure _marketDecimals is populated (has supported_size_decimals per symbol)
|
|
1204
1333
|
await this.ensureMarketMap();
|
|
1205
1334
|
// Fetch order book details and funding rates in parallel
|
|
1206
1335
|
const [obRes, fundingRates] = await Promise.all([
|
|
1207
1336
|
this.restGet("/orderBookDetails", {}),
|
|
1208
|
-
|
|
1337
|
+
// Settled + predicted hourly rates (WS market_stats snapshot — the public directory and
|
|
1338
|
+
// perp-ws read the same source). Unreadable → fundingRate null (unknown), reason printed.
|
|
1339
|
+
!withFunding ? Promise.resolve(new Map()) : this.fundingStats().catch((e) => {
|
|
1340
|
+
console.error(`[${this.name}] settled funding rate unavailable (fundingRate null): ${e instanceof Error ? e.message : String(e)}`);
|
|
1341
|
+
return new Map();
|
|
1342
|
+
}),
|
|
1209
1343
|
]);
|
|
1210
1344
|
// An absent list is a failed fetch, never "no markets" (SSOT Rule #2).
|
|
1211
1345
|
if (!Array.isArray(obRes.order_book_details)) {
|
|
1212
1346
|
throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBookDetails: missing order_book_details array`, { exchange: this.name });
|
|
1213
1347
|
}
|
|
1214
1348
|
const isPriceStr = (v) => typeof v === "string" && v.length > 0 && Number(v) > 0;
|
|
1215
|
-
|
|
1349
|
+
// Live perps only (status "active", not frozen): the one rule shared with the WS directory and
|
|
1350
|
+
// lighter-public.ts. Inactive markets (26 on mainnet, 2026-10-19) were listed as live.
|
|
1351
|
+
const perps = obRes.order_book_details.filter(d => d.market_type === "perp" && isLighterMarketLive(d));
|
|
1216
1352
|
const unpriced = [];
|
|
1217
1353
|
for (const d of perps) {
|
|
1218
1354
|
const imf = d.min_initial_margin_fraction ?? d.default_initial_margin_fraction ?? 500;
|
|
1219
1355
|
const maxLev = imf > 0 ? Math.floor(10000 / imf) : 50;
|
|
1220
|
-
const fr = fundingRates.get(d.
|
|
1356
|
+
const fr = fundingRates.get(d.market_id);
|
|
1221
1357
|
const dec = this._marketDecimals.get(d.symbol.toUpperCase());
|
|
1222
1358
|
// markPrice = the venue's MARK price: orderBookDetails `mark_price` (the source the public
|
|
1223
1359
|
// data path lighter-public.ts uses; /funding-rates carries no mark price). last_trade_price
|
|
@@ -1230,13 +1366,20 @@ export class LighterAdapter {
|
|
|
1230
1366
|
symbol: d.symbol,
|
|
1231
1367
|
markPrice: d.mark_price,
|
|
1232
1368
|
indexPrice: isPriceStr(d.index_price) ? d.index_price : null,
|
|
1369
|
+
// Last settled hourly rate (market_stats funding_rate, = the newest /fundings print, = the
|
|
1370
|
+
// perp-ws `rate`), a fraction per 1 h; the running hour's predicted rate is additive.
|
|
1233
1371
|
fundingRate: fr?.rate ?? null,
|
|
1372
|
+
fundingRateKind: fr?.rate != null ? "lastSettled" : null,
|
|
1373
|
+
predictedFundingRate: fr?.predictedRate ?? null,
|
|
1234
1374
|
volume24h: d.daily_quote_token_volume != null ? String(d.daily_quote_token_volume) : null,
|
|
1375
|
+
// BASE units, one side (WS market_stats open_interest is the same quantity in USD).
|
|
1235
1376
|
openInterest: d.open_interest != null ? String(d.open_interest) : null,
|
|
1236
1377
|
maxLeverage: maxLev,
|
|
1237
1378
|
sizeDecimals: dec ? dec.size : null,
|
|
1238
|
-
stepSize
|
|
1239
|
-
|
|
1379
|
+
// The lot grid of the size decimals (the public directory's stepSize, lighter-public.ts).
|
|
1380
|
+
stepSize: dec ? (dec.size === 0 ? "1" : `0.${"0".repeat(dec.size - 1)}1`) : null,
|
|
1381
|
+
// `fundingRate` is per 1 h (hourly settlement).
|
|
1382
|
+
fundingHours: 1,
|
|
1240
1383
|
marginTiers: null,
|
|
1241
1384
|
});
|
|
1242
1385
|
}
|
|
@@ -1248,8 +1391,11 @@ export class LighterAdapter {
|
|
|
1248
1391
|
exchange: this.name, remediation: "Retry later; the venue market data is incomplete",
|
|
1249
1392
|
});
|
|
1250
1393
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1394
|
+
// A marks-only listing (no funding read) is not cached as the full one.
|
|
1395
|
+
if (withFunding) {
|
|
1396
|
+
this._marketsCache = markets;
|
|
1397
|
+
this._marketsCacheTime = Date.now();
|
|
1398
|
+
}
|
|
1253
1399
|
return markets;
|
|
1254
1400
|
}
|
|
1255
1401
|
get accountCacheKey() {
|
|
@@ -1283,27 +1429,22 @@ export class LighterAdapter {
|
|
|
1283
1429
|
* Book from GET /orderBookOrders?market_id=<id> on this venue's host (Lighter RH: its own host
|
|
1284
1430
|
* and market ids, e.g. RH testnet TSLA = 112). Rule #2: an unknown symbol (SYMBOL_NOT_FOUND) or
|
|
1285
1431
|
* a failed read throws — it is never reported as an empty book. An empty `bids`/`asks` is the
|
|
1286
|
-
* venue's real answer (no liquidity).
|
|
1432
|
+
* venue's real answer (no liquidity). The endpoint lists individual ORDERS (up to 250 per side,
|
|
1433
|
+
* the documented max); they are aggregated into price levels (lighterBookLevels), as the WS
|
|
1434
|
+
* `order_book` channel publishes them.
|
|
1287
1435
|
*/
|
|
1288
|
-
async getOrderbook(symbol) {
|
|
1436
|
+
async getOrderbook(symbol, depth) {
|
|
1289
1437
|
await this.ensureMarketMap();
|
|
1290
1438
|
const marketId = this.getMarketIndex(symbol);
|
|
1291
1439
|
const res = await this.restGet("/orderBookOrders", {
|
|
1292
1440
|
market_id: String(marketId),
|
|
1293
|
-
limit:
|
|
1441
|
+
limit: String(LIGHTER_BOOK_ORDERS_LIMIT),
|
|
1294
1442
|
});
|
|
1295
|
-
|
|
1296
|
-
const size = l.remaining_base_amount ?? l.size;
|
|
1297
|
-
if (typeof l.price !== "string" || typeof size !== "string") {
|
|
1298
|
-
throw new PerpError("EXCHANGE_ERROR", `${this.name} orderBookOrders ${symbol}: level without price / size`, { exchange: this.name });
|
|
1299
|
-
}
|
|
1300
|
-
return [l.price, size];
|
|
1301
|
-
};
|
|
1302
|
-
return { bids: (res.bids ?? []).map(level), asks: (res.asks ?? []).map(level) };
|
|
1443
|
+
return lighterBook(this.name, res, symbol, depth);
|
|
1303
1444
|
}
|
|
1304
1445
|
async getBalance() {
|
|
1305
1446
|
if (!this._address)
|
|
1306
|
-
throw
|
|
1447
|
+
throw noAccountKeyError(this.name);
|
|
1307
1448
|
const acct = await this.fetchAccount();
|
|
1308
1449
|
// Rule #2: no account is not a zero balance — say so (like getMarketSettings).
|
|
1309
1450
|
if (!acct) {
|
|
@@ -1312,58 +1453,15 @@ export class LighterAdapter {
|
|
|
1312
1453
|
remediation: `A ${this.name} account is created by the first deposit; check the address / account index (perp -e ${this.name} wallet show) or deposit first`,
|
|
1313
1454
|
});
|
|
1314
1455
|
}
|
|
1315
|
-
|
|
1316
|
-
const available = parseFiniteVenueNumber(acct.available_balance, "available_balance", "lighter");
|
|
1317
|
-
const collateral = parseFiniteVenueNumber(acct.collateral, "collateral", "lighter");
|
|
1318
|
-
const unrealizedPnl = acct.positions?.reduce((sum, p) => sum + parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "lighter"), 0) ?? 0;
|
|
1319
|
-
// Include spot collateral balance (USDC on Lighter, USDG on Lighter RH; separate from perp collateral)
|
|
1320
|
-
// An account without that asset holds none of it (0); an unreadable assets list / balance throws.
|
|
1321
|
-
let spotUsdc = 0;
|
|
1322
|
-
const collateralSymbol = this.collateralSymbol;
|
|
1323
|
-
if (!Array.isArray(acct.assets)) {
|
|
1324
|
-
throw new PerpError("EXCHANGE_ERROR", `${this.name} account has no assets list — spot ${collateralSymbol} balance unknown`, { exchange: this.name });
|
|
1325
|
-
}
|
|
1326
|
-
const usdcAsset = acct.assets.find(a => a?.symbol === collateralSymbol);
|
|
1327
|
-
if (usdcAsset !== undefined)
|
|
1328
|
-
spotUsdc = parseFiniteVenueNumber(usdcAsset.balance, `assets.${collateralSymbol}.balance`, "lighter");
|
|
1329
|
-
return {
|
|
1330
|
-
equity: String(totalAsset + spotUsdc),
|
|
1331
|
-
available: String(available),
|
|
1332
|
-
marginUsed: String(Math.max(0, collateral - available)),
|
|
1333
|
-
unrealizedPnl: String(unrealizedPnl),
|
|
1334
|
-
};
|
|
1456
|
+
return lighterBalanceOf(acct, this.name, this.collateralSymbol);
|
|
1335
1457
|
}
|
|
1336
1458
|
async getPositions() {
|
|
1337
1459
|
if (!this._address)
|
|
1338
|
-
throw
|
|
1460
|
+
throw noAccountKeyError(this.name);
|
|
1339
1461
|
const acct = await this.fetchAccount();
|
|
1340
1462
|
if (!acct)
|
|
1341
1463
|
return [];
|
|
1342
|
-
return (acct.positions
|
|
1343
|
-
.filter((p) => parseFiniteVenueNumber(p.position, "position", "lighter") !== 0)
|
|
1344
|
-
.map((p) => {
|
|
1345
|
-
const posSize = parseFiniteVenueNumber(p.position, "position", "lighter");
|
|
1346
|
-
const positionValue = parseFiniteVenueNumber(p.position_value, "position_value", "lighter");
|
|
1347
|
-
return {
|
|
1348
|
-
symbol: String(p.symbol || `Market-${p.market_id}`),
|
|
1349
|
-
side: (Number(p.sign) > 0 ? "long" : "short"),
|
|
1350
|
-
size: String(Math.abs(posSize)),
|
|
1351
|
-
entryPrice: String(p.avg_entry_price || "0"),
|
|
1352
|
-
markPrice: (() => {
|
|
1353
|
-
if (posSize === 0)
|
|
1354
|
-
return "0";
|
|
1355
|
-
const rawMark = positionValue / Math.abs(posSize);
|
|
1356
|
-
const priceDec = this._marketDecimals.get(String(p.symbol || "").toUpperCase())?.price;
|
|
1357
|
-
return priceDec !== undefined ? rawMark.toFixed(priceDec) : formatComputedPrice(rawMark);
|
|
1358
|
-
})(),
|
|
1359
|
-
// The venue reports "0" when the position has no liquidation price (e.g. cross margin
|
|
1360
|
-
// well covered): that is "none", not a price.
|
|
1361
|
-
liquidationPrice: Number(p.liquidation_price) > 0 ? String(p.liquidation_price) : null,
|
|
1362
|
-
unrealizedPnl: String(p.unrealized_pnl || "0"),
|
|
1363
|
-
// The leverage CONFIGURED for the market (what `trade leverage` set) — not notional / equity.
|
|
1364
|
-
...lighterPositionSettings(p, this.name),
|
|
1365
|
-
};
|
|
1366
|
-
});
|
|
1464
|
+
return lighterPositionRows(acct.positions, this.name, (sym) => this._marketDecimals.get(sym.toUpperCase())?.price);
|
|
1367
1465
|
}
|
|
1368
1466
|
/**
|
|
1369
1467
|
* Per-market leverage / margin mode the venue exposes: every `positions[]` entry of the
|
|
@@ -1372,7 +1470,7 @@ export class LighterAdapter {
|
|
|
1372
1470
|
*/
|
|
1373
1471
|
async getMarketSettings() {
|
|
1374
1472
|
if (!this._address)
|
|
1375
|
-
throw
|
|
1473
|
+
throw noAccountKeyError(this.name);
|
|
1376
1474
|
const acct = await this.fetchAccount();
|
|
1377
1475
|
if (acct === null)
|
|
1378
1476
|
throw new PerpError("EXCHANGE_ERROR", `${this.name}: no account found for ${this._address}`, { exchange: this.name });
|
|
@@ -1394,7 +1492,7 @@ export class LighterAdapter {
|
|
|
1394
1492
|
}
|
|
1395
1493
|
async getOpenOrders() {
|
|
1396
1494
|
if (!this._address)
|
|
1397
|
-
throw
|
|
1495
|
+
throw noAccountKeyError(this.name);
|
|
1398
1496
|
if (this._accountIndex < 0 || this._readOnly)
|
|
1399
1497
|
return [];
|
|
1400
1498
|
// ONE request for every perp market. Docs (apidocs.lighter.xyz/reference/accountactiveorders):
|
|
@@ -1451,7 +1549,7 @@ export class LighterAdapter {
|
|
|
1451
1549
|
return isAsk ? "sell" : "buy";
|
|
1452
1550
|
}
|
|
1453
1551
|
/**
|
|
1454
|
-
* Slippage-bounded IOC
|
|
1552
|
+
* Slippage-bounded IOC: protection price = orderbook mid ± opts.slippage.bps,
|
|
1455
1553
|
* tick-aligned toward crossing. The returned raw sendTx result carries `protection`
|
|
1456
1554
|
* and, when the venue reported the fill, `iocOutcome` (see iocOutcomeOf).
|
|
1457
1555
|
* `opts.clientOrderIndex` (optional) = caller's numeric client order id (lighterClientOrderIndex rule).
|
|
@@ -1466,8 +1564,9 @@ export class LighterAdapter {
|
|
|
1466
1564
|
try {
|
|
1467
1565
|
this.ensureSigner();
|
|
1468
1566
|
await this.ensureMarketMap();
|
|
1469
|
-
const nonce = await this.getNextNonce();
|
|
1470
1567
|
const marketIndex = this.getMarketIndex(symbol);
|
|
1568
|
+
this.assertOpenable(symbol, opts.reduceOnly);
|
|
1569
|
+
const nonce = await this.getNextNonce();
|
|
1471
1570
|
({ baseAmount } = this.toTicks(symbol, size, "0"));
|
|
1472
1571
|
const reference = await this.getReferencePrice(symbol);
|
|
1473
1572
|
const p = this.protectionFor(symbol, side, reference, opts.slippage, LIGHTER_REFERENCE_PRICE_SOURCE);
|
|
@@ -1502,9 +1601,9 @@ export class LighterAdapter {
|
|
|
1502
1601
|
return this.awaitIocOutcome(result, symbol, baseAmount, `market ${side} ${size} ${symbol}`, { protection, clientOrderIndex }, match);
|
|
1503
1602
|
}
|
|
1504
1603
|
/**
|
|
1505
|
-
* TIF mapping
|
|
1604
|
+
* TIF mapping: GTC/GTT → GTT (28-day expiry, gttExpiryMs), IOC → IOC,
|
|
1506
1605
|
* ALO / POST_ONLY → time-in-force 2 (POST_ONLY per apidocs trading.md and the official
|
|
1507
|
-
* lighter-go signer; 28-day expiry like GTT — testnet confirmation pending
|
|
1606
|
+
* lighter-go signer; 28-day expiry like GTT — testnet confirmation pending).
|
|
1508
1607
|
* FOK and any other value → CAPABILITY_UNSUPPORTED (never silently GTT).
|
|
1509
1608
|
* `opts.clientOrderIndex` as in marketOrder.
|
|
1510
1609
|
* IOC: the same definite outcome as marketOrder (awaitIocOutcome — zero fill →
|
|
@@ -1530,8 +1629,9 @@ export class LighterAdapter {
|
|
|
1530
1629
|
throw unsupported(this.name, tif === "FOK" ? "fok" : "timeInForce", `time-in-force ${opts?.tif} is not offered`, "Use --tif GTC or IOC");
|
|
1531
1630
|
this.ensureSigner();
|
|
1532
1631
|
await this.ensureMarketMap();
|
|
1533
|
-
const nonce = await this.getNextNonce();
|
|
1534
1632
|
const marketIndex = this.getMarketIndex(symbol);
|
|
1633
|
+
this.assertOpenable(symbol, opts?.reduceOnly === true);
|
|
1634
|
+
const nonce = await this.getNextNonce();
|
|
1535
1635
|
const ticks = this.toTicks(symbol, size, price);
|
|
1536
1636
|
sentBase = ticks.baseAmount;
|
|
1537
1637
|
isIoc = timeInForce === LIGHTER_TIF.IOC;
|
|
@@ -1695,6 +1795,7 @@ export class LighterAdapter {
|
|
|
1695
1795
|
// The order must be an open order of this market, and not a trigger order: a modify is
|
|
1696
1796
|
// signed with triggerPrice 0, which would turn a stop / take-profit into a plain limit.
|
|
1697
1797
|
existing = await this.activeOrderRow(symbol, marketIndex, orderId, "modified");
|
|
1798
|
+
this.assertOpenable(symbol, existing.reduce_only === true || existing.reduce_only === 1);
|
|
1698
1799
|
const type = String(existing.type ?? "").toLowerCase();
|
|
1699
1800
|
if (/stop|take|trigger/.test(type) || Number(existing.trigger_price ?? 0) > 0) {
|
|
1700
1801
|
throw unsupported(this.name, "amend", "trigger orders cannot be modified", "cancel and re-place");
|
|
@@ -1841,7 +1942,7 @@ export class LighterAdapter {
|
|
|
1841
1942
|
/**
|
|
1842
1943
|
* Trigger order. Market trigger (no limitPrice): execution price bound = trigger price ±
|
|
1843
1944
|
* `opts.slippage` bps (the caller's resolveSlippage() output — required; the adapter never
|
|
1844
|
-
* reads settings
|
|
1945
|
+
* reads settings), tick-aligned toward crossing, recorded in the
|
|
1845
1946
|
* result as `protection`. Accepted risk: a fast move through the bound leaves the
|
|
1846
1947
|
* triggered order unfilled.
|
|
1847
1948
|
* TP vs SL: `opts.kind` given (TP/SL legs) → that type as given (a trigger already crossed
|
|
@@ -1864,8 +1965,9 @@ export class LighterAdapter {
|
|
|
1864
1965
|
}
|
|
1865
1966
|
this.ensureSigner();
|
|
1866
1967
|
await this.ensureMarketMap();
|
|
1867
|
-
const nonce = await this.getNextNonce();
|
|
1868
1968
|
const marketIndex = this.getMarketIndex(symbol);
|
|
1969
|
+
this.assertOpenable(symbol, opts?.reduceOnly === true);
|
|
1970
|
+
const nonce = await this.getNextNonce();
|
|
1869
1971
|
const { baseAmount } = this.toTicks(symbol, size, "0");
|
|
1870
1972
|
const { priceTicks: triggerTicks } = this.toTicks(symbol, "0", triggerPrice);
|
|
1871
1973
|
const triggerNum = parseFloat(triggerPrice);
|
|
@@ -2000,6 +2102,12 @@ export class LighterAdapter {
|
|
|
2000
2102
|
return this.updateLeverage(symbol, leverage, marginMode);
|
|
2001
2103
|
}
|
|
2002
2104
|
async getRecentTrades(symbol, limit = 20) {
|
|
2105
|
+
// /recentTrades `limit`: 1..100 (apidocs.lighter.xyz/reference/recenttrades; 101 → code 20001).
|
|
2106
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > LIGHTER_RECENT_TRADES_MAX) {
|
|
2107
|
+
throw new PerpError("INVALID_PARAMS", `${this.name} trades limit must be 1..${LIGHTER_RECENT_TRADES_MAX} (got ${limit})`, {
|
|
2108
|
+
exchange: this.name, remediation: `${this.name} serves at most the last ${LIGHTER_RECENT_TRADES_MAX} trades`,
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2003
2111
|
await this.ensureMarketMap();
|
|
2004
2112
|
const marketId = this.getMarketIndex(symbol);
|
|
2005
2113
|
const res = await this.restGet("/recentTrades", { market_id: String(marketId), limit: String(limit) });
|
|
@@ -2013,30 +2121,128 @@ export class LighterAdapter {
|
|
|
2013
2121
|
side: t.is_maker_ask ? "buy" : "sell",
|
|
2014
2122
|
price: String(t.price ?? "0"),
|
|
2015
2123
|
size: String(t.size ?? t.base_amount ?? t.amount ?? ""),
|
|
2016
|
-
fee
|
|
2124
|
+
// The tape's taker_fee / maker_fee are the parties' fee RATES, not a fee the viewer paid.
|
|
2125
|
+
fee: null,
|
|
2017
2126
|
}));
|
|
2018
2127
|
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Settled funding prints: GET /api/v1/fundings (apidocs.lighter.xyz/reference/fundings;
|
|
2130
|
+
* `count_back` rows from `start_timestamp`, max 750). Each row is one hourly settlement:
|
|
2131
|
+
* `rate` is PERCENT per 1 h, unsigned, with `direction` "long" (longs pay → positive) or
|
|
2132
|
+
* "short" (shorts pay → negative). Live 2026-10-19 BTC: rate "0.0012", value "0.99315" =
|
|
2133
|
+
* mark 82,763 × 0.000012, i.e. value / mark = rate / 100. NOT the /funding-rates unit (8 h).
|
|
2134
|
+
*/
|
|
2019
2135
|
async getFundingHistory(symbol, limit = 10) {
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2136
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 750) {
|
|
2137
|
+
throw new PerpError("INVALID_PARAMS", `${this.name} funding history limit must be 1..750 (got ${limit})`, { exchange: this.name });
|
|
2138
|
+
}
|
|
2139
|
+
await this.ensureMarketMap();
|
|
2140
|
+
const marketId = this.getMarketIndex(symbol); // unlisted market: SYMBOL_NOT_FOUND, not an empty history
|
|
2141
|
+
const end = Math.floor(Date.now() / 1000);
|
|
2142
|
+
const res = await this.restGet("/fundings", {
|
|
2143
|
+
market_id: String(marketId),
|
|
2144
|
+
resolution: "1h",
|
|
2145
|
+
// One spare hour covers the settlement in progress.
|
|
2146
|
+
start_timestamp: String(end - (limit + 1) * 3600),
|
|
2147
|
+
end_timestamp: String(end),
|
|
2148
|
+
count_back: String(limit),
|
|
2149
|
+
});
|
|
2150
|
+
if (!Array.isArray(res.fundings)) {
|
|
2151
|
+
throw new PerpError("EXCHANGE_ERROR", `${this.name} /fundings: missing fundings array (${JSON.stringify(res).slice(0, 120)})`, { exchange: this.name });
|
|
2152
|
+
}
|
|
2153
|
+
return res.fundings.map((f) => lighterFundingRow(this.name, f)).sort((a, b) => a.time - b.time).slice(-limit);
|
|
2026
2154
|
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Candles opening in [startTime, endTime] (ms). GET /api/v1/candles answers the
|
|
2157
|
+
* max(count_back, bars in the window) newest bars ending at `end_timestamp`, at most 500
|
|
2158
|
+
* (live 2026-10-19, BTC 1h: a 96-bar window with count_back 1 / 96 → those 96 bars; with
|
|
2159
|
+
* count_back 500 → 500 bars, 404 of them before start_timestamp; a future window → past bars).
|
|
2160
|
+
* So count_back is the window's bar count (≤ 500), rows outside the window are dropped, and a
|
|
2161
|
+
* longer window is paged backwards in 500-bar chunks by moving end_timestamp. `end_timestamp` is
|
|
2162
|
+
* exclusive at second resolution (live: end = a bar's open excludes it, open + 1 s includes it),
|
|
2163
|
+
* so end + 1 s is sent for an inclusive --end. Each answer's `r`
|
|
2164
|
+
* must be the requested resolution. Intervals outside LIGHTER_CANDLE_INTERVALS and windows over
|
|
2165
|
+
* LIGHTER_KLINE_MAX_PAGES pages are refused with INVALID_PARAMS before any request.
|
|
2166
|
+
*/
|
|
2027
2167
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2168
|
+
const ivMs = LIGHTER_CANDLE_INTERVALS[interval];
|
|
2169
|
+
if (ivMs === undefined) {
|
|
2170
|
+
throw new PerpError("INVALID_PARAMS", `${this.name} kline interval ${JSON.stringify(interval)} is not served (supported: ${Object.keys(LIGHTER_CANDLE_INTERVALS).join(", ")})`, {
|
|
2171
|
+
exchange: this.name, remediation: `Use one of: ${Object.keys(LIGHTER_CANDLE_INTERVALS).join(", ")}`,
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
|
|
2175
|
+
throw new PerpError("INVALID_PARAMS", `${this.name} kline window must be start ≤ end (unix ms; got ${startTime}..${endTime})`, { exchange: this.name });
|
|
2176
|
+
}
|
|
2177
|
+
// Bar opens in [start, end] (bars open on multiples of the interval since the epoch).
|
|
2178
|
+
const barsIn = (from, to) => Math.floor(to / ivMs) - Math.ceil(from / ivMs) + 1;
|
|
2179
|
+
const bars = barsIn(startTime, endTime);
|
|
2180
|
+
if (bars > LIGHTER_CANDLES_PER_CALL * LIGHTER_KLINE_MAX_PAGES) {
|
|
2181
|
+
throw new PerpError("INVALID_PARAMS", `${this.name} kline window spans ${bars} ${interval} bars; at most ${LIGHTER_CANDLES_PER_CALL * LIGHTER_KLINE_MAX_PAGES} are read (${LIGHTER_KLINE_MAX_PAGES} requests of ${LIGHTER_CANDLES_PER_CALL})`, {
|
|
2182
|
+
exchange: this.name, remediation: "Narrow --start / --end or use a longer interval",
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
if (bars <= 0) {
|
|
2186
|
+
// No bar opens in the window: nothing to ask for (count_back 0 is never sent); the symbol is still checked.
|
|
2187
|
+
await this.ensureMarketMap();
|
|
2188
|
+
this.getMarketIndex(symbol);
|
|
2189
|
+
return [];
|
|
2190
|
+
}
|
|
2191
|
+
const byTime = new Map();
|
|
2192
|
+
let cursorEnd = endTime;
|
|
2193
|
+
let prevEarliest = Infinity;
|
|
2194
|
+
let covered = false;
|
|
2195
|
+
for (let page = 0; page < LIGHTER_KLINE_MAX_PAGES; page++) {
|
|
2196
|
+
const left = barsIn(startTime, cursorEnd);
|
|
2197
|
+
if (left <= 0) {
|
|
2198
|
+
covered = true;
|
|
2199
|
+
break;
|
|
2200
|
+
} // every bar open in the window is read
|
|
2201
|
+
const want = Math.min(LIGHTER_CANDLES_PER_CALL, left);
|
|
2202
|
+
const res = await this.getCandles(symbol, interval, startTime, cursorEnd + 1000, want);
|
|
2203
|
+
assertCandleInterval(this.name, interval, res.r);
|
|
2204
|
+
// /candles returns the array under key `c` (top keys: code, r, c).
|
|
2205
|
+
if (!Array.isArray(res.c)) {
|
|
2206
|
+
throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: no candle list (${JSON.stringify(res).slice(0, 120)})`, { exchange: this.name });
|
|
2207
|
+
}
|
|
2208
|
+
const rows = res.c;
|
|
2209
|
+
let earliest = Infinity;
|
|
2210
|
+
for (const c of rows) {
|
|
2211
|
+
const t = Number(c.t);
|
|
2212
|
+
if (!Number.isFinite(t))
|
|
2213
|
+
throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: row without an open time: ${JSON.stringify(c).slice(0, 120)}`, { exchange: this.name });
|
|
2214
|
+
if (t < earliest)
|
|
2215
|
+
earliest = t;
|
|
2216
|
+
if (t >= startTime && t <= endTime)
|
|
2217
|
+
byTime.set(t, c);
|
|
2218
|
+
}
|
|
2219
|
+
// Done: the window start is covered, or the venue has no older bars (a short answer).
|
|
2220
|
+
if (rows.length === 0 || earliest <= startTime || rows.length < want) {
|
|
2221
|
+
covered = true;
|
|
2222
|
+
break;
|
|
2223
|
+
}
|
|
2224
|
+
if (earliest >= prevEarliest) {
|
|
2225
|
+
throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: paging made no progress (oldest bar ${earliest} not before the previous page's ${prevEarliest})`, { exchange: this.name });
|
|
2226
|
+
}
|
|
2227
|
+
prevEarliest = earliest;
|
|
2228
|
+
cursorEnd = earliest - 1000; // sent as `earliest`: the bars strictly before the oldest one read
|
|
2229
|
+
}
|
|
2230
|
+
if (!covered) {
|
|
2231
|
+
// Every page was full and the window start is still not reached: never a silently short window.
|
|
2232
|
+
throw new PerpError("EXCHANGE_ERROR", `${this.name} /candles: ${LIGHTER_KLINE_MAX_PAGES} full pages did not reach the window start ${startTime} (oldest ${prevEarliest})`, { exchange: this.name });
|
|
2233
|
+
}
|
|
2234
|
+
const candles = [...byTime.values()].sort((a, b) => Number(a.t) - Number(b.t));
|
|
2032
2235
|
return candles.map((c) => ({
|
|
2033
|
-
time: Number(c.
|
|
2236
|
+
time: Number(c.t),
|
|
2034
2237
|
open: String(c.open ?? c.o ?? "0"),
|
|
2035
2238
|
high: String(c.high ?? c.h ?? "0"),
|
|
2036
2239
|
low: String(c.low ?? c.l ?? "0"),
|
|
2037
2240
|
close: String(c.close ?? c.c ?? "0"),
|
|
2038
|
-
volume:
|
|
2039
|
-
|
|
2241
|
+
volume: candleNumber(c.base_token_volume ?? c.v),
|
|
2242
|
+
// /candles: v = base volume, V = quote (USD) volume; no per-candle trade count (live
|
|
2243
|
+
// 2026-10-14 keys: t o h l c v V i — `i` is the last trade id, not a count).
|
|
2244
|
+
quoteVolume: candleNumber(c.V),
|
|
2245
|
+
trades: typeof c.trades_count === "number" ? c.trades_count : typeof c.n === "number" ? c.n : null,
|
|
2040
2246
|
}));
|
|
2041
2247
|
}
|
|
2042
2248
|
/**
|
|
@@ -2095,7 +2301,7 @@ export class LighterAdapter {
|
|
|
2095
2301
|
*/
|
|
2096
2302
|
async getOrderHistory(limit = 30) {
|
|
2097
2303
|
if (!this._address)
|
|
2098
|
-
throw
|
|
2304
|
+
throw noAccountKeyError(this.name);
|
|
2099
2305
|
const orders = await this.historyRows("/accountInactiveOrders", "orders", { market_type: "perp" }, limit, true);
|
|
2100
2306
|
// Every field is required: a missing price / size / status / type is an unreadable row, never "0" / "done" / "limit".
|
|
2101
2307
|
const req = (o, k) => lighterRowField(o, k, this.name, "order history");
|
|
@@ -2125,7 +2331,7 @@ export class LighterAdapter {
|
|
|
2125
2331
|
*/
|
|
2126
2332
|
async getTradeHistory(limit = 30) {
|
|
2127
2333
|
if (!this._address)
|
|
2128
|
-
throw
|
|
2334
|
+
throw noAccountKeyError(this.name);
|
|
2129
2335
|
const trades = await this.historyRows("/trades", "trades", { sort_by: "timestamp", market_type: "perp" }, limit, false);
|
|
2130
2336
|
return trades.map((t) => {
|
|
2131
2337
|
const sold = t.ask_account_id === this._accountIndex;
|
|
@@ -2160,7 +2366,7 @@ export class LighterAdapter {
|
|
|
2160
2366
|
*/
|
|
2161
2367
|
async getFundingPayments(limit = 200) {
|
|
2162
2368
|
if (!this._address)
|
|
2163
|
-
throw
|
|
2369
|
+
throw noAccountKeyError(this.name);
|
|
2164
2370
|
const items = await this.historyRows("/positionFunding", "position_fundings", { side: "all" }, limit, true);
|
|
2165
2371
|
return items.map((f) => {
|
|
2166
2372
|
if (typeof f.timestamp !== "number" || typeof f.change !== "string") {
|
|
@@ -2170,8 +2376,9 @@ export class LighterAdapter {
|
|
|
2170
2376
|
});
|
|
2171
2377
|
}
|
|
2172
2378
|
/**
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2379
|
+
* GET /api/v1/funding-rates: the venue's PREDICTED rate × 8, a fraction per 8 h — not the venue
|
|
2380
|
+
* basis (1 h; market rows carry the settled hourly rate, fundingStats). Not used for funding
|
|
2381
|
+
* comparisons; normalize with 8 h if read.
|
|
2175
2382
|
*/
|
|
2176
2383
|
async getFundingRates() {
|
|
2177
2384
|
const map = new Map();
|
|
@@ -2428,7 +2635,7 @@ export class LighterAdapter {
|
|
|
2428
2635
|
}
|
|
2429
2636
|
}
|
|
2430
2637
|
/**
|
|
2431
|
-
* One WASM signer (one Go runtime) per `${venueId}:${network}
|
|
2638
|
+
* One WASM signer (one Go runtime) per `${venueId}:${network}`.
|
|
2432
2639
|
* lighter-go keys its clients by (accountIndex, apiKeyIndex) only and keeps a
|
|
2433
2640
|
* package-global chainId for L1 messages, so two venues must never share a
|
|
2434
2641
|
* runtime. Keyed by venue+network rather than chainId because Lighter testnet
|