> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phoenix.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Margin

> Compute Phoenix trader margin and liquidation estimates with the Rise SDK.

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](/sdk/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.

<CodeGroup>
  ```ts TypeScript theme={null}
  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);
    }
  });
  ```

  ```rust Rust theme={null}
  use phoenix_rise::api::{PhoenixHttpClient, PhoenixMetadata, PhoenixWSClient};

  let http = PhoenixHttpClient::new_from_env()?;
  let exchange = http.get_exchange().await?.into();
  let mut metadata = PhoenixMetadata::new(exchange);

  let ws = PhoenixWSClient::new_from_env()?;
  let (mut market_rx, _handle) = ws.subscribe_to_market("SOL".to_string())?;

  while let Some(stats) = market_rx.recv().await {
      metadata.apply_market_stats(&stats)?;
      println!("{} mark={}", stats.symbol, stats.mark_price);
  }
  ```
</CodeGroup>

## 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.

<CodeGroup>
  ```ts TypeScript theme={null}
  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);
  }
  ```

  ```rust Rust theme={null}
  if let Some(cross) = trader.primary_subaccount() {
      let portfolio = cross.to_trader_portfolio();
      let margin = portfolio.compute_margin(metadata.all_perp_asset_metadata())?;

      println!(
          "maintenance margin={}",
          margin.margin.maintenance_margin.as_inner()
      );
  }
  ```
</CodeGroup>

Reference: [Rust live margin example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/compute_trader_margin.rs).

## 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())`.

<CodeGroup>
  ```ts TypeScript theme={null}
  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();
  ```

  ```rust Rust theme={null}
  use phoenix_rise::api::PhoenixMetadata;
  use phoenix_rise::math::{RiskTier, TraderPortfolio};

  fn estimate_liquidation_price_usd(
      portfolio: &TraderPortfolio,
      metadata: &PhoenixMetadata,
      symbol: &str,
      current_price: f64,
  ) -> Result<Option<f64>, String> {
      if current_price <= 0.0 {
          return Ok(None);
      }

      let symbol = symbol.to_ascii_uppercase();
      let Some(position) = portfolio.positions.get(&symbol) else {
          return Ok(None);
      };
      if position.is_neutral() {
          return Ok(None);
      }

      let calculator = *metadata
          .get_market_calculator(&symbol)
          .ok_or_else(|| format!("missing market calculator for {symbol}"))?;
      let mut provider = metadata.all_perp_asset_metadata().clone();

      let mut crosses_liquidation = |price: f64| -> Result<bool, String> {
          let mark_price = calculator
              .price_to_ticks(price)
              .map_err(|err| format!("{err:?}"))?;
          provider
              .get_mut(&symbol)
              .ok_or_else(|| format!("missing perp metadata for {symbol}"))?
              .set_mark_price(mark_price);

          let margin = portfolio
              .compute_margin(&provider)
              .map_err(|err| err.to_string())?;
          let tier = margin.risk_tier().map_err(|err| err.to_string())?;
          Ok(tier >= RiskTier::Liquidatable)
      };

      if crosses_liquidation(current_price)? {
          return Ok(Some(current_price));
      }

      if position.is_long() {
          let mut low = (current_price / 1_000_000.0).max(0.000001);
          let mut high = current_price;
          if !crosses_liquidation(low)? {
              return Ok(None);
          }

          for _ in 0..48 {
              let mid = (low + high) / 2.0;
              if crosses_liquidation(mid)? {
                  low = mid;
              } else {
                  high = mid;
              }
          }
          return Ok(Some(high));
      }

      let mut low = current_price;
      let mut high = current_price * 2.0;
      for _ in 0..20 {
          if crosses_liquidation(high)? {
              break;
          }
          low = high;
          high *= 2.0;
      }
      if !crosses_liquidation(high)? {
          return Ok(None);
      }

      for _ in 0..48 {
          let mid = (low + high) / 2.0;
          if crosses_liquidation(mid)? {
              high = mid;
          } else {
              low = mid;
          }
      }
      Ok(Some(high))
  }

  let cross = trader
      .primary_subaccount()
      .ok_or_else(|| "trader state is not ready".to_string())?;
  let portfolio = cross.to_trader_portfolio();
  let current_price = metadata
      .get_perp_asset_metadata("SOL")
      .and_then(|market| {
          metadata
              .get_market_calculator("SOL")
              .map(|calculator| calculator.ticks_to_price(market.mark_price))
      })
      .ok_or_else(|| "SOL market stats are not initialized".to_string())?;

  let estimated_liquidation_price_usd =
      estimate_liquidation_price_usd(&portfolio, &metadata, "SOL", current_price)?;

  println!("{estimated_liquidation_price_usd:?}");
  ```
</CodeGroup>

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.

<CodeGroup>
  ```ts TypeScript theme={null}
  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);
  ```

  ```rust Rust theme={null}
  use phoenix_rise::{PhoenixHawkeyeClient, TraderKey};

  let trader_key = TraderKey::new(authority);
  let hawkeye = PhoenixHawkeyeClient::new(
      "https://api.mainnet-beta.solana.com",
      &metadata,
  );

  let margin = hawkeye
      .view_margin_for_trader(trader_key.pda())
      .await?;
  let liquidation = hawkeye
      .view_liquidation_price_for_trader(trader_key.pda(), sol_asset_id)
      .await?;

  println!("margin={:?}", margin.value);
  println!(
      "liquidation price ticks={}",
      liquidation.value.liquidation_price_ticks
  );
  ```
</CodeGroup>

## Isolated Order Estimates

Server-built isolated order routes can also return the post-trade isolated liquidation estimate.

<CodeGroup>
  ```ts TypeScript theme={null}
  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);
  ```

  ```rust Rust theme={null}
  let (ixs, estimated_liq) = http
      .orders()
      .build_isolated_market_order_tx_enhanced(
          &authority,
          "SOL",
          Side::Bid,
          67,
          Some(100_000_000),
          true,
          None,
      )
      .await?;

  println!("estimated liquidation price: {:?}", estimated_liq);
  ```
</CodeGroup>
