Skip to content

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.

MethodPurposeWhen to Use
getPositionsComprehensive position queryingInitial load, position history, filtered searches, reconciliation
User Updates SubscriptionReal-time notificationsLive PnL updates, liquidation alerts

Endpoint

ws.send() wss://api.synthetix.io/v1/ws/trade

Request

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

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "post"
paramsobjectYesContains all parameters for the request

Params Object

ParameterTypeRequiredDescription
actionstringYesMust be "getPositions"
subAccountIdstringYesSubaccount identifier
fromTimeintegerNoStart timestamp in milliseconds (inclusive)
toTimeintegerNoEnd timestamp in milliseconds (inclusive)
limitintegerNoMaximum number of positions to return (default: 50, max: 1000)
offsetintegerNoNumber of positions to skip for pagination (default: 0)
expiresAfterintegerNoOptional expiration timestamp in seconds
signatureobjectYesEIP-712 signature using SubAccountAction type
Important Notes:
  • This endpoint uses SubAccountAction EIP-712 type (no nonce required, only expiresAfter is 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

FieldTypeDescription
typestringAlways "response"
responseTypestringAlways "position" for position query responses
resultobjectContains position data (omitted when error occurs)
result.subAccountIdstringSubaccount identifier
result.positionsarrayArray of position objects matching filter criteria
errorobjectError details (only present when an error occurs)
requestIdstringRequest tracking identifier
timestampintegerUnix 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

StatusDescriptionCharacteristics
openActive positionHas unrealized PnL, margin requirements, liquidation price
closeClosed positionOnly realized PnL, no margin requirements
updatePosition being updatedTransitional 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 SubAccountAction EIP-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.

FieldTypeDescription
positionIdstringUnique position identifier
subAccountIdstringSubaccount identifier
symbolstringMarket symbol
sidestringPosition side ("long" or "short")
quantitystringPosition size (always positive)
entryPricestringVolume-weighted average entry price
markPricestringCurrent mark price
notionalValuestringPosition notional value (quantity × mark price)
usedMarginstringMargin currently allocated to position
maintenanceMarginstringMinimum margin for position maintenance
unrealizedPnlstringCurrent unrealized profit/loss
leveragestringPosition leverage
liquidationPricestringPrice at which position will be liquidated
statusstringPosition status ("open", "close", "update")
netFundingstringNet funding payments received/paid
takeProfitOrderIdsstring[]Associated take profit order IDs
stopLossOrderIdsstring[]Associated stop loss order IDs
createdAtstringPosition creation timestamp (Unix ms)
updatedAtstringLast update timestamp (Unix ms)
ErrorDescription
Invalid signatureEIP-712 signature validation failed
Invalid market symbolMarket symbol not recognized
Nonce already usedNonce must be greater than previous value
Rate limit exceededToo many requests in time window
Request expiredexpiresAfter timestamp has passed

Next Steps