Positions & PnL
How Flint tracks a perp position, what each number in the Positions tab means, and where to find your history.
A position
You hold at most one position per perp market. It's a signed size,
positive for long and negative for short, inside your trading account.
Every position shares the account's collateral (cross-margin), so
there's no per-position margin to add or remove. To give a position
more room, deposit collateral or shrink another position. An account
can hold a limited number of perp positions at once
(LinearPerpPositionLimitExceeded past that), and resting orders on a
market take one of those slots even while you hold no position there.
A market may also cap each account's net size (max_position_lots,
LinearPerpPositionSizeExceeded past it).
| Field | API (PerpPosition) | Meaning |
|---|---|---|
| Size | quantity | Signed base units. |
| Entry | entry_price | Average entry price of the open size. |
| Mark | mark_price | The price PnL and margin are measured at (below). |
| Value | notional_usd | |size × mark| in USD. |
| Unrealised PnL | unrealized_pnl_usd | What closing at the mark would realise, before fees. |
| Realised PnL | realized_pnl_usd | Booked by reductions and closes. |
| Funding | accrued_funding_usd | Funding accrued on this position. Positive is a credit to you, negative a debit. |
| Liq. price | liquidation_price | Mark at which the account would reach maintenance margin (below). |
Entry price
Entry is a running average. The program stores the position's size and the cash paid for it, and the entry price is the ratio.
- Adding on the same side blends the fill into the average.
- Reducing keeps the entry price and realises PnL on the closed part.
- Flipping (selling more than a long, or buying more than a short) closes the old position in full and opens the remainder at the fill price.
Mark price
The mark is the underlying asset's oracle price, put on the contract's tick grid. It is not the book mid or the last trade. Unrealised PnL, margin, and liquidation all use it. The mark expires quickly, and a market with a stale mark can't trade until someone refreshes it. See Prices, sizes, units.
PnL
unrealised PnL = size × (mark − entry)
This is in the contract's PnL currency, converted to USD at that asset's oracle price. A long gains when the mark rises; a short gains when it falls.
Your position's PnL has four parts, all of which end up in your cash balance once settled:
| Leg | When it's booked |
|---|---|
| Realised trading PnL | On every reduce, close, or flip. |
| Fees | On every fill (taker fee; makers earn a rebate). See Fees. |
| Funding | Lazily: on every fill in that market and whenever anyone runs settleFunding for the account. Margin checks always count funding accrued so far. |
| Unrealised PnL | Not booked; it's marked to market continuously. |
For margin purposes, positive unrealised PnL counts at a haircut, while losses count in full. So an open winning position adds less buying power than its PnL figure suggests.
Funding
Funding is quoted as an hourly rate and accrues between longs and
shorts. A positive rate means longs pay shorts. The Funding column shows the market's
current per-hour rate; FundingService.GetFundingRates returns it
from the API. How the rate is set is on Funding.
Margin and liquidation price
Each position needs margin based on its notional:
- Initial margin = notional × the market's initial margin rate. You need this to open or add.
- Maintenance margin = notional × the maintenance rate. Fall below this and the account can be liquidated.
Both are summed across the whole account and compared with its net
equity. The account snapshot's health carries net_equity,
initial_margin, maintenance_margin, free_collateral, and
health_ratio (net equity ÷ maintenance margin). The maximum leverage
on a market is 1 ÷ its initial margin rate. See
Collateral & margin.
Liquidation price is the mark at which the account's equity would fall to its maintenance margin if only this market moved. Every other position and balance is held fixed. Because margin is shared, another position's PnL or a withdrawal moves it. It's unset when the account is already below maintenance, the position isn't priced, or no positive price would trigger liquidation.
The app estimates it client-side across the account, shown with ≈, or None when no price would liquidate the position. The ticket's Liquidation price row previews it before you trade. What happens at liquidation is on Liquidations.
The Positions tab
Columns: Type, Market, Side, Size, Entry, Mark, Value, Liq. price, Unrealised PnL, Margin (initial margin for the position), Funding (per-hour rate), and a Close button. Lend and borrow legs appear in the same table with Redeem / Repay.
Adding and reducing
Trade in the same market from the order ticket. The main button says what will happen: Add to long, Reduce long, Close long, or Flip short. Reducing realises PnL on the closed part; the entry price of the rest doesn't change.
Closing
Close on a position row opens Close position:
- Choose 25%, 50%, 75%, or Max.
- Review Closing, Mark, Exit value, Realised PnL (est.), and Fee (est.).
- Press Close position (or Reduce {n}%).
It sends a reduce-only FOK order at your Max slippage from the current mark. The selected size is rounded down to whole lots and must fill in full or nothing changes. If it cannot fill, review the available liquidity, close size, and price bound before trying again. There's no "close all" button; close each position separately.
Settlement
Realised PnL, fees, and funding stay on the position until they're
settled into a cash balance in the contract's settlement asset.
The Settlements tab lists Unsettled PnL per market, with
Settle and Settle all buttons. Each sends settleRealisedPnl,
signed by your wallet.
- A net loss is paid from your idle balance of that asset, and borrowed if the balance runs short.
- A net gain is paid from the contract's PnL pool, up to what the
pool can pay in whole ticks. An oversized gain can settle partially; the
unpaid amount stays on the position.
PnlSettlePoolExhaustedmeans no payable ticks remain.SettlementBelowMinimummeans the payable amount is below the caller'sminSettleTicks.
settleRealisedPnl is permissionless. Anyone can settle any account,
and it reverts with NoRealisedPnlToSettle if there's nothing to do.
Settle before withdrawing profits.
History
In the app's account drawer:
| Tab | Shows |
|---|---|
| History | Fills plus deposits, withdrawals, lending, and borrowing in one table: time, market / asset, action, amount, price, value, realised PnL, fee. |
| Performance | Account value, Account PnL, Health, and Perp PnL charts over 24h / 7d / 30d. Perp PnL → Outstanding breaks unsettled PnL into trading, funding, and fees. |
From the API (all public, keyed by trader id):
| RPC | Returns |
|---|---|
AccountService.GetFills | Your fills, newest first: price, quantity, side, maker flag, fee and rebate in USD. Paginated (page_size up to 1000). |
AccountService.SubscribeFills | Live fills. On DATA_LOSS, reconcile with GetFills. |
AccountService.GetAccountHistory | Account value (nav_usd), PnL, debt, and health over time. With include_perp_pnl, also total, realised trading, fee, and funding PnL. |
AccountService.GetActivity | Deposits, withdrawals, lend, redeem, borrow, repay. |
FundingService.GetFundingRates | Current funding rate per market. |
use flint_margin_user_client::{Client, DEVNET_PUBLIC_ENDPOINT};
use flint_margin_user_client::proto::{GetAccountHistoryRequest, GetFillsRequest, TraderId};
let client = Client::connect(DEVNET_PUBLIC_ENDPOINT).await?;
let fills = client
.account()
.get_fills(GetFillsRequest {
trader_id: Some(TraderId { id: 42 }),
perp_ids: vec![],
start: None,
end: None,
page_size: 100,
page_token: String::new(),
})
.await?
.into_inner();
let history = client
.account()
.get_account_history(GetAccountHistoryRequest {
trader_id: Some(TraderId { id: 42 }),
start: None,
end: None,
resolution_seconds: 0,
include_perp_pnl: true,
})
.await?
.into_inner();import { createAccountClient, DEVNET_PUBLIC_ENDPOINT } from "@superis-labs/flint-margin-user-client";
const account = createAccountClient({ baseUrl: DEVNET_PUBLIC_ENDPOINT });
const { fills, nextPageToken } = await account.getFills({
traderId: { id: 42n },
pageSize: 100,
});
const { points, perpPnl } = await account.getAccountHistory({
traderId: { id: 42n },
includePerpPnl: true,
});All money fields are decimal strings. Parse them with rust_decimal
or bignumber.js, never floats. Unset start / end default to the
last 24 hours for GetAccountHistory.
Leaderboard
The app's Leaderboard page ranks accounts by PnL, PnL %, Volume, Account value, Open interest, Closest to liquidation, Largest liquidation, or Sharpe ratio. It covers 24h, 7d, 30d, or All time, with an optional minimum account value and the top 20, 50, or 100.
The same data is public through AccountService.GetRankedAccounts:
| Metric | Definition |
|---|---|
PNL | Change in account value minus deposits and withdrawals. |
PNL_PERCENT | Linked hourly Modified Dietz returns, in percent. |
VOLUME | Perp notional traded, as taker or maker. |
SHARPE_RATIO | Annualised hourly Sharpe, zero risk-free rate; needs 24 hourly returns. |
LARGEST_LIQUIDATION | Largest single liquidation in the window. |
ACCOUNT_VALUE, OPEN_INTEREST, CLOSEST_TO_LIQUIDATION | Current state; the timeframe is ignored. |
Windows are rolling 24 hours, 7 days, or 30 days of completed hours.
Every metric sorts descending except CLOSEST_TO_LIQUIDATION. Results
are capped at 100 and aren't paginated. history_complete is always
false while the archive may have gaps.