Portfolio
Read account-level performance — overview metrics, interval buckets, risk ratios, and a daily series.
Portfolio answers "how is this account doing", not "how did this trade do". Every other read in Trade Activity is scoped to one object: a fill, an execution, a position. These two endpoints aggregate all of them.
| Endpoint | SDK | Answers |
|---|---|---|
GET /portfolio | sdk.core.portfolio.overview | The scoreboard. Volume, open interest, realized and unrealized PnL, fees, win and loss counts, plus prebuilt chart buckets. |
GET /portfolio/analytics | sdk.core.portfolio.analytics | The breakdown. Risk ratios, per-asset performance, a per-basket ranking, and a daily series. |
Use Closed Position when you want one trade. Use these when you want the book.
Both endpoints need two headers:
| Header | Value |
|---|---|
x-api-key | Your API key. A read key is enough — both endpoints are GET. A session or bearer token also works. |
x-trade-account-id | The trade account to report on. Required. |
Neither endpoint pages. There is no limit, no cursor, and no nextCursor. Each returns the whole aggregate in one response. Neither accepts connector either; the venue comes from the trade account.
Every number on this page is a JSON number, not a decimal string. Positions, fills, and closed trades return decimals as strings such as
"+41.72". Portfolio returns41.72. If you feed both into the same parser, portfolio is the one that breaks it. A JSON number is a double, so a very large volume loses precision. Treat these figures as reporting values, not as accounting inputs.
GET /portfolio
curl "https://pro-gateway.pearprotocol.io/portfolio?startDate=2026-01-01&endDate=2026-01-15" \
-H "x-api-key: $PEAR_API_KEY" \
-H "x-trade-account-id: $TRADE_ACCOUNT_ID"{
"intervals": null,
"overall": {
"totalWinningTradesCount": 38,
"totalLosingTradesCount": 21,
"totalWinningUsd": 4210.77,
"totalLosingUsd": 1830.42,
"currentOpenInterest": 8420.15,
"currentTotalVolume": 1284500.5,
"unrealizedPnl": -112.38,
"realizedPnl": 2380.35,
"totalFees": 318.94,
"totalTrades": 59
}
}Query parameters
| Parameter | Values |
|---|---|
startDate | Lower bound on the window. Absent means unbounded. |
endDate | Upper bound. Absent means now. |
source | all (default) or pear. pear counts only fills Pear placed, so volume excludes trades you made directly on the exchange. It changes volume only — PnL, fees, and trade counts are unaffected. |
Both dates accept an ISO 8601 string or an epoch-millisecond number. An unparseable date is not a 400. It is silently dropped, and the request behaves as if you never sent it.
overall
| Field | Meaning |
|---|---|
totalTrades | Closed trades in the window. |
totalWinningTradesCount | Trades with positive net PnL. |
totalLosingTradesCount | Trades with negative net PnL. |
totalWinningUsd | Summed net PnL of the winners. |
totalLosingUsd | Summed net PnL of the losers, as a positive number. It is an absolute value, not a signed loss. |
realizedPnl | Net realized PnL over the window, after fees. |
totalFees | Fees on those closed trades. |
currentTotalVolume | Fill notional in the window. Honours source. |
currentOpenInterest | Mark-price notional of everything open right now. |
unrealizedPnl | Unrealized PnL of everything open right now. |
Two things to read carefully:
currentOpenInterestandunrealizedPnlignore the date window. They are a live snapshot taken when the request arrives. The other eight fields are window-scoped. A query for last March still returns today's open book.totalWinningTradesCount + totalLosingTradesCountcan be less thantotalTrades. A trade that closed at exactly zero net PnL is neither a win nor a loss, but it is still a trade.
intervals
intervals holds prebuilt chart buckets so a dashboard can draw five sparklines from one request.
intervalsisnullwheneverstartDateorendDateresolves to a real date. This is not "null when there is nothing to bucket". A custom window returnsoverallfor that exact range and no buckets at all, even on a busy account. Only a bareGET /portfolio— or one whose dates were unparseable and therefore dropped — returns buckets.
When present, intervals carries five arrays of identical buckets:
| Key | Buckets |
|---|---|
oneDay | 24 hourly buckets, ending with the current UTC hour. |
oneWeek | 7 daily buckets, ending today (UTC). |
oneMonth | 30 daily buckets, ending today (UTC). |
oneYear | 12 monthly buckets, ending with the current month. |
all | 10 equal buckets spanning your first confirmed fill to today. The last one can end in the future. |
Every bucket has the same shape:
{
"periodStart": "2026-01-15T09:00:00.000Z",
"periodEnd": "2026-01-15T10:00:00.000Z",
"volume": 12500,
"openInterest": 8420.15,
"winningTradesCount": 1,
"winningTradesUsd": 42.5,
"losingTradesCount": 0,
"losingTradesUsd": 0
}periodStart is inclusive and periodEnd is exclusive. volume is the fill notional in the bucket, and the trade counts and sums follow the same rules as overall.
openInterestis the same value in every bucket. It is today's open interest repeated, not the open interest as it stood in that period. Do not plot it as a time series.
GET /portfolio/analytics
curl "https://pro-gateway.pearprotocol.io/portfolio/analytics?interval=1m&rollingWindowDays=14" \
-H "x-api-key: $PEAR_API_KEY" \
-H "x-trade-account-id: $TRADE_ACCOUNT_ID"{
"config": {
"rollingWindowDays": 14,
"startDate": "2025-12-17T00:00:00.000Z",
"endDate": "2026-01-15T11:04:22.881Z",
"fundingSynced": true
},
"portfolio": {
"realizedOnly": {
"pnlAbsolute": 2380.35,
"pnlPercent": 0.0412,
"hitRate": 0.6441,
"profitFactor": 2.3005,
"avgWinSize": 110.81,
"avgLossSize": -87.16,
"avgReturnPerWin": 0.0231,
"avgReturnPerLoss": -0.0164,
"maxDrawdown": -0.1842,
"sharpeRatio": 1.62,
"sortinoRatio": 2.08
},
"realizedPlusUnrealized": {
"pnlAbsolute": 2267.97,
"pnlPercent": 0.0374,
"hitRate": 0.6441,
"profitFactor": 2.3005,
"avgWinSize": 110.81,
"avgLossSize": -87.16,
"avgReturnPerWin": 0.0231,
"avgReturnPerLoss": -0.0164,
"maxDrawdown": null,
"sharpeRatio": null,
"sortinoRatio": null
},
"realizedHitRate": 0.6441,
"profitFactor": 2.3005,
"avgWinSize": 110.81,
"avgLossSize": -87.16,
"breakevenHitRate": 0.4403,
"cushion": 0.2038
},
"assets": [
{
"asset": "0",
"realizedOnly": {
"pnlAbsolute": 1620.4,
"pnlPercent": 0.0521,
"hitRate": 0.6552,
"profitFactor": 2.51,
"avgWinSize": 96.4,
"avgLossSize": -71.2,
"avgReturnPerWin": 0.0248,
"avgReturnPerLoss": -0.0177,
"maxDrawdown": null,
"sharpeRatio": null,
"sortinoRatio": null
},
"realizedPlusUnrealized": {
"pnlAbsolute": 1508.02,
"pnlPercent": 0.0446,
"hitRate": 0.6552,
"profitFactor": 2.51,
"avgWinSize": 96.4,
"avgLossSize": -71.2,
"avgReturnPerWin": 0.0248,
"avgReturnPerLoss": -0.0177,
"maxDrawdown": null,
"sharpeRatio": null,
"sortinoRatio": null
},
"unrealizedReturnPerDay": -9.36
}
],
"pairs": [
{
"key": "L:0|S:1",
"longAssets": ["0"],
"shortAssets": ["1"],
"trades": 24,
"wins": 16,
"losses": 8,
"hitRate": 0.6667,
"avgWinSize": 128.4,
"avgLossSize": -94.2,
"expectancy": 54.2,
"totalPnl": 1300.8,
"bookPercent": 0.5465
}
],
"series": [
{
"date": "2026-01-14",
"trades": 3,
"wins": 2,
"losses": 1,
"dayPnl": 84.2,
"dayVolume": 42500,
"dayFunding": -3.12,
"dayNetPnl": 81.08,
"hitRate": 0.6667,
"winLossRatio": 2.14,
"rollingHitRate": 0.6129,
"rollingWinLossRatio": 1.87,
"dailyReturn": 0.0084,
"cumulativeReturn": 0.0398,
"equity": 1.0398,
"drawdown": 0,
"rollingSharpe": 1.44,
"rollingSortino": 1.91,
"rollingVolatility": 0.0112
},
{
"date": "2026-01-15",
"trades": 0,
"wins": 0,
"losses": 0,
"dayPnl": 0,
"dayVolume": 0,
"dayFunding": -1.04,
"dayNetPnl": -1.04,
"hitRate": null,
"winLossRatio": null,
"rollingHitRate": 0.6,
"rollingWinLossRatio": 1.82,
"dailyReturn": 0,
"cumulativeReturn": 0.0398,
"equity": 1.0398,
"drawdown": 0,
"rollingSharpe": 1.39,
"rollingSortino": 1.86,
"rollingVolatility": 0.0109
}
]
}The real response is longer. assets holds one entry per instrument you traded or still hold, pairs one per distinct basket, and series one per UTC day in the window — 30 points for the request above. Only the first entry of each is shown here.
asset, longAssets, and shortAssets hold instrument IDs, so "0" is BTC and "1" is ETH on Hyperliquid. See Instrument ID.
Query parameters
| Parameter | Values |
|---|---|
startDate, endDate | As on GET /portfolio. endDate defaults to now. |
interval | 1d, 1w, 1m, 1y, or all. Sets the start of the window when you do not send startDate. |
rollingWindowDays | Integer. Window for every rolling statistic. Default 7. |
includeSeries | true or false. Default true. |
source | all (default) or pear. Affects series[].dayVolume only. Funding is an account-level cash flow and ignores it. |
interval never appears in the response. It only picks a start date, and startDate overrides it:
interval | Window starts at |
|---|---|
1d | The start of the end day, UTC. |
1w | 6 days before the end day. |
1m | 29 days before the end day. |
1y | 365 days before the end day. |
all | Your first confirmed fill, or 2020-01-01T00:00:00.000Z if you have none. |
| absent | 365 days before the end day. Same as 1y. |
Two behaviours worth planning for:
rollingWindowDaysis clamped to 7–365, not rejected.rollingWindowDays=1becomes7, androllingWindowDays=9999becomes365. Readconfig.rollingWindowDaysback to learn what was applied. A non-integer such as7.5is a400.- The series is on by default. A bare
GET /portfolio/analyticsreturns a year of daily points. SendincludeSeries=falsewhen you only want the ratios; it also skips the volume and funding reads, so it is measurably faster.
config
config reports the window the server actually used, after defaults and clamping.
| Field | Meaning |
|---|---|
rollingWindowDays | The clamped value applied to every rolling statistic. |
startDate, endDate | The resolved window, as ISO 8601 strings. |
fundingSynced | Whether funding history has ever reached Pear for this account. |
fundingSynced: falsemeans everydayFundingis unknown, not zero. Pear has no funding record for this account, so eachdayFundingreads0and eachdayNetPnlequals itsdayPnl— by default, not by measurement. A chart that plots that as "no funding paid" is wrong. Check the flag before you draw funding. Funding reaches Pear through the same exchange sync that settles fills, so an account that has never been synced has no funding to report — see Fills. The flag says only that funding once arrived. It makes no claim about how recent it is.
portfolio
portfolio carries the same metrics under two lenses.
| Variant | Covers |
|---|---|
realizedOnly | Closed trades in the window. This is booked performance. |
realizedPlusUnrealized | Closed trades plus the mark-to-market of everything still open. This is what the account is worth today. |
Both variants have the same eleven fields:
| Field | Meaning |
|---|---|
pnlAbsolute | PnL in USD under this lens. Never null. |
pnlPercent | pnlAbsolute over the capital deployed, as a fraction. null when no capital was deployed. |
hitRate | Winners over winners plus losers. A breakeven trade is in neither term. |
profitFactor | Gross profit over gross loss. null when either side is zero. |
avgWinSize | Mean USD PnL of the winners. Positive. |
avgLossSize | Mean USD PnL of the losers. Negative, not an absolute value. |
avgReturnPerWin | Mean return of the winners, as a fraction of entry notional. |
avgReturnPerLoss | Mean return of the losers. Negative. |
maxDrawdown | Deepest peak-to-trough fall of the daily equity curve, as a negative fraction. 0 when it never fell. |
sharpeRatio | Mean daily return over its sample standard deviation, annualised by √365. The risk-free rate is zero. |
sortinoRatio | The same, measured against downside deviation only. |
maxDrawdown,sharpeRatio, andsortinoRatioare populated inportfolio.realizedOnlyand nowhere else. They need a daily return history, and only closed trades have one. Inportfolio.realizedPlusUnrealizedand in every entry ofassets, all three are alwaysnull. That is the design, not missing data.
A ratio is null, never 0, when it is undefined. A null sortinoRatio means there was no losing day, or fewer than two days of data — not that downside risk was zero.
portfolio also carries four fields outside the two variants:
| Field | Meaning |
|---|---|
realizedHitRate | The realized hit rate. Same value as realizedOnly.hitRate. |
profitFactor, avgWinSize, avgLossSize | Repeats of the realizedOnly figures, lifted to the top level for convenience. |
breakevenHitRate | The hit rate you need to break even at your current average win and loss: the absolute avgLossSize divided by avgWinSize plus the absolute avgLossSize. null unless both averages exist. |
cushion | realizedHitRate - breakevenHitRate. Positive means you win often enough for the sizes you win and lose. Negative means your win rate is not carrying your loss sizes. |
In the example, a cushion of 0.2038 says the account wins 20 percentage points more often than its win-to-loss size ratio requires.
assets
One entry per instrument that appears in a closed trade or in an open position, sorted by instrument ID.
| Field | Meaning |
|---|---|
asset | The instrument ID. |
realizedOnly | The metric variant above, restricted to this instrument's share of each closed trade. |
realizedPlusUnrealized | The same, plus this instrument's current unrealized PnL and mark notional. |
unrealizedReturnPerDay | Current unrealized PnL divided by the position-days this instrument has been open. null when nothing is open in it. |
Per-asset figures split a basket's PnL back onto its legs. The pairs breakdown below keeps each basket whole. Read Synthetic Position for why the two views differ.
pairs
One entry per distinct basket shape, sorted by absolute totalPnl, largest first. It answers "which pair actually makes me money".
| Field | Meaning |
|---|---|
key | The basket identity: the long instrument IDs after L:, the short ones after S:, each list comma-joined and the two halves separated by a pipe. The example above is L:0|S:1. Treat it as opaque. |
longAssets, shortAssets | The legs of the basket. |
trades | Closed trades of this basket in the window. |
wins, losses | How many made or lost money. wins + losses can be below trades, because a breakeven close is in neither. |
hitRate | wins / trades. This divides by every trade, including breakeven ones, unlike hitRate inside a metric variant. |
avgWinSize | Mean USD PnL of this basket's winners. Positive. |
avgLossSize | Mean USD PnL of its losers. Negative. |
expectancy | totalPnl / trades. The mean USD result of trading this basket once. |
totalPnl | Net realized PnL of this basket over the window. |
bookPercent | totalPnl as a share of the account's total realized PnL. null when that total is zero. |
bookPercentdoes not behave like a percentage of a whole. The denominator is net realized PnL, so a losing basket gets a negative share and a single winning basket can exceed1when other baskets lost money. The shares do not sum to1in any useful way. Use it to rank contribution, not to fill a pie chart.
series
One point per UTC calendar day from config.startDate to config.endDate, inclusive. Days with no activity are still emitted, so the array length is fixed by the window, not by how much you traded. A year window returns 366 points.
| Field | Meaning |
|---|---|
date | The UTC day, as YYYY-MM-DD. |
trades | Trades closed that day. Equals wins + losses, so a breakeven close is counted nowhere. |
wins, losses | How many of them made or lost money. |
dayPnl | Net realized PnL of the trades closed that day, after fees. |
dayVolume | Fill notional traded that day. Honours source. |
dayFunding | Funding accrued that day, signed. Negative means you paid. Read config.fundingSynced first. |
dayNetPnl | dayPnl + dayFunding. The figure that reconciles with the venue's own statement. |
hitRate | Wins over wins plus losses, that day. null on a day with no closes. |
winLossRatio | Average win over the absolute average loss, that day. null without both. |
rollingHitRate, rollingWinLossRatio | The same two over the trailing rollingWindowDays. |
dailyReturn | dayPnl over the entry notional of the trades closed that day. 0 on a day with no closes. |
equity | Daily returns compounded, starting at 1. |
cumulativeReturn | equity - 1. |
drawdown | Fall from the running peak of equity, as a negative fraction. 0 at a new high. |
rollingSharpe, rollingSortino, rollingVolatility | Trailing-window statistics of dailyReturn. rollingVolatility is the sample standard deviation, not annualised. |
Every rolling field is null for the first rollingWindowDays - 1 points, because the window has not filled yet. Chart them from the first non-null point rather than from the start of the series.
Trades are booked on the day they close, and funding on the day it accrues. A trade held for a week contributes to one dayPnl and to seven dayFunding values.
The equity curve compounds trade returns, not your wallet balance. Deposits and withdrawals do not move it. That is deliberate: it keeps the risk metrics comparable across accounts of different sizes.
What gets rejected
| Status | Cause |
|---|---|
400 | x-trade-account-id missing, or not a UUID. source not all or pear. interval not one of the five values. includeSeries not true or false. rollingWindowDays not an integer. |
401 | Missing or invalid credentials. |
404 | A well-formed x-trade-account-id that is not yours. Pear does not distinguish "not found" from "not yours". |
Three inputs are accepted and quietly ignored rather than rejected: an unparseable startDate or endDate, an interval sent alongside a startDate, and any parameter the endpoint does not define. A rollingWindowDays outside 7–365 is clamped, not refused. config is the only reliable record of what the server used, so read it back instead of assuming your parameters applied.
See Error Handling for the two error body shapes.