Skip to content

Get Positions (WebSocket)

Retrieve position information for the authenticated subaccount through the WebSocket connection with comprehensive filtering, sorting, and pagination capabilities, providing access to positions in any status (open, close, update) with flexible query options.

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",
    "status": ["open", "close"],
    "symbol": "BTC-USDT",
    "fromTime": 1704067200000,
    "toTime": 1704153600000,
    "limit": 50,
    "offset": 0,
    "sortBy": "updatedAt",
    "sortOrder": "desc",
    "subAccountId": "1867542890123456789",
    "nonce": 1704067200000,
    "signature": {
      "v": 28,
      "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
    }
  }
}

EIP-712 Domain: { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" }

Parameters

Request Parameters

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

Params Object

ParameterTypeRequiredDescription
actionstringYesMust be "getPositions"
statusstring[]NoFilter by position status: ["open", "close", "update"]
symbolstringNoFilter positions by specific market symbol
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)
sortBystringNoSort field: "createdAt", "updatedAt" (default: "updatedAt")
sortOrderstringNoSort direction: "asc" or "desc" (default: "desc")
subAccountIdstringYesSubaccount identifier
nonceintegerYesIncrementing nonce (Unix ms timestamp as number)
signatureobjectYesEIP-712 signature

Filter Combinations

The endpoint supports powerful filter combinations:

  • All Positions: Omit status to get positions in any status
  • Open Positions Only: "status": ["open"]
  • Historical Positions: "status": ["close"] with time range
  • Recent Activity: Use fromTime without toTime for positions since a specific time
  • Symbol-Specific: Combine symbol with any other filters

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, signer) {
    this.ws = ws;
    this.signer = signer;
    this.requestId = 0;
  }
 
  async getPositions(options = {}) {
    const {
      status = [],
      symbol = null,
      fromTime = null,
      toTime = null,
      limit = 50,
      offset = 0,
      sortBy = "updatedAt",
      sortOrder = "desc",
      subAccountId
    } = options;
 
    // Generate nonce
    const nonce = Date.now();
 
    // Create query signature
    const signature = await this.signGetPositions(options, nonce);
 
    // Build action object
    const action = {
      type: "getPositions",
      limit,
      offset,
      sortBy,
      sortOrder
    };
 
    if (status.length > 0) {
      action.status = status;
    }
    if (symbol) {
      action.symbol = symbol;
    }
    if (fromTime) {
      action.fromTime = fromTime;
    }
    if (toTime) {
      action.toTime = toTime;
    }
 
    // Build request
    const request = {
      method: "getPositions",
      params: {
        action,
        subAccountId,
        nonce,
        signature
      }
    };
 
    // Send and wait for response
    return this.sendRequest(request);
  }
 
  async signGetPositions(options, nonce) {
    const domain = {
      name: "Synthetix",
      version: "1",
      chainId: 1,
      verifyingContract: "0x0000000000000000000000000000000000000000"
    };
 
    const types = {
      GetPositions: [
        { name: "action", type: "GetPositionsAction" },
        { name: "subAccountId", type: "string" },
        { name: "nonce", type: "uint256" }
      ],
      GetPositionsAction: [
        { name: "action", type: "string" },
        { name: "status", type: "string" }, // Note: stringified array
        { name: "symbol", type: "string" },
        { name: "fromTime", type: "uint256" },
        { name: "toTime", type: "uint256" },
        { name: "limit", type: "uint256" },
        { name: "offset", type: "uint256" },
        { name: "sortBy", type: "string" },
        { name: "sortOrder", type: "string" }
      ]
    };
 
    const message = {
      action: {
        type: "getPositions",
        status: JSON.stringify(options.status || []),
        symbol: options.symbol || "",
        fromTime: options.fromTime || 0,
        toTime: options.toTime || 0,
        limit: options.limit || 50,
        offset: options.offset || 0,
        sortBy: options.sortBy || "updatedAt",
        sortOrder: options.sortOrder || "desc"
      },
      subAccountId: options.subAccountId,
      nonce
    };
 
    const signature = await this.signer._signTypedData(domain, types, message);
    return ethers.utils.splitSignature(signature);
  }
 
  sendRequest(request) {
    return new Promise((resolve, reject) => {
      this.pendingRequests.set(request.requestId, { resolve, reject });
      this.ws.send(JSON.stringify(request));
 
      setTimeout(() => {
        if (this.pendingRequests.has(request.requestId)) {
          this.pendingRequests.delete(request.requestId);
          reject(new Error('Request timeout'));
        }
      }, 10000); // 10 second timeout
    });
  }
}
 
// Usage Examples
async function getOpenPositions(query, subAccountId) {
  try {
    const result = await query.getPositions({
      subAccountId,
      status: ["open"]
    });
    console.log(`Found ${result.positions.length} open positions`);
    return result;
  } catch (error) {
    console.error('Failed to get open positions:', error);
    throw error;
  }
}
 
async function getPositionHistory(query, subAccountId, fromTime) {
  try {
    const result = await query.getPositions({
      subAccountId,
      status: ["close"],
      fromTime,
      sortBy: "updatedAt",
      sortOrder: "desc"
    });
    console.log(`Found ${result.positions.length} historical positions`);
    return result;
  } catch (error) {
    console.error('Failed to get position history:', error);
    throw error;
  }
}
 
async function getPositionBySymbol(query, subAccountId, symbol) {
  try {
    const result = await query.getPositions({
      subAccountId,
      symbol,
      status: ["open"]
    });
 
    if (result.positions.length > 0) {
      const position = result.positions[0];
      console.log(`${symbol} position: ${position.quantity} @ ${position.entryPrice}`);
      return position;
    } else {
      console.log(`No open position for ${symbol}`);
      return null;
    }
  } catch (error) {
    console.error(`Failed to get position for ${symbol}:`, error);
    throw error;
  }
}

Implementation Notes

  • Position queries must include valid subaccount authentication
  • Authentication timestamps must be monotonically increasing per subaccount
  • EIP-712 domain separator must use exactly "Synthetix" as domain name (not "Synthetix Offchain" or other variants)
  • Time range filters must specify valid Unix millisecond timestamps

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)

Signing

All trading methods are signed using EIP-712. Each successful trading request will contain:

  • A piece of structured data that includes the sender address
  • A signature of the hash of that structured data, signed by the sender

For detailed information on EIP-712 signing, see EIP-712 Signing.

Nonce Management

The nonce system prevents replay attacks and ensures order uniqueness:

  • Use current timestamp in milliseconds as nonce
  • Each nonce must be greater than the previous one
  • Recommended: Use Date.now() or equivalent
  • If nonce conflicts occur, increment by 1 and retry
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