Skip to main content
This guide shows how to build a portfolio tracker that fetches live prices for a set of stock holdings and calculates current value, cost basis, and unrealized gain or loss — the same core logic NGN Market uses in its own dashboard. Endpoints used:
  • GET /companies (Free plan) — bulk price fetch for all holdings at once
  • GET /companies/:symbol (Starter plan) — single stock quote when needed

How it works

  1. Store each holding as { symbol, quantity, avg_cost } — either in a database or client-side
  2. Fetch current prices for all held symbols in one call
  3. Calculate position value and P&L per stock
  4. Sum up for the total portfolio view

Step 1: Define your holdings

JavaScript
const holdings = [
  { symbol: 'DANGCEM',   quantity: 500,  avg_cost: 285.00 },
  { symbol: 'GTCO',      quantity: 2000, avg_cost: 55.40  },
  { symbol: 'AIRTELAFRI',quantity: 300,  avg_cost: 1820.00},
  { symbol: 'MTNN',      quantity: 800,  avg_cost: 198.50 },
  { symbol: 'ZENITHBANK',quantity: 1500, avg_cost: 34.20  },
];
Python
holdings = [
    {'symbol': 'DANGCEM',    'quantity': 500,  'avg_cost': 285.00},
    {'symbol': 'GTCO',       'quantity': 2000, 'avg_cost': 55.40 },
    {'symbol': 'AIRTELAFRI', 'quantity': 300,  'avg_cost': 1820.00},
    {'symbol': 'MTNN',       'quantity': 800,  'avg_cost': 198.50},
    {'symbol': 'ZENITHBANK', 'quantity': 1500, 'avg_cost': 34.20 },
]

Step 2: Fetch live prices for all holdings

Fetch the full company list and filter to your held symbols — one API call covers the whole portfolio.
const API_KEY = 'ngm_live_YOUR_KEY';

async function fetchPrices(symbols) {
  const res = await fetch(
    'https://api.ngnmarket.com/v1/companies?limit=200',
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  const { data } = await res.json();

  // Build a symbol → price map for fast lookup
  return Object.fromEntries(
    data.companies
      .filter(c => symbols.includes(c.symbol))
      .map(c => [c.symbol, c])
  );
}

Step 3: Calculate P&L for each position

function calcPortfolio(holdings, priceMap) {
  let totalCost         = 0;
  let totalCurrentValue = 0;

  const positions = holdings.map(({ symbol, quantity, avg_cost }) => {
    const stock         = priceMap[symbol];
    const current_price = stock?.current_price ?? avg_cost;
    const cost_basis    = quantity * avg_cost;
    const current_value = quantity * current_price;
    const gain_loss     = current_value - cost_basis;
    const gain_loss_pct = (gain_loss / cost_basis) * 100;

    totalCost         += cost_basis;
    totalCurrentValue += current_value;

    return {
      symbol,
      company_name:   stock?.company_name ?? symbol,
      quantity,
      avg_cost,
      current_price,
      cost_basis,
      current_value,
      gain_loss,
      gain_loss_pct,
      last_updated: stock?.last_updated,
    };
  });

  return {
    positions,
    summary: {
      total_cost:          totalCost,
      total_current_value: totalCurrentValue,
      total_gain_loss:     totalCurrentValue - totalCost,
      total_gain_loss_pct: ((totalCurrentValue - totalCost) / totalCost) * 100,
    },
  };
}

Step 4: Put it all together

const symbols  = holdings.map(h => h.symbol);
const priceMap = await fetchPrices(symbols);
const { positions, summary } = calcPortfolio(holdings, priceMap);

console.log('Portfolio summary:');
console.log(`  Cost basis:     ₦${summary.total_cost.toLocaleString()}`);
console.log(`  Current value:  ₦${summary.total_current_value.toLocaleString()}`);
console.log(`  Unrealized P&L: ₦${summary.total_gain_loss.toLocaleString()} (${summary.total_gain_loss_pct.toFixed(2)}%)`);

console.log('\nPositions:');
positions.forEach(p => {
  const sign = p.gain_loss >= 0 ? '+' : '';
  console.log(
    `  ${p.symbol.padEnd(12)}${p.current_price.toFixed(2).padStart(8)}  ` +
    `${sign}${p.gain_loss.toLocaleString()} (${sign}${p.gain_loss_pct.toFixed(2)}%)`
  );
});

Sample output

Portfolio summary:
  Cost basis:     ₦19,275,000.00
  Current value:  ₦21,043,500.00
  Unrealized P&L: ₦1,768,500.00 (+9.17%)

Positions:
  DANGCEM      ₦  302.50  +₦8,750.00 (+6.14%)
  GTCO         ₦   62.10  +₦13,400.00 (+12.10%)
  AIRTELAFRI   ₦ 1910.00  +₦27,000.00 (+4.95%)
  MTNN         ₦  205.30  +₦5,440.00 (+3.42%)
  ZENITHBANK   ₦   37.80  +₦5,400.00 (+10.53%)

React component

React
import { useEffect, useState } from 'react';

const API_KEY  = 'ngm_live_YOUR_KEY';
const INTERVAL = 5 * 60 * 1000; // refresh every 5 minutes

const HOLDINGS = [
  { symbol: 'DANGCEM',    quantity: 500,  avg_cost: 285.00 },
  { symbol: 'GTCO',       quantity: 2000, avg_cost: 55.40  },
  { symbol: 'AIRTELAFRI', quantity: 300,  avg_cost: 1820.00},
];

async function loadPortfolio() {
  const res = await fetch('https://api.ngnmarket.com/v1/companies?limit=200', {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  const { data } = await res.json();
  const priceMap = Object.fromEntries(data.companies.map(c => [c.symbol, c]));

  return HOLDINGS.map(({ symbol, quantity, avg_cost }) => {
    const stock         = priceMap[symbol] ?? {};
    const current_price = stock.current_price ?? avg_cost;
    const cost_basis    = quantity * avg_cost;
    const current_value = quantity * current_price;
    const gain_loss     = current_value - cost_basis;
    return { symbol, company_name: stock.company_name, quantity, avg_cost,
             current_price, cost_basis, current_value, gain_loss,
             gain_loss_pct: (gain_loss / cost_basis) * 100 };
  });
}

export function PortfolioTracker() {
  const [positions, setPositions] = useState([]);

  useEffect(() => {
    loadPortfolio().then(setPositions);
    const t = setInterval(() => loadPortfolio().then(setPositions), INTERVAL);
    return () => clearInterval(t);
  }, []);

  const total = positions.reduce((acc, p) => ({
    cost: acc.cost + p.cost_basis,
    value: acc.value + p.current_value,
  }), { cost: 0, value: 0 });

  const totalGL    = total.value - total.cost;
  const totalGLPct = total.cost ? (totalGL / total.cost) * 100 : 0;

  return (
    <div>
      <h2>Portfolio — ₦{total.value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</h2>
      <p style={{ color: totalGL >= 0 ? 'green' : 'red' }}>
        {totalGL >= 0 ? '+' : ''}{totalGL.toLocaleString(undefined, { maximumFractionDigits: 2 })} ({totalGLPct.toFixed(2)}%)
      </p>
      <table>
        <thead>
          <tr><th>Symbol</th><th>Qty</th><th>Avg cost</th><th>Price</th><th>Value</th><th>P&L</th></tr>
        </thead>
        <tbody>
          {positions.map(p => (
            <tr key={p.symbol}>
              <td>{p.symbol}</td>
              <td>{p.quantity}</td>
              <td>{p.avg_cost.toFixed(2)}</td>
              <td>{p.current_price.toFixed(2)}</td>
              <td>{p.current_value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</td>
              <td style={{ color: p.gain_loss >= 0 ? 'green' : 'red' }}>
                {p.gain_loss >= 0 ? '+' : ''}{p.gain_loss.toLocaleString(undefined, { maximumFractionDigits: 2 })} ({p.gain_loss_pct.toFixed(2)}%)
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Tips

Store holdings server-side. Keeping avg_cost and quantity in your own database means you can calculate P&L for any user without exposing API keys to the client. Refresh on the same cadence as the ticker. Prices update every 20 minutes during trading hours — a 5-minute poll interval is a good balance between freshness and quota usage. Handle missing prices gracefully. If a symbol isn’t in the response (e.g. suspended stock), fall back to avg_cost so the position still appears with a 0% change rather than crashing.

Companies list reference

All fields returned by GET /companies

Live price ticker guide

How to keep prices updating automatically