perp-cli 0.14.3 → 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 +2 -2
- package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-utils.js +5 -5
- 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 +2 -2
- 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 +167 -35
- 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/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 +4 -4
- package/dist/exchanges/aster.js +31 -13
- 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 -12
- package/dist/exchanges/hyperliquid.js +80 -34
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +94 -6
- package/dist/exchanges/interface.js +16 -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 +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.js +2 -2
- 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)
|
|
@@ -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);
|
|
@@ -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
|
*/
|
|
@@ -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
|
|
@@ -172,7 +172,7 @@ function hasPerSymbolFundingHours(adapter) {
|
|
|
172
172
|
/**
|
|
173
173
|
* Funding basis (hours one quoted rate covers) for a market: the market's own
|
|
174
174
|
* value when the venue publishes one per symbol, else the venue definition.
|
|
175
|
-
* History timestamp spacing is settlement timing, never the basis
|
|
175
|
+
* History timestamp spacing is settlement timing, never the basis.
|
|
176
176
|
* @returns null when the venue publishes per-symbol values and this symbol has none
|
|
177
177
|
*/
|
|
178
178
|
async function marketFundingHours(adapter, exchangeName, symbol) {
|
|
@@ -217,7 +217,7 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
|
|
|
217
217
|
export async function fetchRates(adapter, exchangeName) {
|
|
218
218
|
const markets = await adapter.getMarkets();
|
|
219
219
|
const withRates = markets.filter(m => m.fundingRate != null);
|
|
220
|
-
// fundingHours = basis
|
|
220
|
+
// fundingHours = basis: market value first, then the venue definition.
|
|
221
221
|
// Per-symbol venues (Aster) load their whole table in one call; symbols it
|
|
222
222
|
// does not list stay null and are skipped below.
|
|
223
223
|
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) {
|
|
@@ -285,7 +285,7 @@ export class SpotPerpArbStrategy {
|
|
|
285
285
|
ctx.state.set("spaPositions", remaining);
|
|
286
286
|
ctx.state.set("arbPositions", remaining.length);
|
|
287
287
|
}
|
|
288
|
-
// ── Rotation check (only to perp venues that match the spot leg
|
|
288
|
+
// ── Rotation check (only to perp venues that match the spot leg) ──
|
|
289
289
|
const perpMarketsCache = new Map();
|
|
290
290
|
const allowedBySpotVenue = new Map();
|
|
291
291
|
const currentPositions = ctx.state.get("spaPositions");
|
|
@@ -375,7 +375,7 @@ export class SpotPerpArbStrategy {
|
|
|
375
375
|
const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
|
|
376
376
|
if (!spotAdapter)
|
|
377
377
|
continue;
|
|
378
|
-
// Canonical symbol + price guard
|
|
378
|
+
// Canonical symbol + price guard: base → perp venues it may hedge on.
|
|
379
379
|
const allowedByBase = await this.spotPerpVenues(spotExName, await spotAdapter.getSpotMarkets(), adapters, perpMarketsCache, ctx);
|
|
380
380
|
for (const [base, allowed] of allowedByBase) {
|
|
381
381
|
// HL spot: skip tokens not in whitelist
|
|
@@ -26,7 +26,7 @@ export type ExecutionResult = {
|
|
|
26
26
|
/**
|
|
27
27
|
* closed = position fully closed; remainingSize = partially closed, this size is still open (hedged).
|
|
28
28
|
* manual = the outcome is unknown / unverified or a leg is unhedged: the caller must quarantine
|
|
29
|
-
* the position and never retry it automatically
|
|
29
|
+
* the position and never retry it automatically.
|
|
30
30
|
*/
|
|
31
31
|
export type CloseResult = {
|
|
32
32
|
closed: true;
|
|
@@ -140,7 +140,7 @@ export class SpotPerpExecutor {
|
|
|
140
140
|
await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
|
|
141
141
|
return { success: false, error: `pre-trade balance: ${msg}` };
|
|
142
142
|
}
|
|
143
|
-
// Step 1: Buy spot — the hedge is sized to what actually filled
|
|
143
|
+
// Step 1: Buy spot — the hedge is sized to what actually filled
|
|
144
144
|
let fill;
|
|
145
145
|
try {
|
|
146
146
|
fill = await spotMarketFill(spotAdapter, symbol, "buy", matched.size, spotStep);
|
|
@@ -10,7 +10,8 @@ export interface EnrichedSnapshot extends MarketSnapshot {
|
|
|
10
10
|
bids: [string, string][];
|
|
11
11
|
asks: [string, string][];
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
/** Base-asset open interest; null = not read / not published (never "0" as a stand-in). */
|
|
14
|
+
openInterest: string | null;
|
|
14
15
|
}
|
|
15
16
|
export type StrategyAction = {
|
|
16
17
|
type: "place_order";
|
|
@@ -78,7 +78,7 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
|
|
|
78
78
|
const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
|
|
79
79
|
const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
|
|
80
80
|
const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
|
|
81
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest ?? "-" })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
|
|
82
82
|
const upnl = parseFloat(p.unrealizedPnl);
|
|
83
83
|
return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${upnl.toFixed(2)}` })] }, i));
|
|
84
84
|
})] }))] }), openOrdersError !== null ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Open orders unavailable (", openOrdersError, ")"] }) })) : (_jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
|
package/dist/cache.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
10
11
|
const CACHE_DIR = join(tmpdir(), "perp-cli-cache");
|
|
11
12
|
// TTL presets
|
|
12
13
|
export const TTL_ACCOUNT = 15_000; // 15s — balance, positions, orders (ticks are 30s; 15s ensures same-tick dedup)
|
|
@@ -41,16 +42,21 @@ export function getCached(key) {
|
|
|
41
42
|
}
|
|
42
43
|
export function setCached(key, data, ttlMs) {
|
|
43
44
|
ensureDir();
|
|
45
|
+
const file = join(CACHE_DIR, `${safeKey(key)}.json`);
|
|
46
|
+
// Unique per writer: concurrent processes writing the same key must not share one tmp file
|
|
47
|
+
// (a rename of another writer's half-written tmp would publish a partial entry).
|
|
48
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
44
49
|
try {
|
|
45
50
|
const entry = { ts: Date.now(), ttl: ttlMs, data };
|
|
46
|
-
|
|
47
|
-
const file = join(CACHE_DIR, `${safeKey(key)}.json`);
|
|
48
|
-
const tmp = file + ".tmp";
|
|
49
|
-
writeFileSync(tmp, content);
|
|
51
|
+
writeFileSync(tmp, JSON.stringify(entry));
|
|
50
52
|
renameSync(tmp, file);
|
|
51
53
|
}
|
|
52
54
|
catch {
|
|
53
|
-
// cache write failure is non-fatal
|
|
55
|
+
// cache write failure is non-fatal; never leave this writer's tmp file behind
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(tmp);
|
|
58
|
+
}
|
|
59
|
+
catch { /* not written */ }
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
@@ -94,7 +100,7 @@ export function invalidateCache(keyPrefix) {
|
|
|
94
100
|
const files = readdirSync(CACHE_DIR);
|
|
95
101
|
for (const f of files) {
|
|
96
102
|
if (!f.endsWith(".json"))
|
|
97
|
-
continue;
|
|
103
|
+
continue; // also skips in-flight "<key>.json.<pid>.<rand>.tmp" files
|
|
98
104
|
if (!prefix || f.startsWith(prefix)) {
|
|
99
105
|
try {
|
|
100
106
|
unlinkSync(join(CACHE_DIR, f));
|
package/dist/cli-program.d.ts
CHANGED
|
@@ -28,8 +28,23 @@ export declare function landingData(program: Command, version: string): {
|
|
|
28
28
|
name: string;
|
|
29
29
|
description: string;
|
|
30
30
|
}[];
|
|
31
|
+
venues: {
|
|
32
|
+
id: string;
|
|
33
|
+
displayName: string;
|
|
34
|
+
roles: string[];
|
|
35
|
+
apiStatus: "official" | "unofficial";
|
|
36
|
+
note: string | null;
|
|
37
|
+
}[];
|
|
31
38
|
};
|
|
32
39
|
export declare function buildProgram(opts: {
|
|
33
40
|
defaultExchange: string;
|
|
34
41
|
version: string;
|
|
35
42
|
}): CliProgram;
|
|
43
|
+
/**
|
|
44
|
+
* The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
|
|
45
|
+
* `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
|
|
46
|
+
* (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
|
|
47
|
+
* before the dry-run guard and before any action, so a dry-run plan can never present a gated
|
|
48
|
+
* venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assertCommandVenueRole(actionCommand: Command, exchangeOpt: string): void;
|