Skip to main content

Overview

The Profile Analysis endpoint aggregates user profile + portfolio data into a single payload for bots, dashboards, and LLM workflows. This page documents the exact calculation logic used by the API so users can verify every metric.
This endpoint is designed for MCP (Model Context Protocol) tools and AI agents. Values are rounded for display in the API response, but formulas below describe the source calculations.

Endpoint

GET
/v1/account/profile-analysis
Full profile analysis with all metrics

Query Parameters

recent_trades
integer
default:"20"
Number of recent trades to include (1-100)
equity_days
integer
default:"90"
Days of equity history to analyze (1-365)
wallet_id
integer | 'all' | 'api'
default:"api"
Wallet scope for the whole analysis — positions, trading stats, risk metrics, snapshots, recent trades, cash basis and PnL baseline alike. An integer scopes to a single wallet you own (404 WALLET_NOT_FOUND otherwise); api scopes to your API wallet (including legacy rows recorded before per-wallet attribution); all restores the cross-wallet blended view. Keywords are case-insensitive; any other value returns 422 VALIDATION_FAILED. Omitted = api.
Default changed on 2026-06-10. Before 2026-06-10 this endpoint always blended API-wallet cash with all wallets’ position values, so api_pnl could report large gains on a flat API wallet. The analysis is now scoped to the API wallet by default and api_pnl reconciles with Balance. Pass wallet_id=all if you depended on the old blended view.

Authentication

API key only — X-API-Key: <key> (or the PM-compat POLY_API_KEY / Authorization: Bearer ps_live_... aliases). A Supabase Bearer JWT is not accepted here.
curl -H "X-API-Key: $POLYSIM_API_KEY" \
  https://api.polysimulator.com/v1/account/profile-analysis

Errors

All errors return {"error": "<CODE>", "message": "<human-readable>"}.
Statuserror codeWhen
401MISSING_API_KEYNo API key header supplied
401INVALID_KEYAPI key is unknown, deactivated, or expired
401USER_NOT_FOUNDAuthenticated user no longer exists in the database — re-authenticate
404ACCOUNT_NOT_FOUNDAuthenticated user has no account record
404WALLET_NOT_FOUNDInteger wallet_id does not exist or is not owned by the caller
422VALIDATION_FAILEDwallet_id is neither an integer, all, nor api
500ANALYSIS_ERRORUnexpected server error building the analysis

Response

The top level also carries analysis_generated_at (ISO-8601 UTC string), analysis_version (string, currently "1.0.0"), and a natural_language_summary string, alongside these sections:

profile — User Metadata

FieldTypeDescription
user_idintegerInternal user ID
usernamestring | nullPublic username
display_namestring | nullDisplay name
biostring | nullUser bio
avatar_urlstring | nullAvatar image URL
auth_providerstring | nullAuth provider used to sign up (e.g. email, google)
account_created_atstring | nullISO-8601 UTC registration timestamp
account_age_daysinteger | nullDays since registration
api_key_tierstringAPI key tier ceiling: free, pro, or enterprise (a Pro+ plan maps to the enterprise key tier). Defaults to free.
profile_visibilitystringProfile visibility (e.g. public, private). Defaults to public.

balance — Balance Summary

FieldTypeDescription
currencystringAlways USD
ui_balancestringUI (MAIN) trading balance — always the MAIN wallet, regardless of wallet_id
api_balancestringCash of the scoped wallet (the API wallet by default; the field name is historical)
starting_ui_balancestringUI baseline = seed + top-ups + grants
starting_api_balancestringPnL baseline of the scoped wallet — its actual starting_balance (tier-aware: Pro 10,000/Pro+10,000 / Pro+ 25,000)
ui_pnlstringUI profit/loss (ui_balance − starting_ui_balance)
api_pnlstringScoped-wallet profit/loss (total_portfolio_value − starting_api_balance)
ui_pnl_percentagestringUI P&L as percentage
api_pnl_percentagestringScoped-wallet P&L as percentage
total_portfolio_valuestringScoped-wallet cash + scoped-wallet positions
unrealized_pnlstringOpen-position mark-to-market P&L (scoped)
realized_pnlstringRealized P&L from closed positions (scoped)

trading_stats — Trading Statistics

FieldTypeDescription
total_tradesintegerTotal filled trades
win_ratestringWin rate percentage
profit_factorstringGross profit / gross loss
total_volumestringTotal traded volume
avg_trade_sizestringAverage trade notional
best_trade_pnlstringBest single trade P&L
worst_trade_pnlstringWorst single trade P&L
unique_markets_tradedintegerDistinct markets
active_trading_daysintegerDays with at least one trade

risk_metrics — Risk Analysis

FieldTypeDescription
portfolio_diversity_scorestring1 - HHI (0=concentrated, 1=diverse)
largest_position_weightstringLargest position as % of portfolio
top_3_concentrationstringTop 3 positions as % of portfolio
max_drawdown_7dstringMax running-peak drawdown over 7 days
max_drawdown_30dstringMax running-peak drawdown over 30 days
cash_percentagestringCash as % of total

How Metrics Are Calculated

Balance and PnL

  • Total portfolio value = api_balance + total_position_value (both scoped to the selected wallet — the API wallet by default)
  • UI PnL = ui_balance - starting_ui_balance (UI baseline = seed + topups + grants)
  • API PnL = total_portfolio_value - starting_api_balance
  • UI PnL % = ui_pnl / starting_ui_balance * 100
  • API PnL % = api_pnl / starting_api_balance * 100
starting_api_balance is the scoped wallet’s actual starting_balance (tier-aware: Pro 10,000/Pro+10,000 / Pro+ 25,000 for the API wallet; a specific wallet’s own baseline when wallet_id=<id>), so api_pnl here agrees with GET /v1/account/balance and GET /v1/account/portfolio for the same wallet. Before 2026-06-10 this endpoint used a fixed $10,000 baseline regardless of tier — that caveat no longer applies.

Open Position Valuation

For each open position:
  • Cost basis = avg_entry_price * quantity
  • Market value = current_price * quantity (if live price exists)
  • If no live price is available, cost basis is used as fallback valuation.
  • Unrealized PnL (position) = market_value - cost_basis
  • Unrealized PnL (account) = sum(open_position_market_values) - sum(open_position_cost_basis)

Realized PnL (Closed Positions)

Closed positions in storage have quantity = 0, so realized PnL is reconstructed from filled orders. For each (market_id, outcome) closed position:
  • buy_notional = sum(BUY order notionals)
  • sell_notional = sum(SELL order notionals)
  • buy_qty = sum(BUY order quantities)
  • sell_qty = sum(SELL order quantities)
  • remaining_qty = buy_qty - sell_qty
  • settlement_value = exit_price * remaining_qty
  • position_realized_pnl = sell_notional + settlement_value - buy_notional
Then:
  • realized_pnl = sum(position_realized_pnl over closed positions)
This handles partial closes and settlement-style closes consistently.

Win Rate and Win/Loss Counts

Win/loss is based on realized PnL sign, not on price comparison alone:
  • Win: position_realized_pnl > 0
  • Loss: position_realized_pnl < 0
  • Break-even: position_realized_pnl == 0 (excluded from win/loss counts)
win_rate uses closed position count as denominator:
  • win_rate = wins / total_closed_positions * 100

Profit Factor and Trade PnL Stats

  • Gross profit = sum(all positive position_realized_pnl)
  • Gross loss = sum(abs(all negative position_realized_pnl))
  • profit_factor = gross_profit / gross_loss (when gross_loss > 0)
  • best_trade_pnl = max positive closed-position PnL
  • worst_trade_pnl = min negative closed-position PnL
  • avg_win_pnl = average of positive closed-position PnLs
  • avg_loss_pnl = average of negative closed-position PnLs

Drawdown (7d and 30d)

Drawdown is computed from portfolio_snapshots.total_value using a running peak:
  1. Walk snapshots in chronological order
  2. Track highest value seen so far (peak)
  3. Compute drawdown each point: (value - peak) / peak
  4. Maximum drawdown = most negative drawdown in the period
This avoids overstating drawdown by comparing early values to a later peak.

Portfolio Diversity and Concentration

For each open position:
  • Effective value = market_value (live price × quantity) when a live price exists, or cost_basis (entry price × quantity) as fallback when no live price is available.
  • Weight per position: w_i = effective_value / total_invested
  • HHI = sum(w_i^2)
  • portfolio_diversity_score = 1 - HHI
  • largest_position_weight = largest w_i
  • top_3_concentration = sum of top 3 w_i
Weights always sum to approximately 100% across all open positions.

Equity Returns

Using snapshots in the requested equity_days window:
  • return_7d: first-to-last return among snapshots in last 7 days
  • return_30d: first-to-last return among snapshots in last 30 days
  • return_all_time: first-to-last return across the full equity_days window
Each is:
  • period_return = (end_value - start_value) / start_value * 100
If there are fewer than two snapshots in a period, return is null.

Data Scope and Semantics

  • Wallet scope: every section — open_positions, trading_stats, risk_metrics, equity_timeline, category_exposure, recent_trades and the balance cash/baseline — covers the wallet selected by wallet_id (the API wallet when omitted; positions/orders recorded before per-wallet attribution count toward the API wallet). wallet_id=all spans every wallet you own. Only ui_balance / ui_pnl are always MAIN-wallet metrics regardless of scope.
  • open_positions contains only status=OPEN with quantity > 0
  • Trading stats and realized metrics use only filled orders
  • Category exposure is based on open-position values
  • Cash/invested percentages are relative to current total portfolio value

equity_timeline — Performance Timeline

FieldTypeDescription
return_7dstring | null7-day return
return_30dstring | null30-day return
return_all_timestring | nullAll-time return
peak_value / peak_datestring | nullPortfolio high watermark
trough_value / trough_datestring | nullPortfolio low point
current_valuestring | nullLatest snapshot’s total value
snapshots_availableintegerNumber of snapshots in the window (default 0)
latest_snapshot_atstring | nullISO-8601 UTC time of the most recent snapshot

category_exposure — Category Breakdown

Array of objects showing exposure per market category:
[
  { "category": "crypto", "position_count": 3, "total_value": "500.00", "weight_percentage": "45.5%" },
  { "category": "sports", "position_count": 2, "total_value": "300.00", "weight_percentage": "27.3%" }
]

recent_trades — Recent Filled Orders

Array of the most recent filled orders (count controlled by the recent_trades query param, default 20). Each entry:
FieldTypeDescription
order_idintegerOrder ID
market_idstringMarket condition_id
market_questionstring | nullMarket question text
sidestringBUY or SELL
outcomestringOutcome name
pricestringFill price per share
quantitystringShares filled
notionalstringTotal cash value of the fill
filled_atstring | nullISO-8601 UTC fill timestamp
See Trade History for the full per-trade semantics.

natural_language_summary

A pre-computed human-readable summary of the profile:
“trader123 is a PolySimulator paper trader who joined 45 days ago. Current API balance: 9,750.00(startedat9,750.00 (started at 10,000.00, P&L: -250.00,2.50250.00, -2.50%). Has made 127 trades across 34 markets over 22 active trading days. Win rate: 62.5%. Currently holding 5 open positions worth ~1,100.00. Top categories: crypto (45.5%), sports (27.3%), politics (18.2%).”

MCP Server

A standalone MCP server for profile analysis exists and will be published alongside the official SDK (coming soon). Beta-cohort members can request early access via their invite contact.

Available MCP Tools

ToolDescription
get_profile_analysisFull analysis (primary tool)
get_balanceQuick balance check
get_positionsPosition list with live prices
get_trade_historyTrade history with pagination
get_equity_curveEquity curve snapshots