PnL & Risk
How it works
Velocity summarizes an account's risk as health, an integer from 0 to 100 derived from total collateral against the maintenance margin requirement. 100 means no maintenance requirement is being used; 0 means the account is at or past the liquidation threshold. It short-circuits to 0 as soon as the account is flagged as being liquidated, so a health of 0 is a state, not just a number.
PnL comes in two forms. Unrealized PnL is the mark-to-market value of open positions, computed against the current oracle price rather than the last trade. Realized PnL is what settles when a position closes. Perps add a third component, funding PnL, from the periodic payments between longs and shorts.
Free collateral is the collateral not currently backing a position: what is available to withdraw, or to open something new against. Margin requirements grow with position size and vary by market, and leverage is notional position value over total collateral. All of these read off the subscription cache, so they move as prices and positions do without a refetch.
The $100 initial-margin uPnL cap. When total or free collateral is computed under 'Initial' margin, which is the path for opening new positions rather than for liquidation checks, each position's weighted, positive unrealized PnL is capped at $100 before it can count toward buying power. The constant is MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN, 100000000 in QUOTE_PRECISION (1e6). It exists so one misconfigured or manipulated market cannot inflate an account's initial margin capacity.
The cap is per position and applies only to gains: losses are never capped. It takes effect only on the asset-weighted PnL, meaning getUnrealizedPNL with a withWeightMarginCategory of 'Initial'. It has no effect on 'Maintenance'-margin health or liquidation math, and it does not cap raw unweighted PnL.
A second, per-market haircut on positive unrealized PnL exists in the program, gated on PerpMarket.unrealizedPnlMaxImbalance. While the field is above 0 it scales the unrealized-PnL asset weight down once the market's net unsettled user PnL exceeds it, under Initial and Fill margin only; while the field is 0 the discount branch does not run at all. The field is per market and admin-settable, so read it off the market account rather than modelling the discount into an integration's own margin math.
SDK Usage
These helpers back risk checks, dashboards, and liquidation logic. The imports in the first example carry through the rest of the page.
User health
Health is an integer from 0 to 100. Lower means closer to liquidation. Pass a perp market index to scope it to one isolated position instead of the cross-margin account.
const user = velocityClient.getUser();
const health = user.getHealth(); // integer, 0 to 100
console.log(health); // 85 means 15% of the maintenance requirement is usedThree cases never reach the ratio at all, and they change what a client should display:
- 100 when the maintenance requirement is zero and total collateral is not negative. Collateral of exactly zero with no requirement reports 100, not 0.
- 0 when total collateral is negative. Collateral of exactly zero also reports 0, but only when there is a non-zero maintenance requirement; with no requirement it takes the case above.
- 0 for an account or isolated position already flagged as being liquidated, short-circuited before the formula runs. A health of 0 is therefore a state, not only a low number.
getHealth(perpMarketIndex) tests its argument for truthiness rather than for null, so perp market index 0 takes the cross-margin liquidation path. Collateral and the requirement still come from market 0's isolated calculation, but the short-circuit reads the cross-margin flag: a cross-liquidated account reports 0 for a healthy isolated market-0 position, and an isolated market-0 position flagged as being liquidated does not short-circuit to 0. A client that displays isolated health for market 0 should check isIsolatedPositionBeingLiquidated(0) separately.
Collateral, margin requirement, leverage
Get total account collateral value in quote units (typically USD precision).
import { QUOTE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";
// getTotalCollateral returns BN in QUOTE_PRECISION (1e6)
// marginCategory defaults to 'Initial'; pass 'Maintenance' for liquidation checks
const total = velocityClient.getUser().getTotalCollateral();
console.log(convertToNumber(total, QUOTE_PRECISION)); // e.g. 1500.50 (USD)Get the required margin for the account under initial or maintenance rules.
// getMarginRequirement(marginCategory, liquidationBuffer?, strict?, includeOpenOrders?, perpMarketIndex?)
// marginCategory: 'Initial' (for new positions) or 'Maintenance' (for liquidation)
const req = velocityClient.getUser().getMarginRequirement('Initial');
console.log(convertToNumber(req, QUOTE_PRECISION)); // USDA perp market whose status is Settlement contributes a margin ratio of 0 to these calculations, and its positions are valued at market.expiryPrice instead of the oracle price. The margin calculation applies that zero itself, after calling calculateMarketMarginRatio; the helper does not apply it. Do not use calculateMarketMarginRatio on its own as a settlement-aware ratio, because on a settling market it returns the market's normal ratio.
Get currently available collateral that can be used for new positions or withdrawals.
const free = velocityClient.getUser().getFreeCollateral();
console.log(convertToNumber(free, QUOTE_PRECISION)); // USD availableGet current account leverage as a scaled value (convert to human-readable x leverage).
import { TEN_THOUSAND } from "@velocity-exchange/sdk";
// getLeverage(includeOpenOrders?, perpMarketIndex?) returns a BN scaled by
// TEN_THOUSAND (1e4), so 20000 is 2x. ZERO when net asset value is zero.
const lev = velocityClient.getUser().getLeverage();
console.log(lev.toNumber() / TEN_THOUSAND.toNumber()); // 2.5 means 2.5x leverageUnrealized PnL
Get unrealized PnL across open positions (optionally including funding effects).
// getUnrealizedPNL(withFunding?, marketIndex?, withWeightMarginCategory?, strict?, liquidationBuffer?)
// Returns BN in QUOTE_PRECISION. Positive = profit, negative = loss.
// withWeightMarginCategory ('Initial' | 'Maintenance') applies asset weighting;
// under 'Initial' it also applies the $100-per-position cap described above.
const pnl = velocityClient.getUser().getUnrealizedPNL(true); // withFunding=true, raw (uncapped) PnL
console.log(convertToNumber(pnl, QUOTE_PRECISION)); // e.g. -25.50 (USD)Get unrealized funding PnL only, separated from price-movement PnL.
// Funding PnL only (accumulated funding payments)
const fundingPnl = velocityClient.getUser().getUnrealizedFundingPNL();
console.log(convertToNumber(fundingPnl, QUOTE_PRECISION)); // USDEntry price helper
Compute the effective entry price for a perp position from its cumulative trade data.
import { PRICE_PRECISION, calculateEntryPrice, convertToNumber } from "@velocity-exchange/sdk";
const position = velocityClient.getUser().getPerpPosition(0);
if (position) {
const entryPrice = calculateEntryPrice(position); // BN in PRICE_PRECISION
console.log(convertToNumber(entryPrice, PRICE_PRECISION)); // e.g. 150.25
}Settle perp PnL
Realize and settle a user's perp PnL for a specific market into spot balances.
const user = velocityClient.getUser();
await velocityClient.settlePNL(user.userAccountPublicKey, user.getUserAccount(), 0);Whether the program accepts the call depends on the position and the market's status:
- The market's
SettlePnloperation must be unpaused in every case. - With a base position still open, the market's
SettlePnlWithPositionoperation must also be unpaused and the market's status must beActive. Either one failing givesInvalidMarketStatusToSettlePnl. - With no base position left,
ReduceOnlyis accepted alongsideActive, andSettlePnlWithPositionis not consulted. So an account can still settle out of a market that has gone reduce-only, but only once it is flat.
Settling someone else's account adds one rule. When the signer is neither the account's authority nor its delegate, and the position's unrealized PnL is negative, the call is rejected while the market's oracle validity is StaleForMargin or InsufficientDataPoints. Both of those are otherwise accepted for settle-PnL, so a keeper settling other users' losses can fail on a market where the same call from the owner succeeds.
On an expired market, positions settle at the market's fixed expiryPrice rather than the live oracle price.