How to Set Up NGX Stock Price Alerts with JavaScript

Build a price alert system for Nigerian Exchange (NGX) stocks using the NGN Market API. Set targets, poll every 20 minutes during market hours, and fire browser notifications or webhook calls when a condition is hit.

NGN Market

Written by NGN Market

·14 min read
How to Set Up NGX Stock Price Alerts with JavaScript

TL;DR: This guide shows you how to build a price alert system for Nigerian Exchange (NGX) stocks using the NGN Market API. You will set target prices for specific stocks, poll the API every 20 minutes during market hours, and fire browser notifications or webhook calls when a target is hit. All the code runs in the browser or Node.js with no extra dependencies.

Price alerts are one of the most requested features in any stock tracking app. The idea is simple: tell the app "notify me when DANGCEM drops below ₦1,100" and forget about it until you get the ping. For global markets there are plenty of tools that do this out of the box. For Nigerian stocks, you build it yourself.

This guide builds a complete alert system from the ground up. By the end you will have working code that monitors any NGX-listed stock, compares the live price against a target you set, and fires a notification the moment the condition is met. You can extend this to send emails, trigger webhooks, or push to a Telegram bot — the core logic stays the same.

How price alerts work

The NGN Market API refreshes NGX equity prices every 20 minutes during trading hours. That means a polling approach is the right one here. Every 20 minutes your code fetches the current price for each stock you are watching, checks whether any alert condition has been met, and fires the notification if it has.

Three types of alerts cover most use cases:

  • Price above: notify me when the stock trades above a target price
  • Price below: notify me when the stock drops below a target price
  • Percentage move: notify me when the stock moves more than X% in either direction today

All three use the same endpoint and the same polling loop. The only difference is the condition you check.

Fetch the current price for a stock

The /companies/:symbol endpoint returns the full company profile including the current price, day high, day low, and day percentage change. You need the Hobby plan for this endpoint, which starts at ₦15,000 per month and gives you 10,000 calls — enough to run a monitoring service alongside a production app without worrying about quota.

If you are on the free plan and want to monitor multiple stocks, you can use the /companies endpoint instead. It returns live price data for all NGX-listed companies in one paginated call, so you can check an entire watchlist in a single request rather than one call per stock.

Here is a function that fetches the current price for a single ticker:

const API_KEY = 'ngm_live_YOUR_KEY';

async function getStockPrice(symbol) {
  const res = await fetch(
    `https://api.ngnmarket.com/v1/companies/${symbol}`,
    { headers: { 'Authorization': `Bearer ${API_KEY}` } }
  );

  const body = await res.json();

  if (!body.success) {
    throw new Error(`Failed to fetch ${symbol}: ${body.error.message}`);
  }

  return {
    symbol:        body.data.symbol,
    name:          body.data.name,
    price:         body.data.current_price,
    changePercent: body.data.price_change_percent,
    dayHigh:       body.data.day_high,
    dayLow:        body.data.day_low,
    updatedAt:     body.data.last_updated,
  };
}

And here is the version that fetches your whole watchlist in one call, using the free /companies endpoint:

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

  const body = await res.json();
  if (!body.success) throw new Error(body.error.message);

  const all = body.data.data;

  // Return only the stocks we care about
  return all.filter(c => symbols.includes(c.symbol)).map(c => ({
    symbol:        c.symbol,
    name:          c.name,
    price:         c.price,
    changePercent: c.price_change_percent,
    dayHigh:       c.day_high,
    dayLow:        c.day_low,
    updatedAt:     c.last_updated,
  }));
}

The second approach is more quota-efficient when you are watching more than five or six stocks because it uses one API call instead of one per symbol.

Define your alerts

An alert is just an object with a symbol, a condition type, and a target value. Keep them in an array so you can add and remove them easily:

const alerts = [
  {
    id:        'alert-1',
    symbol:    'DANGCEM',
    type:      'price_above',
    target:    1200,
    triggered: false,
    createdAt: new Date().toISOString(),
  },
  {
    id:        'alert-2',
    symbol:    'GTCO',
    type:      'price_below',
    target:    55,
    triggered: false,
    createdAt: new Date().toISOString(),
  },
  {
    id:        'alert-3',
    symbol:    'MTNN',
    type:      'pct_move',
    target:    3,    // fire if day change exceeds 3% in either direction
    triggered: false,
    createdAt: new Date().toISOString(),
  },
];

The triggered flag is important. Once an alert fires, you mark it triggered so it does not fire again every time the condition is still met on the next poll. You can reset it manually if you want the alert to be repeatable.

Check the alert conditions

This function takes a stock's current data and an alert object, and returns true if the condition is met:

Advertisement

function checkCondition(stock, alert) {
  if (alert.triggered) return false;

  switch (alert.type) {
    case 'price_above':
      return stock.price >= alert.target;

    case 'price_below':
      return stock.price <= alert.target;

    case 'pct_move':
      return Math.abs(stock.changePercent) >= alert.target;

    default:
      return false;
  }
}

Fire the notification

For browser-based apps, the Web Notifications API lets you push a native OS notification. The user needs to grant permission once, and after that you can fire notifications even when the tab is in the background.

async function requestNotificationPermission() {
  if (!('Notification' in window)) {
    console.warn('This browser does not support notifications');
    return false;
  }

  if (Notification.permission === 'granted') return true;

  const permission = await Notification.requestPermission();
  return permission === 'granted';
}

function fireNotification(stock, alert) {
  const messages = {
    price_above: `${stock.symbol} is trading above ₦${alert.target.toLocaleString()} — now at ₦${stock.price.toLocaleString()}`,
    price_below: `${stock.symbol} has dropped below ₦${alert.target.toLocaleString()} — now at ₦${stock.price.toLocaleString()}`,
    pct_move:    `${stock.symbol} has moved ${stock.changePercent.toFixed(2)}% today (target: ${alert.target}%)`,
  };

  const body = messages[alert.type] || `${stock.symbol} alert triggered`;

  new Notification('NGX Price Alert', {
    body,
    icon: `https://cdn.jsdelivr.net/gh/ngnmarket/ngx-logos/dist/png/${stock.symbol}.png`,
    tag:  alert.id,   // prevents duplicate notifications for the same alert
  });

  console.log(`[${new Date().toLocaleTimeString()}] Alert fired: ${body}`);
}

The polling loop

Now put it all together. This function runs on an interval, fetches fresh prices, checks every active alert, and fires notifications when conditions are met:

function isMarketOpen() {
  const now  = new Date();
  const day  = now.getUTCDay();
  const hour = now.getUTCHours();
  // NGX trades Monday to Friday, 09:00 to 16:00 WAT (08:00 to 15:00 UTC)
  return day >= 1 && day <= 5 && hour >= 8 && hour < 15;
}

async function runAlertCheck() {
  if (!isMarketOpen()) {
    console.log('Market closed — skipping check');
    return;
  }

  const watchlist = [...new Set(alerts.map(a => a.symbol))];

  let stocks;
  try {
    stocks = await getWatchlistPrices(watchlist);
  } catch (err) {
    console.error('Price fetch failed:', err.message);
    return;
  }

  for (const alert of alerts) {
    if (alert.triggered) continue;

    const stock = stocks.find(s => s.symbol === alert.symbol);
    if (!stock) continue;

    if (checkCondition(stock, alert)) {
      alert.triggered = true;
      fireNotification(stock, alert);
    }
  }

  const activeCount    = alerts.filter(a => !a.triggered).length;
  const triggeredCount = alerts.filter(a => a.triggered).length;
  console.log(`[${new Date().toLocaleTimeString()}] Checked ${stocks.length} stocks — ${activeCount} active alerts, ${triggeredCount} triggered`);
}

// Run immediately, then every 20 minutes
runAlertCheck();
const TWENTY_MINUTES = 20 * 60 * 1000;
const alertInterval  = setInterval(runAlertCheck, TWENTY_MINUTES);

// Request notification permission on page load
requestNotificationPermission();

Persisting alerts across page refreshes

The setup above keeps alerts in memory, so they disappear when the user closes the tab. For a real app you want to save them to localStorage so they survive page refreshes:

const STORAGE_KEY = 'ngx_price_alerts';

function saveAlerts(alertList) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(alertList));
}

function loadAlerts() {
  const stored = localStorage.getItem(STORAGE_KEY);
  return stored ? JSON.parse(stored) : [];
}

function addAlert(symbol, type, target) {
  const alert = {
    id:        `alert-${Date.now()}`,
    symbol:    symbol.toUpperCase(),
    type,
    target:    parseFloat(target),
    triggered: false,
    createdAt: new Date().toISOString(),
  };

  const current = loadAlerts();
  current.push(alert);
  saveAlerts(current);

  return alert;
}

function removeAlert(id) {
  const current = loadAlerts().filter(a => a.id !== id);
  saveAlerts(current);
}

function resetAlert(id) {
  const current = loadAlerts().map(a =>
    a.id === id ? { ...a, triggered: false } : a
  );
  saveAlerts(current);
}

Load from storage when the page initialises:

// Replace the hardcoded alerts array with this
let alerts = loadAlerts();

// Save back whenever the array changes
// Call saveAlerts(alerts) after any add, remove, or trigger

Sending alerts via webhook (for server-side or Node.js)

If you are running this in Node.js rather than the browser, webhooks are a better delivery mechanism than browser notifications. You can point the webhook at a Slack channel, a Telegram bot, an email service, or any HTTP endpoint.

async function fireWebhook(stock, alert, webhookUrl) {
  const messages = {
    price_above: `*${stock.symbol}* crossed above ₦${alert.target.toLocaleString()} — now trading at ₦${stock.price.toLocaleString()}`,
    price_below: `*${stock.symbol}* dropped below ₦${alert.target.toLocaleString()} — now trading at ₦${stock.price.toLocaleString()}`,
    pct_move:    `*${stock.symbol}* has moved ${stock.changePercent.toFixed(2)}% today`,
  };

  const payload = {
    text:      messages[alert.type],
    symbol:    stock.symbol,
    price:     stock.price,
    target:    alert.target,
    alertType: alert.type,
    firedAt:   new Date().toISOString(),
  };

  await fetch(webhookUrl, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(payload),
  });
}

For Slack, the text field maps directly to a Slack Incoming Webhook message. For Telegram, swap it for the Bot API's sendMessage call. For email, pass the payload to Resend, SendGrid, or any transactional email service.

A simple alert management UI

Here is a minimal HTML interface for adding and viewing alerts. Pair it with the JavaScript above to get a working alert manager in one HTML file:

<div id="alert-manager">
  <h3>NGX Price Alerts</h3>

  <form id="alert-form">
    <input id="symbol-input" type="text" placeholder="Symbol e.g. ZENITHBANK" required />
    <select id="type-input">
      <option value="price_above">Price above</option>
      <option value="price_below">Price below</option>
      <option value="pct_move">% move exceeds</option>
    </select>
    <input id="target-input" type="number" step="0.01" placeholder="Target" required />
    <button type="submit">Add alert</button>
  </form>

  <ul id="alert-list"></ul>
</div>

<script>
  function renderAlertList() {
    const list    = document.getElementById('alert-list');
    const current = loadAlerts();

    list.innerHTML = current.map(a => `
      <li class="${a.triggered ? 'triggered' : 'active'}">
        <strong>${a.symbol}</strong>
        ${a.type === 'price_above' ? 'above' : a.type === 'price_below' ? 'below' : 'move ≥'}
        ${a.type === 'pct_move' ? a.target + '%' : '₦' + a.target.toLocaleString()}
        ${a.triggered ? '<span class="badge">Triggered</span>' : ''}
        <button onclick="removeAlert('${a.id}'); renderAlertList()">Remove</button>
        ${a.triggered
          ? `<button onclick="resetAlert('${a.id}'); renderAlertList()">Reset</button>`
          : ''}
      </li>
    `).join('');
  }

  document.getElementById('alert-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const symbol = document.getElementById('symbol-input').value.trim();
    const type   = document.getElementById('type-input').value;
    const target = document.getElementById('target-input').value;

    addAlert(symbol, type, target);
    renderAlertList();

    e.target.reset();
  });

  renderAlertList();
</script>

Keeping your quota in check

Each poll calls /companies once to cover the full watchlist, regardless of how many stocks you are watching. At 20-minute intervals over a seven-hour trading day, that is around 21 calls per day. Over a full month with roughly 22 trading days, the alert system uses around 460 calls. On the free plan with 3,000 calls per month, you have plenty of room for other features running alongside it.

The key habit: always check isMarketOpen() before polling. Running the check overnight or on weekends uses real quota and returns identical data to the last session close. There is nothing to alert on outside market hours.

Frequently asked questions

How quickly will I get notified after a price moves?

The NGX updates prices every 20 minutes during trading hours, so your maximum lag is 20 minutes from the moment the price moves to when you receive the alert. If you need tighter timing for a specific use case, reduce the polling interval, but keep in mind that more frequent polls will use more of your monthly API calls.

Can I set alerts for more than one condition on the same stock?

Yes. You can have multiple alerts for the same symbol in the alerts array — a price above alert, a price below alert, and a percentage move alert all on DANGCEM at the same time. Each has its own triggered flag and fires independently.

What happens to triggered alerts when the market closes?

Nothing changes automatically. A triggered alert stays triggered until you reset it. If you want alerts to auto-reset at the start of each new trading session, run a reset check when the market opens each morning:

function resetDailyAlerts() {
  const current = loadAlerts().map(a => ({
    ...a,
    triggered: false,
  }));
  saveAlerts(current);
}

Call this once when isMarketOpen() returns true for the first time each day.

Can I alert on ETF prices as well as equities?

Yes. Swap the fetch URL to /etfs and the same polling and condition logic applies. ETF prices on the NGN Market API follow the same response structure as equities.

Does this work as a background service on a server?

Yes. Replace the browser notification call with the webhook function and run the polling loop in a Node.js script with setInterval. A cron job that runs every 20 minutes during market hours is another clean option if you prefer not to keep a long-running process alive.

The NGN Market API documentation covers the full response schema for the company and companies endpoints, including all the price fields you can build additional alert types on — 52-week highs and lows, value traded thresholds, and volume spikes.

Advertisement

Advertisement