Skip to main content
Use this page when you need account health, margin requirements, transferable collateral, or liquidation estimates after you have live trader state. For account setup and trader-state subscriptions across subaccounts, see Accounts.

Join Market Prices

For margin, liquidation, and account-health views, subscribe to trader state and market stats for the symbols with open positions or orders. Let trader-state resources maintain positions/orders and market-data resources maintain mark prices.
const marketData = client.marketData();
const releaseMarketData = marketData.retain();
await marketData.ready();

resource.subscribe(() => {
  const symbols = new Set<string>();

  for (const subaccountIndex of resource.subaccountIndices()) {
    const subaccount = resource.subaccount(subaccountIndex);
    for (const symbol of subaccount?.positionSymbols ?? []) {
      symbols.add(symbol);
    }
    for (const symbol of subaccount?.orderSymbols ?? []) {
      symbols.add(symbol);
    }
  }

  for (const symbol of symbols) {
    const row = marketData.market(symbol);
    console.log(symbol, row?.markPrice);
  }
});

Local Margin

Use trader-state marginInputs() and market params to compute local margin. For real-time monitoring, update market prices from WebSocket streams before recomputing.
import {
  MarginMarketParamsStore,
  createMarginCalculator,
} from "@ellipsis-labs/rise";

const paramsStore = new MarginMarketParamsStore({
  client: client.api,
  autoRefreshIntervalMs: 30_000,
});

const paramsBySymbol = await paramsStore.getMarketParamsBySymbol();
const calculator = createMarginCalculator(Object.values(paramsBySymbol));
const marginInputs = resource.marginInputs();

if (marginInputs) {
  const margin = calculator.computeTraderMarginFromInputs(marginInputs);
  console.log(margin.subaccounts[0]?.margin);
}
Reference: Rust live margin example.

Cache-Backed Liquidation Estimates

For fast UI updates and risk monitoring, estimate liquidation prices from local trader state plus the exchange cache. This avoids API polling and uses the same SDK margin primitives described above. The examples below binary-search the mark price where the subaccount risk tier crosses into liquidatable or worse. This keeps the estimate tied to SDK margin math, leverage tiers, funding, discounted uPnL, and limit-order margin. Treat it as a local estimate: stream freshness and search bounds matter. Use Hawkeye for the authoritative on-chain simulation. In TypeScript, the flow is client.exchange.ready(), client.exchange.market(symbol), client.marketData().market(symbol), resource.marginInputs(), then createMarginCalculator(...). In Rust, the flow is PhoenixMetadata::apply_market_stats(&stats), Trader::apply_update(&msg), SubaccountState::to_trader_portfolio(), then TraderPortfolio::compute_margin(metadata.all_perp_asset_metadata()).
import {
  createMarginCalculator,
  priceUsdToTicksWithMarketParams,
  type ExchangeMarketSnapshot,
  type MarketParams,
  type SubaccountMarginInputs,
} from "@ellipsis-labs/rise";

const bps = (value: number | undefined, fallback: number) =>
  String(value ?? fallback);

const paramsFromExchangeCache = (
  market: ExchangeMarketSnapshot,
  markPriceUsd: number
): MarketParams => ({
  symbol: market.symbol,
  assetId: market.assetId,
  markPriceTicks: priceUsdToTicksWithMarketParams(markPriceUsd, {
    tickSize: market.tickSize,
    baseLotsDecimals: market.baseLotsDecimals,
  }).toString(),
  tickSize: market.tickSize.toString(),
  baseLotDecimals: market.baseLotsDecimals,
  leverageTiers: market.leverageTiers.map((tier) => ({
    upperBoundSize: tier.maxSizeBaseLots.toString(),
    maxLeverage: String(tier.maxLeverage),
    limitOrderRiskFactorBps: String(tier.limitOrderRiskFactor),
  })),
  riskFactors: {
    maintenanceMarginFactorBps: bps(
      market.riskFactors.maintenanceBps,
      market.riskFactors.maintenance
    ),
    backstopMarginFactorBps: bps(
      market.riskFactors.backstopBps,
      market.riskFactors.backstop
    ),
    highRiskMarginFactorBps: bps(
      market.riskFactors.highRiskBps,
      market.riskFactors.highRisk
    ),
  },
  cancelOrderRiskFactorBps: bps(
    market.riskFactors.cancelOrderBps,
    market.riskFactors.cancelOrder
  ),
  upnlRiskFactor: bps(market.riskFactors.upnlBps, market.riskFactors.upnl),
  upnlRiskFactorForWithdrawals: bps(
    market.riskFactors.upnlForWithdrawalsBps,
    market.riskFactors.upnlForWithdrawals
  ),
  isolatedOnly: market.isolatedOnly,
});

const liquidationTiers = new Set([
  "liquidatable",
  "backstopLiquidatable",
  "highRisk",
]);

const estimateLiquidationPriceUsd = (
  basePositionLots: bigint,
  currentPriceUsd: number,
  crossesLiquidation: (priceUsd: number) => boolean
) => {
  if (basePositionLots === 0n || currentPriceUsd <= 0) return null;
  if (crossesLiquidation(currentPriceUsd)) return currentPriceUsd;

  if (basePositionLots > 0n) {
    let low = Math.max(currentPriceUsd / 1_000_000, Number.EPSILON);
    let high = currentPriceUsd;
    if (!crossesLiquidation(low)) return null;

    for (let i = 0; i < 48; i++) {
      const mid = (low + high) / 2;
      if (crossesLiquidation(mid)) low = mid;
      else high = mid;
    }
    return high;
  }

  let low = currentPriceUsd;
  let high = currentPriceUsd * 2;
  for (let i = 0; i < 20 && !crossesLiquidation(high); i++) {
    low = high;
    high *= 2;
  }
  if (!crossesLiquidation(high)) return null;

  for (let i = 0; i < 48; i++) {
    const mid = (low + high) / 2;
    if (crossesLiquidation(mid)) high = mid;
    else low = mid;
  }
  return high;
};

await client.exchange.ready();

const marketData = client.marketData();
const releaseMarketData = marketData.retain();
await marketData.ready();

const marginInputs = resource.marginInputs();
const subaccount = marginInputs?.subaccounts.find(
  (item) => item.subaccountIndex === 0
);
if (!subaccount) throw new Error("subaccount margin inputs are not ready");

const paramsBySymbol: Record<string, MarketParams> = {};
for (const marketInput of subaccount.markets) {
  const market = client.exchange.market(marketInput.symbol);
  const markPrice = marketData.market(marketInput.symbol)?.markPrice;
  if (!market || markPrice === null || markPrice === undefined) {
    throw new Error(`missing cached market data for ${marketInput.symbol}`);
  }
  paramsBySymbol[marketInput.symbol] = paramsFromExchangeCache(market, markPrice);
}

const symbol = "SOL";
const target = subaccount.markets.find((market) => market.symbol === symbol);
const basePositionLots = BigInt(target?.position?.basePositionLots ?? "0");
const currentPrice = marketData.market(symbol)?.markPrice;
if (currentPrice === null || currentPrice === undefined) {
  throw new Error(`missing mark price for ${symbol}`);
}

const crossesLiquidation = (priceUsd: number) => {
  const market = client.exchange.market(symbol);
  if (!market) throw new Error(`unknown market ${symbol}`);

  const calculator = createMarginCalculator(
    Object.values({
      ...paramsBySymbol,
      [symbol]: paramsFromExchangeCache(market, priceUsd),
    })
  );
  const result = calculator.computeSubaccountMarginFromInputs(
    subaccount as SubaccountMarginInputs
  );
  return liquidationTiers.has(result.margin.riskTier);
};

const estimatedLiquidationPriceUsd = estimateLiquidationPriceUsd(
  basePositionLots,
  currentPrice,
  crossesLiquidation
);

console.log(estimatedLiquidationPriceUsd);
releaseMarketData();
This estimate depends on the same cache freshness as margin monitoring. In TypeScript, keep client.exchange and client.marketData() live. In Rust, keep applying PhoenixMetadata::apply_market_stats(&stats) and trader-state updates before recomputing.

Hawkeye Simulation

For an authoritative liquidation-price view, use Hawkeye simulation through the SDK. This runs a read-only simulation and decodes the program return data.
const margin = await client.rpc.hawkeye.viewMargin({
  authority: "AUTHORITY_PUBKEY",
  traderPdaIndex: 0,
  traderSubaccountIndex: 0,
});

const liquidation = await client.rpc.hawkeye.viewLiquidationPrice({
  authority: "AUTHORITY_PUBKEY",
  traderPdaIndex: 0,
  traderSubaccountIndex: 0,
  symbol: "SOL",
});

console.log(margin.returnData?.decoded);
console.log(liquidation.returnData?.decoded.liquidationPriceTicks);

Isolated Order Estimates

Server-built isolated order routes can also return the post-trade isolated liquidation estimate.
const result = await client.api.orders().placeIsolatedMarketOrderEnhanced({
  authority: "AUTHORITY_PUBKEY",
  symbol: "SOL",
  side: "buy",
  numBaseLots: 67,
  transferAmount: 100_000_000,
  allowCrossAndIsolatedForAsset: true,
});

console.log(result.estimatedLiquidationPriceUsd);