Moonbase Docs

Portfolio Channel

Real-time portfolio updates including balances and statistics

Overview

The portfolio channel provides real-time updates of your account portfolio, including asset balances, locked amounts, trading fees, and account statistics. Updates are sent whenever any balance or stat changes due to trades, orders, deposits, or withdrawals.

Channel: portfolio Authentication: Required Product: Not applicable (all assets included)

Subscribe

You must authenticate before subscribing to the portfolio channel. See Authentication for details.

Request:

{
  "op": "sub",
  "channel": "portfolio"
}

Friendly format:

sub portfolio

Parameters:

  • op (string, required) - Must be "sub"
  • channel (string, required) - Must be "portfolio"

Responses

Subscription Confirmation

Upon successful subscription:

{
  "channel": "portfolio",
  "type": "subscribed"
}

Snapshot Message

Immediately after subscription, you'll receive a complete snapshot of your portfolio:

{
  "channel": "portfolio",
  "type": "snapshot",
  "data": [
    {
      "id": "1000004",
      "maker_fee_rate": "0.0001",
      "taker_fee_rate": "0.0005",
      "asset_balances": [
        {
          "asset": "VND",
          "balance": "850102477.1110636",
          "free": "724006849.6121439",
          "locked": "126095627.4989197"
        },
        {
          "asset": "BTC",
          "balance": "2.130511",
          "free": "2.058586",
          "locked": "0.071925"
        },
        {
          "asset": "ETH",
          "balance": "15.5",
          "free": "15.5",
          "locked": "0"
        }
      ],
      "stats": {
        "order_stats": {
          "total_orders": "916152",
          "total_done_orders": "916105",
          "total_open_orders": "46",
          "total_pending_orders": "1",
          "total_accepted_orders": "0"
        },
        "trading_stats": {
          "total_trading_volume": "99612906669"
        }
      }
    }
  ],
  "timestamp": "1763121358960696000",
  "gsn": 105240183
}

Update Messages

After the snapshot, you'll receive updates whenever your portfolio changes:

{
  "channel": "portfolio",
  "type": "update",
  "data": [
    {
      "id": "1000004",
      "maker_fee_rate": "0.0001",
      "taker_fee_rate": "0.0005",
      "asset_balances": [
        {
          "asset": "VND",
          "balance": "850202477.1110636",
          "free": "724106849.6121439",
          "locked": "126095627.4989197"
        },
        {
          "asset": "BTC",
          "balance": "2.130511",
          "free": "2.058586",
          "locked": "0.071925"
        }
      ],
      "stats": {
        "order_stats": {
          "total_orders": "916153",
          "total_done_orders": "916106",
          "total_open_orders": "46",
          "total_pending_orders": "1",
          "total_accepted_orders": "0"
        },
        "trading_stats": {
          "total_trading_volume": "99612906669"
        }
      }
    }
  ],
  "timestamp": "1763121359960696000",
  "gsn": 105240184
}

Data Structure

interface PortfolioData {
  id: string;                  // Account ID
  maker_fee_rate: string;      // Maker fee rate (e.g., "0.0001" = 0.01%)
  taker_fee_rate: string;      // Taker fee rate (e.g., "0.0005" = 0.05%)
  asset_balances: AssetBalance[];
  stats: AccountStats;
}

interface AssetBalance {
  asset: string;      // Asset symbol (e.g., "BTC", "VND")
  balance: string;    // Total balance
  free: string;       // Available balance
  locked: string;     // Locked balance
}

interface AccountStats {
  order_stats: {
    total_orders: string;         // Total orders created
    total_done_orders: string;    // Total completed orders
    total_open_orders: string;    // Currently open orders
    total_pending_orders: string; // Pending orders
    total_accepted_orders: string; // Accepted orders
  };
  trading_stats: {
    total_trading_volume: string;  // Total trading volume (in VND)
  };
}

interface PortfolioMessage {
  channel: "portfolio";
  type: "snapshot" | "update";
  data: PortfolioData[];  // Array (supports future multi-account features)
  timestamp: string;      // Nanoseconds since epoch
  gsn: number;           // Global Sequence Number
}

Balance Calculations

Understanding the relationship between balance fields:

balance = free + locked

free = balance - locked
  • balance - Total amount you own of this asset
  • free - Amount available for trading or withdrawal
  • locked - Amount locked by open orders and pending withdrawals

Update Triggers

Portfolio updates are sent when:

  1. Orders are created - Locks funds for the order
  2. Orders are triggered - Delayed orders activate and lock funds
  3. Orders are filled - Updates balance and free amounts
  4. Orders are cancelled - Unlocks previously locked funds
  5. Trades are executed - Updates balances for both assets in the pair
  6. Deposits occur - Increases balance and free amounts
  7. Withdrawals are created - Locks funds for the withdrawal
  8. Withdrawals complete - Reduces balance and unlocks funds
  9. Order statistics change - Updates total orders, done orders, etc.
  10. Trading volume changes - Updates total trading volume

Example: Portfolio Tracker

const WebSocket = require('ws');

class PortfolioTracker {
  constructor() {
    this.portfolio = null;
  }

  handleMessage(message) {
    if (message.type === 'snapshot' || message.type === 'update') {
      const newPortfolio = message.data[0];  // First account

      if (this.portfolio) {
        this.compareAndLog(this.portfolio, newPortfolio);
      }

      this.portfolio = newPortfolio;
      this.displayPortfolio();
    }
  }

  compareAndLog(oldPortfolio, newPortfolio) {
    // Check for balance changes
    oldPortfolio.asset_balances.forEach(oldAsset => {
      const newAsset = newPortfolio.asset_balances.find(
        a => a.asset === oldAsset.asset
      );

      if (newAsset) {
        const oldBalance = parseFloat(oldAsset.balance);
        const newBalance = parseFloat(newAsset.balance);
        const change = newBalance - oldBalance;

        if (change !== 0) {
          console.log(`💰 Balance Change: ${oldAsset.asset}`);
          console.log(`   ${change > 0 ? '+' : ''}${change.toFixed(8)}`);
          console.log(`   New Balance: ${newBalance.toFixed(8)}`);
        }

        const oldLocked = parseFloat(oldAsset.locked);
        const newLocked = parseFloat(newAsset.locked);
        const lockedChange = newLocked - oldLocked;

        if (lockedChange !== 0) {
          console.log(`🔒 Locked Change: ${oldAsset.asset}`);
          console.log(`   ${lockedChange > 0 ? '+' : ''}${lockedChange.toFixed(8)}`);
        }
      }
    });

    // Check for new assets
    newPortfolio.asset_balances.forEach(newAsset => {
      const oldAsset = oldPortfolio.asset_balances.find(
        a => a.asset === newAsset.asset
      );
      if (!oldAsset) {
        console.log(`🆕 New Asset: ${newAsset.asset}`);
        console.log(`   Balance: ${newAsset.balance}`);
      }
    });
  }

  displayPortfolio() {
    console.log('\n=== PORTFOLIO ===');
    console.log(`Account ID: ${this.portfolio.id}`);
    console.log(`Maker Fee: ${(parseFloat(this.portfolio.maker_fee_rate) * 100).toFixed(2)}%`);
    console.log(`Taker Fee: ${(parseFloat(this.portfolio.taker_fee_rate) * 100).toFixed(2)}%`);
    console.log('\nAsset Balances:');

    this.portfolio.asset_balances.forEach(asset => {
      console.log(`  ${asset.asset}:`);
      console.log(`    Total:  ${asset.balance}`);
      console.log(`    Free:   ${asset.free}`);
      console.log(`    Locked: ${asset.locked}`);
    });

    const stats = this.portfolio.stats;
    console.log('\nOrder Statistics:');
    console.log(`  Total Orders: ${stats.order_stats.total_orders}`);
    console.log(`  Done Orders:  ${stats.order_stats.total_done_orders}`);
    console.log(`  Open Orders:  ${stats.order_stats.total_open_orders}`);

    console.log('\nTrading Statistics:');
    console.log(`  Total Volume: ${stats.trading_stats.total_trading_volume} VND`);
  }

  getAssetBalance(asset) {
    const assetBalance = this.portfolio?.asset_balances.find(
      a => a.asset === asset
    );
    return assetBalance || null;
  }

  getTotalValue(prices) {
    // Calculate total portfolio value in VND
    let total = 0;

    this.portfolio?.asset_balances.forEach(asset => {
      if (asset.asset === 'VND') {
        total += parseFloat(asset.balance);
      } else {
        const price = prices[`${asset.asset}-VND`];
        if (price) {
          total += parseFloat(asset.balance) * price;
        }
      }
    });

    return total;
  }
}

// Usage
const tracker = new PortfolioTracker();

ws.on('open', async () => {
  // Authenticate first
  await authenticate(ws);

  // Subscribe to portfolio
  ws.send(JSON.stringify({
    op: 'sub',
    channel: 'portfolio'
  }));
});

ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message.channel === 'portfolio') {
    tracker.handleMessage(message);
  }
});

Example: Balance Alert System

class BalanceAlert {
  constructor(asset, threshold, type = 'below') {
    this.asset = asset;
    this.threshold = threshold;
    this.type = type; // 'below' or 'above'
    this.triggered = false;
  }

  check(assetBalance) {
    if (this.triggered) return false;

    const balance = parseFloat(assetBalance.balance);

    if (this.type === 'below' && balance < this.threshold) {
      this.triggered = true;
      return true;
    }

    if (this.type === 'above' && balance > this.threshold) {
      this.triggered = true;
      return true;
    }

    return false;
  }
}

// Set up alerts
const alerts = [
  new BalanceAlert('VND', 1000000000, 'below'),  // Alert if VND < 1B
  new BalanceAlert('BTC', 1, 'below'),           // Alert if BTC < 1
];

ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message.channel === 'portfolio' && message.type === 'update') {
    const portfolio = message.data[0];

    portfolio.asset_balances.forEach(asset => {
      alerts.forEach((alert, index) => {
        if (alert.asset === asset.asset && alert.check(asset)) {
          console.log(`🚨 BALANCE ALERT ${index + 1}!`);
          console.log(`${asset.asset} balance is ${alert.type} ${alert.threshold}`);
          console.log(`Current balance: ${asset.balance}`);
          // Send notification
        }
      });
    });
  }
});

Unsubscribe

To stop receiving portfolio updates:

Request:

{
  "op": "unsub",
  "channel": "portfolio"
}

Response:

{
  "type": "unsubscribed",
  "channel": "portfolio"
}

Important Notes

  1. Authentication Required - You must authenticate before subscribing
  2. Array Format - Data is always an array (supports future multi-account admin features)
  3. Nanosecond Timestamps - All timestamps are in nanoseconds since Unix epoch
  4. GSN Tracking - Use Global Sequence Number to ensure message ordering
  5. Fee Rates - Maker and taker fee rates are decimal strings (e.g., "0.0001" = 0.01%)
  6. Asset Inclusion - Only assets with non-zero balances are included
  7. Locked Amounts - Includes funds locked by both orders and pending withdrawals

Use Cases

  1. Balance Display - Show real-time account balances in your UI
  2. Portfolio Valuation - Calculate total portfolio value
  3. Risk Management - Monitor available balances before placing orders
  4. Balance Alerts - Notify users of low balances
  5. Trading Bots - Track available funds for automated trading
  6. Analytics - Track trading volume and order statistics

Best Practices

  1. Cache Portfolio State - Maintain local portfolio state and apply updates
  2. Handle Reconnections - Re-subscribe and process snapshot after reconnecting
  3. Validate Balances - Verify balance = free + locked relationship
  4. Use GSN - Track GSN to detect and handle missed messages
  5. Combine with Orders - Use orders channel to understand why balances are locked