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
package/dist/bot/apex/radar.js
CHANGED
|
@@ -20,7 +20,8 @@ function scoreStructure(snapshot) {
|
|
|
20
20
|
else if (vol > 1_000_000)
|
|
21
21
|
score += 15;
|
|
22
22
|
// OI component (0-50): higher open interest = more conviction
|
|
23
|
-
|
|
23
|
+
// null (unknown) → no OI score, the same as below every threshold — never a parsed fake 0.
|
|
24
|
+
const oi = snapshot.openInterest === null ? NaN : parseFloat(snapshot.openInterest);
|
|
24
25
|
if (oi > 50_000_000)
|
|
25
26
|
score += 50;
|
|
26
27
|
else if (oi > 20_000_000)
|
package/dist/bot/engine.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
2
2
|
import { filledOf } from "./fill-size.js";
|
|
3
3
|
import type { BotConfig } from "./config.js";
|
|
4
|
-
import type { StrategyAction } from "./strategy-types.js";
|
|
4
|
+
import type { StrategyAction, EnrichedSnapshot } from "./strategy-types.js";
|
|
5
5
|
import { type QuoteRules } from "./quote-rules.js";
|
|
6
6
|
import "./strategies/grid-strategy.js";
|
|
7
7
|
import "./strategies/dca-strategy.js";
|
|
@@ -87,6 +87,15 @@ export declare function executeActions(adapter: ExchangeAdapter, symbol: string,
|
|
|
87
87
|
filled: number;
|
|
88
88
|
}>;
|
|
89
89
|
export { filledOf };
|
|
90
|
+
/**
|
|
91
|
+
* The tick's market snapshot. `unknown` names a failed orderbook / klines read (null = both
|
|
92
|
+
* read): the empty arrays in `snapshot` then are NOT data — the engine skips the tick of a
|
|
93
|
+
* strategy that reads them (MARKET_DATA_STRATEGIES).
|
|
94
|
+
*/
|
|
95
|
+
export declare function getEnrichedSnapshot(adapter: ExchangeAdapter, symbol: string, log: BotLog): Promise<{
|
|
96
|
+
snapshot: EnrichedSnapshot;
|
|
97
|
+
unknown: string | null;
|
|
98
|
+
}>;
|
|
90
99
|
/** TUI positions of one venue; a failed read → `error` set (shown "unavailable"), never an empty list. */
|
|
91
100
|
export declare function fetchPositions(adapter: ExchangeAdapter, symbol: string, exchangeName?: string): Promise<{
|
|
92
101
|
positions: import("./tui/index.js").Position[];
|
package/dist/bot/engine.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getVenue, requireRole, tryResolveVenueId } from "../exchanges/venue.js";
|
|
1
2
|
import { assertCanClosePositions } from "../exchanges/trading-v2.js";
|
|
2
3
|
import { resolveAdapterSlippage } from "../exchanges/slippage.js";
|
|
3
4
|
import { filledOf } from "./fill-size.js";
|
|
@@ -100,6 +101,12 @@ export async function runBot(adapter, config, jobId, log, extraAdapters, // for
|
|
|
100
101
|
mode,
|
|
101
102
|
/** Global --dry-run: strategy actions are logged, never sent; non-simulatable strategies are refused. */
|
|
102
103
|
isDryRun) {
|
|
104
|
+
// A venue without the trading role (TxFlow, release-gated RISEx) runs no bot — dry-run included.
|
|
105
|
+
for (const a of [adapter, ...(extraAdapters?.values() ?? [])]) {
|
|
106
|
+
const venueId = tryResolveVenueId(a.name);
|
|
107
|
+
if (venueId !== null)
|
|
108
|
+
requireRole(getVenue(venueId), "trading", "automation");
|
|
109
|
+
}
|
|
103
110
|
if (isDryRun) {
|
|
104
111
|
assertStrategySimulatable(config.strategy.type);
|
|
105
112
|
adapter = dryRunAdapter(adapter);
|
|
@@ -192,7 +199,7 @@ isDryRun) {
|
|
|
192
199
|
if (mode === "tui") {
|
|
193
200
|
try {
|
|
194
201
|
const { startDashboard } = await import("./tui/index.js");
|
|
195
|
-
const initialTuiState = buildTuiState(state, config, { price: 0, volatility24h: 0, fundingRate: 0, volume24h: 0 }, [], [], strategyCtx);
|
|
202
|
+
const initialTuiState = buildTuiState(state, config, { price: 0, volatility24h: 0, fundingRate: 0, volume24h: 0, openInterest: null }, [], [], strategyCtx);
|
|
196
203
|
const result = startDashboard({
|
|
197
204
|
initialState: initialTuiState,
|
|
198
205
|
onQuit: () => { running = false; state.phase = "stopped"; },
|
|
@@ -883,14 +890,14 @@ export { filledOf };
|
|
|
883
890
|
* read): the empty arrays in `snapshot` then are NOT data — the engine skips the tick of a
|
|
884
891
|
* strategy that reads them (MARKET_DATA_STRATEGIES).
|
|
885
892
|
*/
|
|
886
|
-
async function getEnrichedSnapshot(adapter, symbol, log) {
|
|
893
|
+
export async function getEnrichedSnapshot(adapter, symbol, log) {
|
|
887
894
|
// Multi-symbol strategies use "ALL" — skip market-specific data
|
|
888
895
|
if (symbol.toUpperCase() === "ALL") {
|
|
889
896
|
return {
|
|
890
897
|
snapshot: {
|
|
891
898
|
price: 0, high24h: 0, low24h: 0, volume24h: 0,
|
|
892
899
|
fundingRate: 0, volatility24h: 0, rsi: NaN, spreadPct: 0,
|
|
893
|
-
klines: [], orderbook: { bids: [], asks: [] }, openInterest:
|
|
900
|
+
klines: [], orderbook: { bids: [], asks: [] }, openInterest: null,
|
|
894
901
|
},
|
|
895
902
|
unknown: null,
|
|
896
903
|
};
|
|
@@ -903,7 +910,8 @@ async function getEnrichedSnapshot(adapter, symbol, log) {
|
|
|
903
910
|
adapter.getKlines(symbol, "1h", Date.now() - 24 * 60 * 60 * 1000, Date.now()).catch(why("klines")),
|
|
904
911
|
]);
|
|
905
912
|
return {
|
|
906
|
-
|
|
913
|
+
// Open interest is not read for the bot snapshot: null (unknown), never a "0" stand-in.
|
|
914
|
+
snapshot: { ...base, klines: klines ?? [], orderbook: orderbook ?? { bids: [], asks: [] }, openInterest: null },
|
|
907
915
|
unknown: failed.length > 0 ? failed.join("; ") : null,
|
|
908
916
|
};
|
|
909
917
|
}
|
|
@@ -982,7 +990,7 @@ export async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
|
|
|
982
990
|
}
|
|
983
991
|
const _spotPriceWarned = new Set();
|
|
984
992
|
async function fetchSpotPositions(adapter) {
|
|
985
|
-
// Bot scope
|
|
993
|
+
// Bot scope: only separate/runtime spot venues with transfer.
|
|
986
994
|
const { getBotSpotAdapter, isSpotQuoteAsset, spotMarketsForValuation, spotNetworkOf, spotTokenBase } = await import("../exchanges/spot-factory.js");
|
|
987
995
|
const { getVenue } = await import("../exchanges/venue.js");
|
|
988
996
|
let raw;
|
|
@@ -1100,7 +1108,7 @@ function buildTuiState(state, config, snapshot, positions, openOrders, strategyC
|
|
|
1100
1108
|
exchange: config.exchange,
|
|
1101
1109
|
price: snapshot.price,
|
|
1102
1110
|
volume24h: snapshot.volume24h ?? 0,
|
|
1103
|
-
openInterest: snapshot.openInterest
|
|
1111
|
+
openInterest: snapshot.openInterest,
|
|
1104
1112
|
fundingRate: snapshot.fundingRate,
|
|
1105
1113
|
volatility24h: snapshot.volatility24h,
|
|
1106
1114
|
positions,
|
package/dist/bot/indicators.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export declare function calculateEMA(prices: number[], period: number): number[]
|
|
|
7
7
|
/** Calculate Simple Moving Average */
|
|
8
8
|
export declare function calculateSMA(prices: number[], period: number): number[];
|
|
9
9
|
/** Calculate Volume Weighted Average Price */
|
|
10
|
+
/**
|
|
11
|
+
* Candle volumes as numbers, or null when ANY candle's volume is unknown — null (the venue does
|
|
12
|
+
* not publish it) or not a finite number ("", "undefined", NaN). Never a 0 stand-in.
|
|
13
|
+
*/
|
|
14
|
+
export declare function klineVolumes(klines: ReadonlyArray<{
|
|
15
|
+
volume: string | null;
|
|
16
|
+
}>): number[] | null;
|
|
10
17
|
export declare function calculateVWAP(candles: {
|
|
11
18
|
close: number;
|
|
12
19
|
volume: number;
|
package/dist/bot/indicators.js
CHANGED
|
@@ -33,6 +33,22 @@ export function calculateSMA(prices, period) {
|
|
|
33
33
|
return sma;
|
|
34
34
|
}
|
|
35
35
|
/** Calculate Volume Weighted Average Price */
|
|
36
|
+
/**
|
|
37
|
+
* Candle volumes as numbers, or null when ANY candle's volume is unknown — null (the venue does
|
|
38
|
+
* not publish it) or not a finite number ("", "undefined", NaN). Never a 0 stand-in.
|
|
39
|
+
*/
|
|
40
|
+
export function klineVolumes(klines) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const k of klines) {
|
|
43
|
+
if (k.volume === null || k.volume.trim() === "")
|
|
44
|
+
return null;
|
|
45
|
+
const v = Number(k.volume);
|
|
46
|
+
if (!Number.isFinite(v))
|
|
47
|
+
return null;
|
|
48
|
+
out.push(v);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
36
52
|
export function calculateVWAP(candles) {
|
|
37
53
|
let cumVolPrice = 0;
|
|
38
54
|
let cumVol = 0;
|
package/dist/bot/perp-order.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Verified perp market order for bot / arb legs
|
|
2
|
+
* Verified perp market order for bot / arb legs.
|
|
3
3
|
*
|
|
4
4
|
* A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
|
|
5
5
|
* when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
|
|
23
23
|
* the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
|
|
24
24
|
*
|
|
25
|
-
* Venue-final IOC outcome
|
|
25
|
+
* Venue-final IOC outcome: when the adapter is IocOutcomeCapable and
|
|
26
26
|
* iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
|
|
27
27
|
* (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
|
|
28
28
|
* step, taken from the precision of the order size / reported fill):
|
|
@@ -61,7 +61,7 @@ export declare function rejectedLotSize(e: unknown): number | null;
|
|
|
61
61
|
/** true when `e` says the order outcome is unknown (quarantine, never retry / roll back blindly). */
|
|
62
62
|
export declare function isOutcomeUnknown(e: unknown): boolean;
|
|
63
63
|
/**
|
|
64
|
-
* Second-leg residual correction
|
|
64
|
+
* Second-leg residual correction: the second leg is sized to the first
|
|
65
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
66
|
* this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
|
|
67
67
|
* verified, never the venue's whole position. `firstSide` is the side the first leg
|
|
@@ -79,7 +79,7 @@ export interface PerpCloseResult {
|
|
|
79
79
|
note: string | null;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Verified reduce-only close of `size` with residual handling
|
|
82
|
+
* Verified reduce-only close of `size` with residual handling. placePerpVerified
|
|
83
83
|
* accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
|
|
84
84
|
* not leave the rest silently open, so:
|
|
85
85
|
* 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
|
package/dist/bot/perp-order.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Verified perp market order for bot / arb legs
|
|
2
|
+
* Verified perp market order for bot / arb legs.
|
|
3
3
|
*
|
|
4
4
|
* A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
|
|
5
5
|
* when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
|
|
23
23
|
* the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
|
|
24
24
|
*
|
|
25
|
-
* Venue-final IOC outcome
|
|
25
|
+
* Venue-final IOC outcome: when the adapter is IocOutcomeCapable and
|
|
26
26
|
* iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
|
|
27
27
|
* (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
|
|
28
28
|
* step, taken from the precision of the order size / reported fill):
|
|
@@ -158,7 +158,7 @@ export async function placePerpVerified(adapter, venue, symbol, side, size, opts
|
|
|
158
158
|
}
|
|
159
159
|
throw orderOutcomeUnknown(venue, what, e, `Do NOT resubmit — the order may still execute. Check the ${venue} ${symbol} position`);
|
|
160
160
|
}
|
|
161
|
-
// Venue-final IOC outcome
|
|
161
|
+
// Venue-final IOC outcome: the venue reported what this order filled; nothing more can fill.
|
|
162
162
|
const outcome = isIocOutcomeCapable(adapter) ? adapter.iocOutcomeOf(raw) : null;
|
|
163
163
|
if (outcome !== null) {
|
|
164
164
|
const reported = Number(outcome.filledSize);
|
|
@@ -226,7 +226,7 @@ export function isOutcomeUnknown(e) {
|
|
|
226
226
|
return e instanceof PerpError && e.structured.code === "ORDER_PENDING";
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
|
-
* Second-leg residual correction
|
|
229
|
+
* Second-leg residual correction: the second leg is sized to the first
|
|
230
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
231
|
* this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
|
|
232
232
|
* verified, never the venue's whole position. `firstSide` is the side the first leg
|
|
@@ -245,7 +245,7 @@ export async function trimFirstLegToHedge(adapter, venue, symbol, firstSide, fir
|
|
|
245
245
|
return excess;
|
|
246
246
|
}
|
|
247
247
|
/**
|
|
248
|
-
* Verified reduce-only close of `size` with residual handling
|
|
248
|
+
* Verified reduce-only close of `size` with residual handling. placePerpVerified
|
|
249
249
|
* accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
|
|
250
250
|
* not leave the rest silently open, so:
|
|
251
251
|
* 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
|
package/dist/bot/quarantine.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Position quarantine
|
|
2
|
+
* Position quarantine — GLOBAL per symbol.
|
|
3
3
|
*
|
|
4
4
|
* A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
|
|
5
5
|
* confirmed, a close whose post-trade verification failed, or a position left unhedged
|
package/dist/bot/quarantine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Position quarantine
|
|
2
|
+
* Position quarantine — GLOBAL per symbol.
|
|
3
3
|
*
|
|
4
4
|
* A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
|
|
5
5
|
* confirmed, a close whose post-trade verification failed, or a position left unhedged
|
package/dist/bot/quote-rules.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function quoteRulesOf(adapter, symbol) {
|
|
|
19
19
|
if (network === null) {
|
|
20
20
|
throw new PerpError("EXCHANGE_ERROR", `${adapter.name}: the adapter's network is unknown, so the ${symbol} tick / minimums cannot be read; nothing was sent`, { exchange: adapter.name });
|
|
21
21
|
}
|
|
22
|
-
const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network), lotStepOf(adapter, symbol)]);
|
|
22
|
+
const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network, { funding: false }), lotStepOf(adapter, symbol)]);
|
|
23
23
|
const vm = markets.find(m => symbolMatch(m.symbol, symbol));
|
|
24
24
|
return { tickSize: vm?.tickSize ?? null, stepSize, minSize: vm?.minSize ?? null, minNotional: vm?.minNotional ?? null };
|
|
25
25
|
}
|
|
@@ -85,11 +85,12 @@ export class ApexStrategy {
|
|
|
85
85
|
this.slots.splice(idx, 1);
|
|
86
86
|
}
|
|
87
87
|
// ── Phase 2: Record pulse history ──
|
|
88
|
-
|
|
88
|
+
// Unknown OI (null) is not recorded: a fake 0 in the history would read as an OI jump later.
|
|
89
89
|
const currentVolume = snapshot.volume24h;
|
|
90
90
|
const historyKey = ctx.symbol;
|
|
91
91
|
const existing = this.pulseHistory.get(historyKey) ?? [];
|
|
92
|
-
|
|
92
|
+
if (snapshot.openInterest !== null)
|
|
93
|
+
existing.push({ oi: parseFloat(snapshot.openInterest), volume: currentVolume });
|
|
93
94
|
// Keep last 10 entries
|
|
94
95
|
if (existing.length > 10)
|
|
95
96
|
existing.shift();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
|
-
import { calculateEMA, calculateVWAP, calculateATR } from "../indicators.js";
|
|
2
|
+
import { calculateEMA, calculateVWAP, calculateATR, klineVolumes } from "../indicators.js";
|
|
3
3
|
export class EngineMmStrategy {
|
|
4
4
|
name = "engine-mm";
|
|
5
5
|
describe() {
|
|
@@ -92,11 +92,12 @@ export class EngineMmStrategy {
|
|
|
92
92
|
const components = [];
|
|
93
93
|
// Mid-price (always available)
|
|
94
94
|
components.push(snapshot.price);
|
|
95
|
-
// VWAP from klines
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// VWAP from klines — only when every candle volume is known (unknown is never weighted as 0)
|
|
96
|
+
const volumes = klineVolumes(snapshot.klines);
|
|
97
|
+
if (snapshot.klines.length > 0 && volumes !== null) {
|
|
98
|
+
const candles = snapshot.klines.map((k, i) => ({
|
|
98
99
|
close: parseFloat(k.close),
|
|
99
|
-
volume:
|
|
100
|
+
volume: volumes[i],
|
|
100
101
|
}));
|
|
101
102
|
const vwap = calculateVWAP(candles);
|
|
102
103
|
if (vwap > 0)
|
|
@@ -549,7 +549,7 @@ export class FundingArbStrategy {
|
|
|
549
549
|
const perpRates = ratesByExchange.get(name);
|
|
550
550
|
if (!perpRates)
|
|
551
551
|
continue;
|
|
552
|
-
// Same-venue spot↔perp: canonical symbol + price-deviation guard
|
|
552
|
+
// Same-venue spot↔perp: canonical symbol + price-deviation guard.
|
|
553
553
|
const spotMarkets = await spotAdapter.getSpotMarkets();
|
|
554
554
|
const matched = await botSpotPerpMatches(name, spotMarkets, [name], perpMarketsCache, (msg) => ctx.log(` [ARB] ${msg}`));
|
|
555
555
|
const spotSymbols = matched.get(name);
|
|
@@ -1064,9 +1064,16 @@ export class FundingArbStrategy {
|
|
|
1064
1064
|
return false;
|
|
1065
1065
|
}
|
|
1066
1066
|
const perpSymbol = getPerpSymbol(opp.symbol, opp.shortExchange);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1067
|
+
let priceEstimate;
|
|
1068
|
+
try {
|
|
1069
|
+
priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
|
|
1070
|
+
}
|
|
1071
|
+
catch (err) {
|
|
1072
|
+
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: market read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
if (priceEstimate === null) {
|
|
1076
|
+
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: no positive mark price on ${opp.shortExchange}`);
|
|
1070
1077
|
return false;
|
|
1071
1078
|
}
|
|
1072
1079
|
// Liquidity check
|
|
@@ -1099,7 +1106,7 @@ export class FundingArbStrategy {
|
|
|
1099
1106
|
const transferAmt = Math.ceil(matched.notional * 1.02); // slight buffer for price movement
|
|
1100
1107
|
if (await transferUsdcToSpot(spotAdapter, transferAmt))
|
|
1101
1108
|
ctx.log(` [ARB] Transferred $${transferAmt} to spot`);
|
|
1102
|
-
// Step 2: Buy spot — the hedge is sized to what actually filled
|
|
1109
|
+
// Step 2: Buy spot — the hedge is sized to what actually filled.
|
|
1103
1110
|
let hedgeSize;
|
|
1104
1111
|
let spotFill;
|
|
1105
1112
|
try {
|
|
@@ -8,7 +8,7 @@ import type { SpotAdapter, SpotMarketInfo } from "../../exchanges/spot-interface
|
|
|
8
8
|
import type { VenueMarket } from "../../exchanges/venue-types.js";
|
|
9
9
|
import "../../exchanges/venues/index.js";
|
|
10
10
|
/**
|
|
11
|
-
* Funding strategies refuse a venue whose funding rate is unverified as a leg (
|
|
11
|
+
* Funding strategies refuse a venue whose funding rate is unverified as a leg (
|
|
12
12
|
* #6): every adapter of the run (primary + extraAdapters) is checked when the strategy starts.
|
|
13
13
|
* Adapter names that are not registered venues (test doubles) are not venues and are skipped.
|
|
14
14
|
*/
|
|
@@ -58,8 +58,12 @@ export declare function cappedLeverage(configured: number, legs: {
|
|
|
58
58
|
}[], logUnknown: (msg: string) => void): number;
|
|
59
59
|
/** Match a symbol against a target, accounting for -PERP suffix variations. */
|
|
60
60
|
export declare function matchSymbol(s: string, target: string): boolean;
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The perp venue's mark price for a symbol. null = the market is not listed or its mark is not a
|
|
63
|
+
* positive finite number (Pacifica rows carry markPrice "-" when /info/prices has none). A failed
|
|
64
|
+
* market read throws (never a 0 that callers would size against).
|
|
65
|
+
*/
|
|
66
|
+
export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number | null>;
|
|
63
67
|
export interface FundingScore {
|
|
64
68
|
symbol: string;
|
|
65
69
|
exchange: string;
|
|
@@ -82,6 +86,14 @@ export declare function isPositionGone(e: unknown): boolean;
|
|
|
82
86
|
* points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
|
|
83
87
|
* mistake a 429 / timeout for "history too thin".
|
|
84
88
|
*/
|
|
89
|
+
/**
|
|
90
|
+
* Funding-history rows one request (or the adapter's paging) can serve per venue — a
|
|
91
|
+
* `history_periods` above it fails every scoring call, so strategies validate it at config time.
|
|
92
|
+
* Venues without an entry page without a limit. Measured / documented in docs/VENUE_READ_CAPS.md.
|
|
93
|
+
*/
|
|
94
|
+
export declare const FUNDING_HISTORY_CAP: Readonly<Record<string, number>>;
|
|
95
|
+
/** Throws INVALID_PARAMS when `periods` is not a positive integer or exceeds a leg venue's cap. */
|
|
96
|
+
export declare function assertFundingHistoryPeriods(periods: unknown, venues: readonly string[], param?: string): void;
|
|
85
97
|
export declare function scoreFunding(adapter: ExchangeAdapter, symbol: string, exchangeName: string, periods: number, cache?: Map<string, {
|
|
86
98
|
score: FundingScore;
|
|
87
99
|
ts: number;
|
|
@@ -10,7 +10,7 @@ import { PerpError, extractErrorMessage } from "../../errors.js";
|
|
|
10
10
|
import { assertFundingArbVenue, tryResolveVenueId } from "../../exchanges/venue.js";
|
|
11
11
|
import "../../exchanges/venues/index.js";
|
|
12
12
|
/**
|
|
13
|
-
* Funding strategies refuse a venue whose funding rate is unverified as a leg (
|
|
13
|
+
* Funding strategies refuse a venue whose funding rate is unverified as a leg (
|
|
14
14
|
* #6): every adapter of the run (primary + extraAdapters) is checked when the strategy starts.
|
|
15
15
|
* Adapter names that are not registered venues (test doubles) are not venues and are skipped.
|
|
16
16
|
*/
|
|
@@ -38,7 +38,7 @@ export function buildAdapterMap(ctx) {
|
|
|
38
38
|
}
|
|
39
39
|
return adapters;
|
|
40
40
|
}
|
|
41
|
-
// ── Transfer helpers
|
|
41
|
+
// ── Transfer helpers ──
|
|
42
42
|
// Spot adapters come from the factory (src/exchanges/spot-factory.ts, getBotSpotAdapter).
|
|
43
43
|
/**
|
|
44
44
|
* Move the spot venue's quote asset (quoteAssets[0]) perp → spot. Returns false when
|
|
@@ -52,7 +52,7 @@ export async function transferUsdcToSpot(spotAdapter, amount) {
|
|
|
52
52
|
export async function transferUsdcToPerp(spotAdapter, amount) {
|
|
53
53
|
return transferQuoteToPerp(spotAdapter, amount);
|
|
54
54
|
}
|
|
55
|
-
// ── Spot ↔ perp matching
|
|
55
|
+
// ── Spot ↔ perp matching ──
|
|
56
56
|
/**
|
|
57
57
|
* Bot spot↔perp candidates: spot bases on `spotVenue` matched to each perp venue by
|
|
58
58
|
* canonical symbol + price-deviation guard (`matchSpotPerp`). Bare-ticker equality is
|
|
@@ -133,18 +133,20 @@ export function matchSymbol(s, target) {
|
|
|
133
133
|
const t = target.toUpperCase();
|
|
134
134
|
return u === t || u === t + "-PERP" || u.replace(/-PERP$/, "") === t;
|
|
135
135
|
}
|
|
136
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* The perp venue's mark price for a symbol. null = the market is not listed or its mark is not a
|
|
138
|
+
* positive finite number (Pacifica rows carry markPrice "-" when /info/prices has none). A failed
|
|
139
|
+
* market read throws (never a 0 that callers would size against).
|
|
140
|
+
*/
|
|
137
141
|
export async function getPriceEstimate(perpAdapter, perpSymbol, fallbackSymbol) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return 0;
|
|
147
|
-
}
|
|
142
|
+
const markets = await perpAdapter.getMarkets();
|
|
143
|
+
const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
|
|
144
|
+
m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
|
|
145
|
+
m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
|
|
146
|
+
if (market === undefined)
|
|
147
|
+
return null;
|
|
148
|
+
const px = Number(market.markPrice);
|
|
149
|
+
return Number.isFinite(px) && px > 0 ? px : null;
|
|
148
150
|
}
|
|
149
151
|
/**
|
|
150
152
|
* true when a reduce-only close was rejected because there is no position left to reduce.
|
|
@@ -172,7 +174,7 @@ function hasPerSymbolFundingHours(adapter) {
|
|
|
172
174
|
/**
|
|
173
175
|
* Funding basis (hours one quoted rate covers) for a market: the market's own
|
|
174
176
|
* value when the venue publishes one per symbol, else the venue definition.
|
|
175
|
-
* History timestamp spacing is settlement timing, never the basis
|
|
177
|
+
* History timestamp spacing is settlement timing, never the basis.
|
|
176
178
|
* @returns null when the venue publishes per-symbol values and this symbol has none
|
|
177
179
|
*/
|
|
178
180
|
async function marketFundingHours(adapter, exchangeName, symbol) {
|
|
@@ -187,6 +189,29 @@ async function marketFundingHours(adapter, exchangeName, symbol) {
|
|
|
187
189
|
* points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
|
|
188
190
|
* mistake a 429 / timeout for "history too thin".
|
|
189
191
|
*/
|
|
192
|
+
/**
|
|
193
|
+
* Funding-history rows one request (or the adapter's paging) can serve per venue — a
|
|
194
|
+
* `history_periods` above it fails every scoring call, so strategies validate it at config time.
|
|
195
|
+
* Venues without an entry page without a limit. Measured / documented in docs/VENUE_READ_CAPS.md.
|
|
196
|
+
*/
|
|
197
|
+
export const FUNDING_HISTORY_CAP = {
|
|
198
|
+
// hyperliquid: fundingHistory pages forward, but `-l` above 5000 is refused (HL_FUNDING_HISTORY_MAX_LIMIT).
|
|
199
|
+
// pacifica: cursor paging, 4000 per page × 10 pages (PACIFICA_FUNDING_PAGE × PACIFICA_MAX_PAGES).
|
|
200
|
+
hyperliquid: 5000, pacifica: 40_000, lighter: 750, "lighter-rh": 750, arcus: 1000, aster: 1000, grvt: 10_000, extended: 10_000,
|
|
201
|
+
};
|
|
202
|
+
/** Throws INVALID_PARAMS when `periods` is not a positive integer or exceeds a leg venue's cap. */
|
|
203
|
+
export function assertFundingHistoryPeriods(periods, venues, param = "history_periods") {
|
|
204
|
+
if (typeof periods !== "number" || !Number.isInteger(periods) || periods < 1) {
|
|
205
|
+
throw new PerpError("INVALID_PARAMS", `${param} must be a positive integer (got ${String(periods)})`, { remediation: `Set ${param} to e.g. 6` });
|
|
206
|
+
}
|
|
207
|
+
const over = venues.map(v => v.toLowerCase()).filter(v => FUNDING_HISTORY_CAP[v] !== undefined && periods > FUNDING_HISTORY_CAP[v]);
|
|
208
|
+
if (over.length > 0) {
|
|
209
|
+
const caps = over.map(v => `${v} ${FUNDING_HISTORY_CAP[v]}`).join(", ");
|
|
210
|
+
throw new PerpError("INVALID_PARAMS", `${param} ${periods} exceeds the funding-history cap of ${caps}`, {
|
|
211
|
+
remediation: `Set ${param} to at most ${Math.min(...over.map(v => FUNDING_HISTORY_CAP[v]))}`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
190
215
|
export async function scoreFunding(adapter, symbol, exchangeName, periods, cache) {
|
|
191
216
|
const key = `${exchangeName}:${symbol}`;
|
|
192
217
|
const cached = cache?.get(key);
|
|
@@ -197,15 +222,23 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
|
|
|
197
222
|
if (fH === null)
|
|
198
223
|
return null; // per-symbol basis unknown — the hourly rate would be a guess
|
|
199
224
|
const history = await adapter.getFundingHistory(symbol, periods);
|
|
200
|
-
|
|
225
|
+
// A print without a rate is unknown, not 0, and a closed-market window (QFEX outside
|
|
226
|
+
// market_hours: rate null, marketClosed) carries no funding: both left out of the average.
|
|
227
|
+
const known = history.filter((h) => h.rate !== null && h.marketClosed !== true);
|
|
228
|
+
if (known.length < 2)
|
|
201
229
|
return null;
|
|
202
|
-
const rates =
|
|
230
|
+
const rates = known.map(h => parseFloat(h.rate));
|
|
203
231
|
if (rates.some(r => !Number.isFinite(r))) {
|
|
204
232
|
throw new PerpError("EXCHANGE_ERROR", `${exchangeName} ${symbol}: funding history has a non-numeric rate`, { exchange: exchangeName });
|
|
205
233
|
}
|
|
206
|
-
|
|
234
|
+
// Every print normalized by its own interval (Lighter /fundings prints are per 1 h while the
|
|
235
|
+
// venue's quoted rate is per 8 h); intervalHours null → the market basis fH.
|
|
236
|
+
const hourly = known.map((h, i) => rates[i] / (typeof h.intervalHours === "number" ? h.intervalHours : fH));
|
|
237
|
+
const avgHourly = hourly.reduce((s, r) => s + r, 0) / hourly.length;
|
|
238
|
+
/** Average rate per the market's own interval (fH), from interval-normalized prints. */
|
|
239
|
+
const avgRate = avgHourly * fH;
|
|
207
240
|
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
208
|
-
const annualized = annualizeHourlyRate(
|
|
241
|
+
const annualized = annualizeHourlyRate(avgHourly);
|
|
209
242
|
const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
|
|
210
243
|
cache?.set(key, { score, ts: Date.now() });
|
|
211
244
|
return score;
|
|
@@ -217,7 +250,7 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
|
|
|
217
250
|
export async function fetchRates(adapter, exchangeName) {
|
|
218
251
|
const markets = await adapter.getMarkets();
|
|
219
252
|
const withRates = markets.filter(m => m.fundingRate != null);
|
|
220
|
-
// fundingHours = basis
|
|
253
|
+
// fundingHours = basis: market value first, then the venue definition.
|
|
221
254
|
// Per-symbol venues (Aster) load their whole table in one call; symbols it
|
|
222
255
|
// does not list stay null and are skipped below.
|
|
223
256
|
for (const m of withRates) {
|
|
@@ -5,7 +5,7 @@ import { closePerpVerified, isOutcomeUnknown, placePerpVerified, trimFirstLegToH
|
|
|
5
5
|
import { fillSizeString, sizeDecimalsOf } from "../../exchanges/spot-result.js";
|
|
6
6
|
import { PerpError } from "../../errors.js";
|
|
7
7
|
import { assertFundingArbLegs } from "./funding-arb-utils.js";
|
|
8
|
-
/** Market basis first (ExchangeMarketInfo.fundingHours), else the venue definition's basis
|
|
8
|
+
/** Market basis first (ExchangeMarketInfo.fundingHours), else the venue definition's basis. */
|
|
9
9
|
function hourlyOf(info, exchange) {
|
|
10
10
|
return info.fundingHours !== null ? info.rate / info.fundingHours : toHourlyRate(info.rate, exchange);
|
|
11
11
|
}
|
|
@@ -177,7 +177,7 @@ class FundingAutoStrategy {
|
|
|
177
177
|
const surviving = [];
|
|
178
178
|
for (const pos of positions) {
|
|
179
179
|
if (isQuarantined(pos.symbol, ctx.log)) {
|
|
180
|
-
// Unknown / unhedged outcome recorded by some bot or arb path: hands off
|
|
180
|
+
// Unknown / unhedged outcome recorded by some bot or arb path: hands off.
|
|
181
181
|
surviving.push(pos);
|
|
182
182
|
continue;
|
|
183
183
|
}
|
|
@@ -19,6 +19,7 @@ export declare class LiquidationMmStrategy implements Strategy {
|
|
|
19
19
|
};
|
|
20
20
|
private get params();
|
|
21
21
|
private _config;
|
|
22
|
+
private _oiUnknownLogged;
|
|
22
23
|
init(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<void>;
|
|
23
24
|
onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
24
25
|
onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
@@ -17,9 +17,10 @@ export class LiquidationMmStrategy {
|
|
|
17
17
|
return this._config;
|
|
18
18
|
}
|
|
19
19
|
_config = {};
|
|
20
|
+
_oiUnknownLogged = false;
|
|
20
21
|
async init(ctx, snapshot) {
|
|
21
22
|
this._config = ctx.config;
|
|
22
|
-
ctx.state.set("prevOi", parseFloat(snapshot.openInterest) || 0);
|
|
23
|
+
ctx.state.set("prevOi", snapshot.openInterest === null ? 0 : parseFloat(snapshot.openInterest) || 0);
|
|
23
24
|
ctx.state.set("cascadeMode", false);
|
|
24
25
|
ctx.state.set("cascadeTicks", 0);
|
|
25
26
|
const p = this.params;
|
|
@@ -29,7 +30,12 @@ export class LiquidationMmStrategy {
|
|
|
29
30
|
const params = this.params;
|
|
30
31
|
const actions = [];
|
|
31
32
|
actions.push({ type: "cancel_all" });
|
|
32
|
-
|
|
33
|
+
// Unknown OI (null): cascade detection is off — said once per run, never computed on a fake 0.
|
|
34
|
+
if (snapshot.openInterest === null && !this._oiUnknownLogged) {
|
|
35
|
+
this._oiUnknownLogged = true;
|
|
36
|
+
ctx.log(" [LIQUIDATION-MM] open interest unknown — OI cascade detection is off");
|
|
37
|
+
}
|
|
38
|
+
const currentOi = snapshot.openInterest === null ? 0 : parseFloat(snapshot.openInterest) || 0;
|
|
33
39
|
const prevOi = ctx.state.get("prevOi") ?? currentOi;
|
|
34
40
|
let cascadeMode = ctx.state.get("cascadeMode") ?? false;
|
|
35
41
|
let cascadeTicks = ctx.state.get("cascadeTicks") ?? 0;
|
|
@@ -9,6 +9,7 @@ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from
|
|
|
9
9
|
export declare class MomentumBreakoutStrategy implements Strategy {
|
|
10
10
|
readonly name = "momentum-breakout";
|
|
11
11
|
private _config;
|
|
12
|
+
private _noVolumeLogged;
|
|
12
13
|
describe(): {
|
|
13
14
|
description: string;
|
|
14
15
|
params: ({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
|
-
import { calculateATR, calculateSMA } from "../indicators.js";
|
|
2
|
+
import { calculateATR, calculateSMA, klineVolumes } from "../indicators.js";
|
|
3
3
|
/**
|
|
4
4
|
* Momentum breakout strategy.
|
|
5
5
|
* Entry: price breaks above N-period high (buy) or below N-period low (sell)
|
|
@@ -10,6 +10,7 @@ import { calculateATR, calculateSMA } from "../indicators.js";
|
|
|
10
10
|
export class MomentumBreakoutStrategy {
|
|
11
11
|
name = "momentum-breakout";
|
|
12
12
|
_config = {};
|
|
13
|
+
_noVolumeLogged = false;
|
|
13
14
|
describe() {
|
|
14
15
|
return {
|
|
15
16
|
description: "Momentum breakout with volume confirmation and ATR trailing stop",
|
|
@@ -42,7 +43,14 @@ export class MomentumBreakoutStrategy {
|
|
|
42
43
|
const closes = klines.map(k => parseFloat(k.close));
|
|
43
44
|
const highs = klines.map(k => parseFloat(k.high));
|
|
44
45
|
const lows = klines.map(k => parseFloat(k.low));
|
|
45
|
-
|
|
46
|
+
// Unknown candle volume (null / non-numeric) cannot confirm a breakout: no entry, exits still run.
|
|
47
|
+
const known = klineVolumes(klines);
|
|
48
|
+
const volumeKnown = known !== null;
|
|
49
|
+
if (!volumeKnown && !this._noVolumeLogged) {
|
|
50
|
+
this._noVolumeLogged = true; // once per run, not every tick
|
|
51
|
+
ctx.log(" [BREAKOUT] no candle volume from the venue — volume confirmation unavailable, no new entries");
|
|
52
|
+
}
|
|
53
|
+
const volumes = known ?? klines.map(() => NaN);
|
|
46
54
|
const price = snapshot.price;
|
|
47
55
|
// Compute indicators
|
|
48
56
|
const candles = klines.map(k => ({
|
|
@@ -61,7 +69,7 @@ export class MomentumBreakoutStrategy {
|
|
|
61
69
|
const avgVolSma = calculateSMA(volumes, this.lookbackPeriod);
|
|
62
70
|
const avgVolume = avgVolSma.length > 0 ? avgVolSma[avgVolSma.length - 1] : 0;
|
|
63
71
|
const currentVolume = volumes[volumes.length - 1];
|
|
64
|
-
const volumeConfirmed = avgVolume > 0 && currentVolume > avgVolume * this.volumeMultiplier;
|
|
72
|
+
const volumeConfirmed = volumeKnown && avgVolume > 0 && currentVolume > avgVolume * this.volumeMultiplier;
|
|
65
73
|
const position = ctx.state.get("position");
|
|
66
74
|
// If no position, look for breakout entry
|
|
67
75
|
if (!position) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spot-Perp Arbitrage Strategy
|
|
3
3
|
*
|
|
4
|
-
* Focused strategy: buy spot (HL or Lighter) + short perp on the
|
|
4
|
+
* Focused strategy: buy spot (HL or Lighter) + short perp on the best of the configured perp exchanges (`exchanges`).
|
|
5
5
|
* No perp-perp logic. History-based entry/exit with short rotation.
|
|
6
6
|
*
|
|
7
7
|
* Strategy layer handles ONLY decisions (opportunity scanning, scoring, close/rotation decisions).
|