Skip to content

fix: hyperliquid end-to-end support (markets, market orders, markPrice)#119

Merged
luokerenx4 merged 3 commits intomasterfrom
dev
Apr 11, 2026
Merged

fix: hyperliquid end-to-end support (markets, market orders, markPrice)#119
luokerenx4 merged 3 commits intomasterfrom
dev

Conversation

@luokerenx4
Copy link
Copy Markdown
Contributor

Summary

Make Hyperliquid actually work in OpenAlice. Three independent fixes needed before users could trade on it, plus a small refactor of the CCXT override mechanism that emerged from the work.

Hyperliquid market loading

`CcxtBroker` was overriding `fetchMarkets.types` with a hardcoded `['spot', 'linear', 'inverse']` list. Bybit defaults match this, but Hyperliquid uses `['spot', 'swap', 'hip3']` — so all swap/perp markets were silently dropped, leaving users unable to find BTC-PERP. The wrapper now uses each exchange's own declared types and only filters out `option`.

E2E setup recognized wallet-based CCXT

`hasCredentials()` hardcoded `bc.apiKey` for ccxt accounts, filtering out wallet-based exchanges. Now uses `CCXT_CREDENTIAL_FIELDS.some()` so hyperliquid (walletAddress + privateKey) is recognized.

Hyperliquid market orders

Hyperliquid has no native market orders — they're emulated as IOC limit orders bounded by a slippage price. CCXT requires the caller to pass a reference price even for `type='market'`. Hyperliquid override now `fetchTicker`s first, then delegates.

CCXT override refactor

The previous override pattern was "either replace the default or copy its body". Refactored so each hook receives `defaultImpl` as the final parameter. Overrides can:

  • Call `defaultImpl(...)` directly
  • Modify inputs and call `defaultImpl(modifiedArgs)`
  • Post-process `defaultImpl`'s result
  • Ignore it entirely (full replacement)

Hyperliquid markPrice fix

CCXT's hyperliquid `parsePosition` hardcodes `markPrice: undefined` (line 3613). It does expose `positionValue` (mapped to `notional`), so the new `fetchPositions` override recovers `markPrice = |notional| / |contracts|`.

New e2e specs

  • `ccxt-hyperliquid-markets.e2e.spec.ts`: credential-free regression test, always runs
  • `ccxt-hyperliquid.e2e.spec.ts`: full e2e against hyperliquid testnet (8 tests, requires testnet wallet config)

Bumps to `0.9.0-beta.12`.

Test plan

  • `npx tsc --noEmit` zero errors
  • `pnpm test` — 974 tests pass
  • Hyperliquid full e2e — 8/8 passing on testnet (BTC perp trade roundtrip, BTC position reports actual mark price ~72931 USD)
  • Bybit / Alpaca / IBKR e2e unaffected

🤖 Generated with Claude Code

Ame and others added 3 commits April 11, 2026 17:36
CcxtBroker hardcoded fetchMarkets.types = ['spot', 'linear', 'inverse'],
which overrode the exchange's own defaults. Bybit happened to work
because its defaults matched, but Hyperliquid uses ['spot', 'swap', 'hip3']
— so all swap/perp markets were silently dropped, leaving users unable
to find BTC-PERP or any derivative.

- Stop overriding fetchMarkets.types in the constructor; let each exchange
  use its declared defaults
- fetchMarkets wrapper reads exchange's own types and only filters out
  'option' (the universal option-stripping intent stays)
- Guard searchContracts against markets with undefined base/quote
  (some hyperliquid spot markets have these fields missing)

E2E setup
- hasCredentials() previously hardcoded `bc.apiKey` for ccxt, filtering
  out any wallet-based account. Use CCXT_CREDENTIAL_FIELDS.some() so
  wallet-based exchanges (hyperliquid, dYdX) are recognized

New e2e specs
- ccxt-hyperliquid-markets.e2e.spec.ts: credential-free regression test
  using dummy keys, validates spot+swap markets load and BTC perp is
  searchable. Always runs.
- ccxt-hyperliquid.e2e.spec.ts: full e2e against hyperliquid testnet.
  Skips when no hyperliquid sandbox account is configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hyperliquid has no native market order type — orders are emulated as IOC
limit orders bounded by a slippage price (default 5%). CCXT requires the
caller to pass a reference price even for type='market' so it can compute
the slippage bound, otherwise it throws ArgumentsRequired. The hyperliquid
server also enforces an 80% deviation cap from mark price, ruling out
extreme dummy values — a real reference price is required.

- Add placeOrder hook to CcxtExchangeOverrides interface (mirrors the
  existing fetchOrderById/cancelOrderById hooks)
- defaultPlaceOrder passes through to ccxt.createOrder unchanged
- exchanges/hyperliquid.ts overrides placeOrder to fetchTicker first when
  the order is a market order missing a reference price
- CcxtBroker.placeOrder routes through this.overrides.placeOrder ?? default
- Bump hyperliquid e2e test timeouts from 15s/30s to 60s — testnet is
  noticeably slower than bybit (extra fetchTicker RTT plus base latency)

E2E result on hyperliquid testnet: 8/8 passing — market buy/sell, position
verification, reduceOnly close, and getOrder all work end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rkPrice

Reshape CcxtExchangeOverrides so each hook receives the original args plus
a defaultImpl as the final parameter. The override decides what to do with
it: invoke directly, modify inputs and call it, post-process its result, or
ignore it entirely. This replaces the previous either/or pattern that forced
overrides to either fully replace the default or copy its body.

- All four hooks (fetchOrderById, cancelOrderById, placeOrder, fetchPositions)
  now follow the same convention
- New hook fetchPositions + defaultFetchPositions for cases where ccxt's
  parsePosition leaves fields undefined
- bybit's fetchOrderById is unchanged (it fully replaces); just adopts the
  new signature with an unused _defaultImpl parameter
- hyperliquid's placeOrder now calls defaultImpl(modifiedArgs) instead of
  exchange.createOrder directly — future enhancements to defaultPlaceOrder
  (retries, logging) will flow through automatically
- New hyperliquid fetchPositions override recovers markPrice from notional
  / contracts. CCXT's parsePosition hardcodes markPrice: undefined for
  hyperliquid, but it does expose positionValue (mapped to notional), so
  the math works out: |notional| / |contracts| = mark price

E2E confirms: BTC perp position now reports `@ 72931 USD` instead of `@ 0`,
and the new regression assertion (marketPrice > 0, marketValue ≈ qty × price)
catches future regressions. All 8 hyperliquid e2e tests pass.

- Bump version to 0.9.0-beta.12

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@luokerenx4 luokerenx4 merged commit 578b0d1 into master Apr 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant