Get Positions (WebSocket)
Retrieve position information for the authenticated subaccount through the WebSocket connection with time-based filtering and pagination capabilities.
Request-Response vs Subscriptions
This method provides on-demand snapshots of current positions. For real-time updates when positions change, use SubAccount Updates Subscription.
| Method | Purpose | When to Use |
|---|---|---|
getPositions | Comprehensive position querying | Initial load, position history, filtered searches, reconciliation |
| User Updates Subscription | Real-time notifications | Live PnL updates, liquidation alerts |
Endpoint
ws.send() wss://api.synthetix.io/v1/ws/tradeRequest
Request Format
{
"id": "getpositions-1",
"method": "post",
"params": {
"action": "getPositions",
"subAccountId": "1867542890123456789",
"fromTime": 1704067200000,
"toTime": 1704153600000,
"limit": 50,
"offset": 0,
"expiresAfter": 1704067500,
"signature": {
"v": 28,
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
}
}Parameters
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated unique request identifier |
method | string | Yes | Must be "post" |
params | object | Yes | Contains all parameters for the request |
Params Object
| Parameter | Type | Required | Description |
|---|---|---|---|
action | string | Yes | Must be "getPositions" |
subAccountId | string | Yes | Subaccount identifier |
fromTime | integer | No | Start timestamp in milliseconds (inclusive) |
toTime | integer | No | End timestamp in milliseconds (inclusive) |
limit | integer | No | Maximum number of positions to return (default: 50, max: 1000) |
offset | integer | No | Number of positions to skip for pagination (default: 0) |
expiresAfter | integer | No | Optional expiration timestamp in seconds |
signature | object | Yes | EIP-712 signature using SubAccountAction type |
- This endpoint uses
SubAccountActionEIP-712 type (no nonce required, onlyexpiresAfteris optional) - Returns all positions for the authenticated subaccount
- Use time range filters to limit results to specific time periods
EIP-712 Signature
const domain = {
name: "Synthetix",
version: "1",
chainId: 1,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
const types = {
SubAccountAction: [
{ name: "subAccountId", type: "uint256" },
{ name: "action", type: "string" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: "1867542890123456789",
action: "getPositions",
expiresAfter: 0
};
const signature = await signer._signTypedData(domain, types, message);Response Format
Response Structure
| Field | Type | Description |
|---|---|---|
type | string | Always "response" |
responseType | string | Always "position" for position query responses |
result | object | Contains position data (omitted when error occurs) |
result.subAccountId | string | Subaccount identifier |
result.positions | array | Array of position objects matching filter criteria |
error | object | Error details (only present when an error occurs) |
requestId | string | Request tracking identifier |
timestamp | integer | Unix milliseconds timestamp |
Success Response Examples
Multiple Positions (Open and Closed)
{
"id": "getpositions-1",
"status": 200,
"result": [
{
"positionId": "pos_12345",
"subAccountId": "1867542890123456789",
"symbol": "ETH-USDT",
"side": "long",
"quantity": "2.5",
"entryPrice": "2400.00",
"realizedPnl": "50.00",
"unrealizedPnl": "125.00",
"usedMargin": "612.50",
"maintenanceMargin": "306.25",
"liquidationPrice": "2160.00",
"status": "open",
"netFunding": "15.50",
"takeProfitOrderIds": ["tp_001"],
"stopLossOrderIds": ["sl_002"],
"createdAt": 1735680000000,
"updatedAt": 1735689900000
},
{
"positionId": "pos_12346",
"subAccountId": "1867542890123456789",
"symbol": "BTC-USDT",
"side": "short",
"quantity": "0.1",
"entryPrice": "42000.00",
"realizedPnl": "200.00",
"unrealizedPnl": "0.00",
"usedMargin": "0.00",
"maintenanceMargin": "0.00",
"liquidationPrice": "0.00",
"status": "close",
"netFunding": "-8.50",
"takeProfitOrderIds": [],
"stopLossOrderIds": [],
"createdAt": 1735685000000,
"updatedAt": 1735689800000
}
]
}Open Positions Only
{
"id": "getpositions-2",
"status": 200,
"result": [
{
"positionId": "pos_12347",
"subAccountId": "1867542890123456789",
"symbol": "BTC-USDT",
"side": "long",
"quantity": "0.2",
"entryPrice": "43500.00",
"realizedPnl": "0.00",
"unrealizedPnl": "100.00",
"usedMargin": "1760.00",
"maintenanceMargin": "880.00",
"liquidationPrice": "41000.00",
"status": "open",
"netFunding": "2.25",
"takeProfitOrderIds": ["tp_003"],
"stopLossOrderIds": ["sl_004"],
"createdAt": 1735689000000,
"updatedAt": 1735689900000
}
]
}Empty Result Set
{
"id": "getpositions-3",
"status": 200,
"result": []
}Position Status Types
| Status | Description | Characteristics |
|---|---|---|
open | Active position | Has unrealized PnL, margin requirements, liquidation price |
close | Closed position | Only realized PnL, no margin requirements |
update | Position being updated | Transitional state during modifications |
Error Response
{
"id": "getpositions-1",
"status": 400,
"result": null,
"error": {
"code": 400,
"message": "Invalid time range: fromTime must be less than or equal to toTime"
}
}Implementation Example
class PositionQuery {
constructor(ws) {
this.ws = ws;
this.pendingRequests = new Map();
}
async getPositions(options = {}) {
const {
fromTime = null,
toTime = null,
limit = 50,
offset = 0
} = options;
// Build request (no signature needed for read-only queries)
const request = {
id: `getpositions-${Date.now()}`,
method: "post",
params: {
action: "getPositions",
limit,
offset
}
};
// Add optional filters
if (fromTime) request.params.fromTime = fromTime;
if (toTime) request.params.toTime = toTime;
// Send and wait for response
return this.sendRequest(request);
}
sendRequest(request) {
return new Promise((resolve, reject) => {
this.pendingRequests.set(request.id, { resolve, reject });
this.ws.send(JSON.stringify(request));
setTimeout(() => {
if (this.pendingRequests.has(request.id)) {
this.pendingRequests.delete(request.id);
reject(new Error('Request timeout'));
}
}, 10000); // 10 second timeout
});
}
handleResponse(response) {
const pending = this.pendingRequests.get(response.id);
if (pending) {
this.pendingRequests.delete(response.id);
if (response.error) {
pending.reject(new Error(response.error.message));
} else {
pending.resolve(response.result);
}
}
}
}
// Usage Examples
async function getAllPositions(query) {
try {
const positions = await query.getPositions({
limit: 100
});
console.log(`Found ${positions.length} positions`);
return positions;
} catch (error) {
console.error('Failed to get positions:', error);
throw error;
}
}
async function getRecentPositions(query, hoursBack = 24) {
const fromTime = Date.now() - (hoursBack * 60 * 60 * 1000);
try {
const positions = await query.getPositions({
fromTime,
limit: 50
});
console.log(`Found ${positions.length} recent positions`);
return positions;
} catch (error) {
console.error('Failed to get recent positions:', error);
throw error;
}
}Implementation Notes
- This endpoint uses
SubAccountActionEIP-712 type (no nonce required) - Time range filters must specify valid Unix millisecond timestamps
- Use pagination (limit/offset) for large result sets
Position Object Structure
class PositionMonitor {
constructor(query, subAccountId) {
this.query = query;
this.subAccountId = subAccountId;
this.positions = new Map();
this.callbacks = new Map();
this.monitoring = false;
this.riskAlerts = new Map();
}
async startMonitoring(interval = 1000) {
this.monitoring = true;
// Initial fetch
await this.refreshPositions();
// Set up periodic refresh
this.monitoringInterval = setInterval(async () => {
if (this.monitoring) {
await this.refreshPositions();
}
}, interval);
}
stopMonitoring() {
this.monitoring = false;
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
}
}
async refreshPositions() {
try {
const result = await this.query.getOpenPositions({
subAccountId: this.subAccountId
});
const newPositions = new Map();
const changes = [];
// Process current positions
for (const position of result.positions) {
const symbol = position.symbol;
const previousPosition = this.positions.get(symbol);
newPositions.set(symbol, position);
if (!previousPosition) {
// New position
changes.push({
type: 'opened',
symbol,
position
});
} else if (this.hasPositionChanged(previousPosition, position)) {
// Position updated
changes.push({
type: 'updated',
symbol,
previousPosition,
position,
changes: this.getPositionChanges(previousPosition, position)
});
}
}
// Find closed positions
for (const [symbol, position] of this.positions) {
if (!newPositions.has(symbol)) {
changes.push({
type: 'closed',
symbol,
position
});
}
}
// Update stored positions
this.positions = newPositions;
// Check risk alerts
this.checkRiskAlerts(result);
// Notify callbacks of changes
for (const change of changes) {
this.notifyCallbacks(change);
}
} catch (error) {
console.error('Position monitoring refresh failed:', error);
}
}
hasPositionChanged(previous, current) {
return previous.size !== current.size ||
previous.unrealizedPnl !== current.unrealizedPnl ||
previous.markPrice !== current.markPrice ||
previous.marginRatio !== current.marginRatio;
}
getPositionChanges(previous, current) {
const changes = [];
if (previous.size !== current.size) {
changes.push({
field: 'size',
from: previous.size,
to: current.size,
delta: parseFloat(current.size) - parseFloat(previous.size)
});
}
if (previous.unrealizedPnl !== current.unrealizedPnl) {
changes.push({
field: 'unrealizedPnl',
from: previous.unrealizedPnl,
to: current.unrealizedPnl,
delta: parseFloat(current.unrealizedPnl) - parseFloat(previous.unrealizedPnl)
});
}
if (previous.markPrice !== current.markPrice) {
changes.push({
field: 'markPrice',
from: previous.markPrice,
to: current.markPrice,
delta: parseFloat(current.markPrice) - parseFloat(previous.markPrice)
});
}
return changes;
}
onPositionChange(callback) {
const id = Date.now() + Math.random();
this.callbacks.set(id, callback);
return () => this.callbacks.delete(id);
}
notifyCallbacks(change) {
for (const callback of this.callbacks.values()) {
try {
callback(change);
} catch (error) {
console.error('Position change callback error:', error);
}
}
}
// Risk management
setRiskAlert(symbol, alertConfig) {
this.riskAlerts.set(symbol, alertConfig);
}
checkRiskAlerts(result) {
for (const position of result.positions) {
const alert = this.riskAlerts.get(position.symbol);
if (!alert) continue;
const marginRatio = parseFloat(position.marginRatio);
const pnlPct = parseFloat(position.unrealizedPnl) / parseFloat(position.margin);
// Check margin ratio alert
if (alert.marginRatioThreshold && marginRatio >= alert.marginRatioThreshold) {
this.triggerAlert('marginRatio', position, { marginRatio, threshold: alert.marginRatioThreshold });
}
// Check PnL percentage alert
if (alert.pnlThreshold && pnlPct <= alert.pnlThreshold) {
this.triggerAlert('pnlThreshold', position, { pnlPct, threshold: alert.pnlThreshold });
}
// Check liquidation distance
const markPrice = parseFloat(position.markPrice);
const liqPrice = parseFloat(position.liquidationPrice);
const liqDistance = Math.abs(markPrice - liqPrice) / markPrice;
if (alert.liquidationDistance && liqDistance <= alert.liquidationDistance) {
this.triggerAlert('liquidationRisk', position, { liqDistance, threshold: alert.liquidationDistance });
}
}
}
triggerAlert(type, position, data) {
const alert = {
type,
symbol: position.symbol,
position,
data,
timestamp: Date.now()
};
console.warn(`🚨 RISK ALERT [${type}] for ${position.symbol}:`, data);
// Notify callbacks with alert
this.notifyCallbacks({
type: 'alert',
alert
});
}
}
// Usage
const monitor = new PositionMonitor(query, subAccountId);
// Set up risk alerts
monitor.setRiskAlert('BTC-USDT', {
marginRatioThreshold: 0.8, // 80% margin ratio
pnlThreshold: -0.1, // -10% PnL
liquidationDistance: 0.05 // 5% from liquidation
});
// Listen for position changes
const unsubscribe = monitor.onPositionChange((change) => {
switch (change.type) {
case 'opened':
console.log('Position opened:', change.position);
break;
case 'updated':
console.log('Position updated:', change.symbol);
for (const fieldChange of change.changes) {
console.log(` ${fieldChange.field}: ${fieldChange.from} → ${fieldChange.to}`);
}
break;
case 'closed':
console.log('Position closed:', change.symbol);
break;
case 'alert':
console.log('Risk alert:', change.alert);
break;
}
});
await monitor.startMonitoring();Portfolio Analytics
class PositionAnalytics {
constructor(query, subAccountId) {
this.query = query;
this.subAccountId = subAccountId;
}
async getPortfolioSummary() {
const result = await this.query.getOpenPositions({
subAccountId: this.subAccountId
});
const positions = result.positions;
const summary = {
totalPositions: positions.length,
longPositions: 0,
shortPositions: 0,
totalNotional: 0,
totalMargin: 0,
totalUnrealizedPnl: 0,
avgLeverage: 0,
marginUtilization: 0,
riskMetrics: {
maxSinglePositionRisk: 0,
correlationRisk: 'low', // simplified
liquidationRisk: 'low'
}
};
let totalLeverageWeighted = 0;
let maxPositionSize = 0;
let closeToLiquidation = 0;
for (const position of positions) {
if (position.side === 'long') {
summary.longPositions++;
} else {
summary.shortPositions++;
}
const notional = Math.abs(parseFloat(position.size)) * parseFloat(position.markPrice);
const margin = parseFloat(position.margin);
const pnl = parseFloat(position.unrealizedPnl);
const leverage = parseFloat(position.leverage);
summary.totalNotional += notional;
summary.totalMargin += margin;
summary.totalUnrealizedPnl += pnl;
totalLeverageWeighted += leverage * margin;
// Risk metrics
if (notional > maxPositionSize) {
maxPositionSize = notional;
}
// Check liquidation risk
const markPrice = parseFloat(position.markPrice);
const liqPrice = parseFloat(position.liquidationPrice);
const liqDistance = Math.abs(markPrice - liqPrice) / markPrice;
if (liqDistance < 0.1) { // Within 10% of liquidation
closeToLiquidation++;
}
}
if (summary.totalMargin > 0) {
summary.avgLeverage = totalLeverageWeighted / summary.totalMargin;
summary.marginUtilization = summary.totalMargin / parseFloat(result.summary.accountEquity);
}
summary.riskMetrics.maxSinglePositionRisk = maxPositionSize / summary.totalNotional;
summary.riskMetrics.liquidationRisk = closeToLiquidation > 0 ? 'high' : 'low';
return summary;
}
async getPositionMetrics() {
const result = await this.query.getOpenPositions({
subAccountId: this.subAccountId
});
return result.positions.map(position => {
const size = parseFloat(position.size);
const markPrice = parseFloat(position.markPrice);
const entryPrice = parseFloat(position.entryPrice);
const margin = parseFloat(position.margin);
const unrealizedPnl = parseFloat(position.unrealizedPnl);
const liqPrice = parseFloat(position.liquidationPrice);
const notional = Math.abs(size) * markPrice;
const pnlPct = (unrealizedPnl / margin) * 100;
const priceMovePct = ((markPrice - entryPrice) / entryPrice) * 100;
const liqDistance = Math.abs(markPrice - liqPrice) / markPrice * 100;
return {
symbol: position.symbol,
side: position.side,
size,
notional,
pnlPct,
priceMovePct,
liquidationDistance: liqDistance,
marginRatio: parseFloat(position.marginRatio) * 100,
leverage: parseFloat(position.leverage),
riskLevel: this.calculateRiskLevel(position)
};
});
}
calculateRiskLevel(position) {
const marginRatio = parseFloat(position.marginRatio);
const markPrice = parseFloat(position.markPrice);
const liqPrice = parseFloat(position.liquidationPrice);
const liqDistance = Math.abs(markPrice - liqPrice) / markPrice;
if (marginRatio > 0.8 || liqDistance < 0.05) {
return 'high';
} else if (marginRatio > 0.6 || liqDistance < 0.1) {
return 'medium';
} else {
return 'low';
}
}
async getDiversificationMetrics() {
const result = await this.query.getOpenPositions({
subAccountId: this.subAccountId
});
const assetExposure = new Map();
let totalNotional = 0;
for (const position of result.positions) {
const asset = position.symbol.split('-')[0]; // Extract base asset
const notional = Math.abs(parseFloat(position.size)) * parseFloat(position.markPrice);
totalNotional += notional;
const currentExposure = assetExposure.get(asset) || 0;
assetExposure.set(asset, currentExposure + notional);
}
// Calculate concentration ratios
const exposureArray = Array.from(assetExposure.values()).sort((a, b) => b - a);
const concentrationRatios = {
top1: totalNotional > 0 ? exposureArray[0] / totalNotional : 0,
top3: totalNotional > 0 ? exposureArray.slice(0, 3).reduce((sum, val) => sum + val, 0) / totalNotional : 0,
herfindahl: totalNotional > 0 ? exposureArray.reduce((sum, val) => sum + Math.pow(val / totalNotional, 2), 0) : 0
};
return {
assetExposure: Object.fromEntries(assetExposure),
concentrationRatios,
diversificationScore: 1 - concentrationRatios.herfindahl // Higher = more diversified
};
}
}Position Risk Manager
class PositionRiskManager {
constructor(query, subAccountId) {
this.query = query;
this.subAccountId = subAccountId;
this.riskLimits = new Map();
}
setRiskLimits(limits) {
// limits = {
// maxPositionSize: 100000,
// maxLeverage: 20,
// maxMarginUtilization: 0.8,
// maxSingleAssetExposure: 0.3
// }
this.riskLimits = new Map(Object.entries(limits));
}
async checkRiskCompliance() {
const result = await this.query.getOpenPositions({
subAccountId: this.subAccountId
});
const violations = [];
const warnings = [];
// Check individual position limits
for (const position of result.positions) {
const notional = Math.abs(parseFloat(position.size)) * parseFloat(position.markPrice);
const leverage = parseFloat(position.leverage);
const marginRatio = parseFloat(position.marginRatio);
// Max position size
const maxSize = this.riskLimits.get('maxPositionSize');
if (maxSize && notional > maxSize) {
violations.push({
type: 'positionSize',
symbol: position.symbol,
value: notional,
limit: maxSize,
severity: 'high'
});
}
// Max leverage
const maxLev = this.riskLimits.get('maxLeverage');
if (maxLev && leverage > maxLev) {
violations.push({
type: 'leverage',
symbol: position.symbol,
value: leverage,
limit: maxLev,
severity: 'medium'
});
}
// Margin ratio warning
if (marginRatio > 0.7) {
warnings.push({
type: 'marginRatio',
symbol: position.symbol,
value: marginRatio,
threshold: 0.7,
severity: marginRatio > 0.8 ? 'high' : 'medium'
});
}
}
// Portfolio-level checks
const portfolioMetrics = await this.calculatePortfolioRisk(result);
const maxMarginUtil = this.riskLimits.get('maxMarginUtilization');
if (maxMarginUtil && portfolioMetrics.marginUtilization > maxMarginUtil) {
violations.push({
type: 'marginUtilization',
value: portfolioMetrics.marginUtilization,
limit: maxMarginUtil,
severity: 'high'
});
}
return {
compliant: violations.length === 0,
violations,
warnings,
portfolioMetrics
};
}
async calculatePortfolioRisk(result) {
const positions = result.positions;
const summary = result.summary;
const totalNotional = positions.reduce((sum, pos) => {
return sum + Math.abs(parseFloat(pos.size)) * parseFloat(pos.markPrice);
}, 0);
const marginUtilization = parseFloat(summary.totalMargin) / parseFloat(summary.accountEquity);
return {
totalNotional,
marginUtilization,
totalUnrealizedPnl: parseFloat(summary.totalUnrealizedPnl),
accountEquity: parseFloat(summary.accountEquity),
freeCollateral: parseFloat(summary.freeCollateral)
};
}
async generateRiskReport() {
const compliance = await this.checkRiskCompliance();
const analytics = new PositionAnalytics(this.query, this.subAccountId);
const diversification = await analytics.getDiversificationMetrics();
const positionMetrics = await analytics.getPositionMetrics();
return {
timestamp: Date.now(),
compliance,
diversification,
positions: positionMetrics,
riskScore: this.calculateOverallRiskScore(compliance, diversification, positionMetrics)
};
}
calculateOverallRiskScore(compliance, diversification, positions) {
let score = 100; // Start with perfect score
// Deduct for violations
score -= compliance.violations.length * 15;
score -= compliance.warnings.length * 5;
// Deduct for concentration
score -= (1 - diversification.diversificationScore) * 20;
// Deduct for high-risk positions
const highRiskPositions = positions.filter(p => p.riskLevel === 'high').length;
score -= highRiskPositions * 10;
return Math.max(0, Math.min(100, score));
}
}Position Object Structure
Position Fields
Important: All numeric fields in position objects are represented as strings to ensure decimal precision. Timestamps are provided as strings containing Unix milliseconds.
| Field | Type | Description |
|---|---|---|
positionId | string | Unique position identifier |
subAccountId | string | Subaccount identifier |
symbol | string | Market symbol |
side | string | Position side ("long" or "short") |
quantity | string | Position size (always positive) |
entryPrice | string | Volume-weighted average entry price |
markPrice | string | Current mark price |
notionalValue | string | Position notional value (quantity × mark price) |
usedMargin | string | Margin currently allocated to position |
maintenanceMargin | string | Minimum margin for position maintenance |
unrealizedPnl | string | Current unrealized profit/loss |
leverage | string | Position leverage |
liquidationPrice | string | Price at which position will be liquidated |
status | string | Position status ("open", "close", "update") |
netFunding | string | Net funding payments received/paid |
takeProfitOrderIds | string[] | Associated take profit order IDs |
stopLossOrderIds | string[] | Associated stop loss order IDs |
createdAt | string | Position creation timestamp (Unix ms) |
updatedAt | string | Last update timestamp (Unix ms) |
| Error | Description |
|---|---|
| Invalid signature | EIP-712 signature validation failed |
| Invalid market symbol | Market symbol not recognized |
| Nonce already used | Nonce must be greater than previous value |
| Rate limit exceeded | Too many requests in time window |
| Request expired | expiresAfter timestamp has passed |
Next Steps
- SubAccount Updates Subscription - Real-time position change notifications
- Get Order History - Order querying via WebSocket
- Cancel All Orders - Emergency position management
- REST Unified Endpoint - Recommended unified approach with historical data support