perp-cli 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -13
- 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/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- 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/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- 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.js +23 -12
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- 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 +125 -5
- 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 +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- 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.js +168 -36
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- 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 +63 -58
- 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/dashboard/server.js +1 -1
- package/dist/dashboard/ui.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- 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/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 +9 -4
- package/dist/exchanges/aster.js +125 -55
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -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 +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -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-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- 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 +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- 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 +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- 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/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 +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- 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 +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- 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 +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- 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 +2 -2
- 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 +148 -0
- package/dist/exchanges/risex/adapter.js +796 -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 +7 -7
- 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 +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -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 +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -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 +10 -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 +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- 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 +6 -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 +116 -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 +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- 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/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -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/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 +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- package/dist/utils.d.ts +12 -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/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
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
|
|
@@ -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)
|
|
@@ -7,7 +7,7 @@ import { logExecution, pruneExecutionLog } from "../../execution-log.js";
|
|
|
7
7
|
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
8
8
|
import { getBotSpotAdapter, nonQuoteSpotBalances, readSpotQuoteAvailableFresh, spotNetworkOf, spotTokenBase, sweepSpotQuoteToPerp } from "../../exchanges/spot-factory.js";
|
|
9
9
|
import { getVenue } from "../../exchanges/venue.js";
|
|
10
|
-
import { isPositionGone, transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, findRate, getPriceEstimate, fetchRates, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
|
|
10
|
+
import { isPositionGone, transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, findRate, cappedLeverage, getPriceEstimate, fetchRates, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
|
|
11
11
|
import { fillSizeString, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../../exchanges/spot-result.js";
|
|
12
12
|
import { floorToStep, lotStepOf } from "../../exchanges/precision.js";
|
|
13
13
|
import { isQuarantined, quarantineKey, quarantineSymbol } from "../quarantine.js";
|
|
@@ -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);
|
|
@@ -637,9 +637,10 @@ export class FundingArbStrategy {
|
|
|
637
637
|
continue;
|
|
638
638
|
// Determine leverage for this symbol
|
|
639
639
|
const configLeverage = params.leverage ?? 3;
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
640
|
+
const leverage = cappedLeverage(configLeverage, [
|
|
641
|
+
{ exchange: best.longExchange, maxLeverage: findRate(ratesByExchange, best.longExchange, best.symbol)?.maxLeverage },
|
|
642
|
+
{ exchange: best.shortExchange, maxLeverage: findRate(ratesByExchange, best.shortExchange, best.symbol)?.maxLeverage },
|
|
643
|
+
], ctx.log);
|
|
643
644
|
// Set leverage on both exchanges before ordering
|
|
644
645
|
try {
|
|
645
646
|
await longAdapter.setLeverage(best.symbol, leverage, "cross");
|
|
@@ -1020,9 +1021,8 @@ export class FundingArbStrategy {
|
|
|
1020
1021
|
const configLeverage = params.leverage ?? 3;
|
|
1021
1022
|
const perpSymbolUpper = getPerpSymbol(opp.symbol, opp.shortExchange).toUpperCase();
|
|
1022
1023
|
const perpMaxLev = findRate(ratesByExchange, opp.shortExchange, perpSymbolUpper)?.maxLeverage
|
|
1023
|
-
?? findRate(ratesByExchange, opp.shortExchange, opp.symbol.toUpperCase())?.maxLeverage
|
|
1024
|
-
|
|
1025
|
-
const leverage = Math.min(configLeverage, perpMaxLev);
|
|
1024
|
+
?? findRate(ratesByExchange, opp.shortExchange, opp.symbol.toUpperCase())?.maxLeverage;
|
|
1025
|
+
const leverage = cappedLeverage(configLeverage, [{ exchange: opp.shortExchange, maxLeverage: perpMaxLev }], ctx.log);
|
|
1026
1026
|
// Set leverage on perp before ordering
|
|
1027
1027
|
try {
|
|
1028
1028
|
await perpAdapter.setLeverage(getPerpSymbol(opp.symbol, opp.shortExchange), leverage, "cross");
|
|
@@ -1099,7 +1099,7 @@ export class FundingArbStrategy {
|
|
|
1099
1099
|
const transferAmt = Math.ceil(matched.notional * 1.02); // slight buffer for price movement
|
|
1100
1100
|
if (await transferUsdcToSpot(spotAdapter, transferAmt))
|
|
1101
1101
|
ctx.log(` [ARB] Transferred $${transferAmt} to spot`);
|
|
1102
|
-
// Step 2: Buy spot — the hedge is sized to what actually filled
|
|
1102
|
+
// Step 2: Buy spot — the hedge is sized to what actually filled.
|
|
1103
1103
|
let hedgeSize;
|
|
1104
1104
|
let spotFill;
|
|
1105
1105
|
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
|
*/
|
|
@@ -47,6 +47,15 @@ export declare function botSpotPerpMatches(spotVenue: string, spotMarkets: reado
|
|
|
47
47
|
export declare function getPerpSymbol(baseSymbol: string, _exchangeName: string): string;
|
|
48
48
|
/** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP. */
|
|
49
49
|
export declare function findRate(ratesByExchange: RateMap, exchange: string, symbol: string): RateEntry | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Leverage for an entry: the configured value, capped by every leg's published max.
|
|
52
|
+
* A leg whose max is unpublished (undefined) is not a cap — the venue enforces its own
|
|
53
|
+
* limit on setLeverage / the order — and `logUnknown` names it, so it is never read as 1x.
|
|
54
|
+
*/
|
|
55
|
+
export declare function cappedLeverage(configured: number, legs: {
|
|
56
|
+
exchange: string;
|
|
57
|
+
maxLeverage: number | undefined;
|
|
58
|
+
}[], logUnknown: (msg: string) => void): number;
|
|
50
59
|
/** Match a symbol against a target, accounting for -PERP suffix variations. */
|
|
51
60
|
export declare function matchSymbol(s: string, target: string): boolean;
|
|
52
61
|
/** Get a price estimate for a symbol from the perp adapter. */
|
|
@@ -68,7 +77,11 @@ export interface FundingScore {
|
|
|
68
77
|
* ORDER_PENDING outcome is never "gone").
|
|
69
78
|
*/
|
|
70
79
|
export declare function isPositionGone(e: unknown): boolean;
|
|
71
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Score a symbol's funding rate using history. null = not enough data (fewer than 2 history
|
|
82
|
+
* points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
|
|
83
|
+
* mistake a 429 / timeout for "history too thin".
|
|
84
|
+
*/
|
|
72
85
|
export declare function scoreFunding(adapter: ExchangeAdapter, symbol: string, exchangeName: string, periods: number, cache?: Map<string, {
|
|
73
86
|
score: FundingScore;
|
|
74
87
|
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
|
|
@@ -110,6 +110,23 @@ export function findRate(ratesByExchange, exchange, symbol) {
|
|
|
110
110
|
const upper = symbol.toUpperCase();
|
|
111
111
|
return map.get(upper) ?? map.get(upper + "-PERP") ?? map.get(upper.replace(/-PERP$/, ""));
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Leverage for an entry: the configured value, capped by every leg's published max.
|
|
115
|
+
* A leg whose max is unpublished (undefined) is not a cap — the venue enforces its own
|
|
116
|
+
* limit on setLeverage / the order — and `logUnknown` names it, so it is never read as 1x.
|
|
117
|
+
*/
|
|
118
|
+
export function cappedLeverage(configured, legs, logUnknown) {
|
|
119
|
+
let leverage = configured;
|
|
120
|
+
for (const leg of legs) {
|
|
121
|
+
if (leg.maxLeverage === undefined) {
|
|
122
|
+
logUnknown(` [LEV] ${leg.exchange} does not publish a max leverage — using ${configured}x; the venue enforces its cap`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
leverage = Math.min(leverage, leg.maxLeverage);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return leverage;
|
|
129
|
+
}
|
|
113
130
|
/** Match a symbol against a target, accounting for -PERP suffix variations. */
|
|
114
131
|
export function matchSymbol(s, target) {
|
|
115
132
|
const u = s.toUpperCase();
|
|
@@ -155,7 +172,7 @@ function hasPerSymbolFundingHours(adapter) {
|
|
|
155
172
|
/**
|
|
156
173
|
* Funding basis (hours one quoted rate covers) for a market: the market's own
|
|
157
174
|
* value when the venue publishes one per symbol, else the venue definition.
|
|
158
|
-
* History timestamp spacing is settlement timing, never the basis
|
|
175
|
+
* History timestamp spacing is settlement timing, never the basis.
|
|
159
176
|
* @returns null when the venue publishes per-symbol values and this symbol has none
|
|
160
177
|
*/
|
|
161
178
|
async function marketFundingHours(adapter, exchangeName, symbol) {
|
|
@@ -165,32 +182,33 @@ async function marketFundingHours(adapter, exchangeName, symbol) {
|
|
|
165
182
|
}
|
|
166
183
|
return getFundingHours(exchangeName);
|
|
167
184
|
}
|
|
168
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Score a symbol's funding rate using history. null = not enough data (fewer than 2 history
|
|
187
|
+
* points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
|
|
188
|
+
* mistake a 429 / timeout for "history too thin".
|
|
189
|
+
*/
|
|
169
190
|
export async function scoreFunding(adapter, symbol, exchangeName, periods, cache) {
|
|
170
191
|
const key = `${exchangeName}:${symbol}`;
|
|
171
192
|
const cached = cache?.get(key);
|
|
172
193
|
if (cached && Date.now() - cached.ts < 10 * 60 * 1000)
|
|
173
194
|
return cached.score;
|
|
174
|
-
// Outside the try: an unknown venue is a configuration error, not "no score".
|
|
175
195
|
const venueHours = hasPerSymbolFundingHours(adapter) ? null : getFundingHours(exchangeName);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (history.length < 2)
|
|
182
|
-
return null;
|
|
183
|
-
const rates = history.map(h => parseFloat(h.rate));
|
|
184
|
-
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
185
|
-
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
186
|
-
const annualized = annualizeHourlyRate(avgRate / fH);
|
|
187
|
-
const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
|
|
188
|
-
cache?.set(key, { score, ts: Date.now() });
|
|
189
|
-
return score;
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
196
|
+
const fH = venueHours !== null ? venueHours : await marketFundingHours(adapter, exchangeName, symbol);
|
|
197
|
+
if (fH === null)
|
|
198
|
+
return null; // per-symbol basis unknown — the hourly rate would be a guess
|
|
199
|
+
const history = await adapter.getFundingHistory(symbol, periods);
|
|
200
|
+
if (history.length < 2)
|
|
192
201
|
return null;
|
|
202
|
+
const rates = history.map(h => parseFloat(h.rate));
|
|
203
|
+
if (rates.some(r => !Number.isFinite(r))) {
|
|
204
|
+
throw new PerpError("EXCHANGE_ERROR", `${exchangeName} ${symbol}: funding history has a non-numeric rate`, { exchange: exchangeName });
|
|
193
205
|
}
|
|
206
|
+
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
207
|
+
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
208
|
+
const annualized = annualizeHourlyRate(avgRate / fH);
|
|
209
|
+
const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
|
|
210
|
+
cache?.set(key, { score, ts: Date.now() });
|
|
211
|
+
return score;
|
|
194
212
|
}
|
|
195
213
|
/**
|
|
196
214
|
* Fetch funding rates from an exchange adapter. Failures propagate — callers
|
|
@@ -199,7 +217,7 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
|
|
|
199
217
|
export async function fetchRates(adapter, exchangeName) {
|
|
200
218
|
const markets = await adapter.getMarkets();
|
|
201
219
|
const withRates = markets.filter(m => m.fundingRate != null);
|
|
202
|
-
// fundingHours = basis
|
|
220
|
+
// fundingHours = basis: market value first, then the venue definition.
|
|
203
221
|
// Per-symbol venues (Aster) load their whole table in one call; symbols it
|
|
204
222
|
// does not list stay null and are skipped below.
|
|
205
223
|
for (const m of withRates) {
|
|
@@ -207,6 +225,7 @@ export async function fetchRates(adapter, exchangeName) {
|
|
|
207
225
|
continue;
|
|
208
226
|
m.fundingHours = await marketFundingHours(adapter, exchangeName, m.symbol);
|
|
209
227
|
}
|
|
228
|
+
// maxLeverage undefined = not published by the venue (callers cap by published maxes only — see cappedLeverage).
|
|
210
229
|
const result = [];
|
|
211
230
|
for (const m of withRates) {
|
|
212
231
|
// Skip aster symbols with unknown funding period — the hourly rate would be a guess.
|
|
@@ -221,7 +240,7 @@ export async function fetchRates(adapter, exchangeName) {
|
|
|
221
240
|
rate: parseFloat(m.fundingRate),
|
|
222
241
|
price: parseFloat(m.markPrice),
|
|
223
242
|
sizeDecimals: m.sizeDecimals,
|
|
224
|
-
maxLeverage: m.maxLeverage,
|
|
243
|
+
maxLeverage: m.maxLeverage ?? undefined,
|
|
225
244
|
fundingHours: m.fundingHours,
|
|
226
245
|
});
|
|
227
246
|
}
|
|
@@ -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) {
|