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);
  }
});
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);
}

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);
}
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()
    );
}
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();
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:?}");
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);
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
);

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);
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);