OHLC.dev editorialIDX

Building an IDX Market Data Dashboard with Broker Codes Forex IDR Impact and US Stocks Parent

This project demonstrates how to build an IDX Market Data Dashboard using Python and RapidAPI. The notebook retrieves Broker Codes, Forex IDR Impact, and US Stocks Parent data for GOTO and BUKA before normalizing the API responses into structured pandas DataFrames and preparing them for a final market dashboard.

August 13, 202610 min readRafatar
Building an IDX Market Data Dashboard with Broker Codes Forex IDR Impact and US Stocks Parent

Market data often comes from different sources and response structures. Combining several datasets into one workflow can make market monitoring more practical and easier to analyze.

In this project, we will use three Indonesia Stock Exchange API endpoints:

  • Broker Codes

  • Forex IDR Impact

  • US Stocks Parent

The Broker Codes endpoint retrieves broker information, Forex IDR Impact provides information about movements in the rupiah and their market impact, while US Stocks Parent retrieves parent company information for GOTO and BUKA.

The project contains five cells covering API configuration, requests, response normalization, DataFrame creation, and the final IDX Market Data Dashboard.

Cell 1 — Setup and RapidAPI Configuration

The first cell imports the required libraries and prepares the RapidAPI configuration.

# ============================================================
# CELL 1 - SETUP & KONFIGURASI RAPIDAPI
# ============================================================

import requests
import pandas as pd
import json
import time
from datetime import datetime

pd.set_option("display.max_columns", None)
pd.set_option("display.width", 200)
pd.set_option("display.max_colwidth", 100)

# ============================================================
# RAPIDAPI CONFIG
# ============================================================

RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"

BASE_URL = "https://indonesia-stock-exchange-idx.p.rapidapi.com"

HEADERS = {
    "Content-Type": "application/json",
    "x-rapidapi-host": "indonesia-stock-exchange-idx.p.rapidapi.com",
    "x-rapidapi-key": RAPIDAPI_KEY
}

print("=" * 90)
print("IDX MAIN MARKET DATA PROJECT")
print("=" * 90)
print("Endpoint:")
print("1. Broker Codes")
print("2. Forex IDR Impact")
print("3. US Stocks Parent")
print("=" * 90)

This cell imports requests for API communication, pandas for data processing, json for JSON handling, time for request delays, and datetime for the final processing timestamp.

It also configures pandas display settings and defines the RapidAPI base URL and headers used by the following cells.

Cell 2 — Request Data from Three API Endpoints

The second cell creates the reusable request_api() function and sends requests to all three endpoints.

# ============================================================
# CELL 2 - REQUEST DATA API
# ============================================================

def request_api(endpoint, params=None):
    """
    Fungsi request API dengan error handling.
    """

    url = BASE_URL + endpoint

    print("=" * 90)
    print(f"Endpoint : {endpoint}")

    try:
        response = requests.get(
            url,
            headers=HEADERS,
            params=params,
            timeout=30
        )

        print(f"Status   : {response.status_code}")

        try:
            data = response.json()
        except Exception:
            data = None

        if response.status_code == 200:
            print("Success  : True")

        elif response.status_code == 403:
            print("Success  : False")
            print("Pesan    : Forbidden / kemungkinan endpoint tidak termasuk paket API.")

        elif response.status_code == 429:
            print("Success  : False")
            print("Pesan    : Rate limit API tercapai.")

        else:
            print("Success  : False")
            print(f"Pesan    : HTTP Error {response.status_code}")

        if response.status_code != 200:
            try:
                print("Response :", response.text[:500])
            except:
                pass

        return {
            "status": response.status_code,
            "data": data,
            "url": response.url
        }

    except requests.exceptions.Timeout:
        print("Status   : TIMEOUT")
        print("Success  : False")
        print("Pesan    : Request melebihi batas waktu.")
        return {
            "status": "TIMEOUT",
            "data": None,
            "url": url
        }

    except requests.exceptions.RequestException as e:
        print("Status   : ERROR")
        print("Success  : False")
        print("Pesan    :", str(e))
        return {
            "status": "ERROR",
            "data": None,
            "url": url
        }


# ============================================================
# 1. BROKER CODES
# ============================================================

broker_result = request_api(
    "/api/main/broker-codes"
)

time.sleep(2)


# ============================================================
# 2. FOREX IDR IMPACT
# ============================================================

forex_result = request_api(
    "/api/main/forex-idr-impact"
)

time.sleep(2)


# ============================================================
# 3. US STOCKS PARENT
# ============================================================

us_parent_result = request_api(
    "/api/main/us-stocks-parent",
    params={
        "symbols": "GOTO,BUKA"
    }
)

print("=" * 90)
print("SEMUA REQUEST SELESAI")
print("=" * 90)

The request_api() function provides basic error handling for successful requests, forbidden access, rate limits, HTTP errors, timeouts, and other request failures. Each response stores the HTTP status, returned data, and request URL.

The requests are executed sequentially. First, the notebook retrieves Broker Codes, followed by Forex IDR Impact, with a two-second delay between requests. The final request retrieves US Stocks Parent information using GOTO,BUKA as the symbols parameter.

At this stage, the raw responses are stored in broker_result, forex_result, and us_parent_result. These responses will be normalized in Cell 3 before being converted into DataFrames.

Cell 3 — Normalize API Responses

The third cell processes the three API responses separately because Broker Codes, Forex IDR Impact, and US Stocks Parent can return different data structures.

# ============================================================
# CELL 3 - NORMALISASI RESPONSE API
# ============================================================

broker_raw = broker_result.get("data")
forex_raw = forex_result.get("data")
us_parent_raw = us_parent_result.get("data")


def extract_data(response):
    """
    Mengambil bagian utama dari response API.
    Aman untuk dict, list, None dan nested data.
    """

    if response is None:
        return None

    if isinstance(response, list):
        return response

    if isinstance(response, dict):

        # Format umum API:
        # {
        #   "success": true,
        #   "data": ...
        # }

        if "data" in response:
            return response["data"]

        return response

    return response


broker_data = extract_data(broker_raw)
forex_data = extract_data(forex_raw)
us_parent_data = extract_data(us_parent_raw)


# ============================================================
# BROKER CODES
# ============================================================

broker_records = []

if isinstance(broker_data, list):

    broker_records = broker_data

elif isinstance(broker_data, dict):

    # Cari kemungkinan list di dalam dict
    for key in [
        "brokers",
        "brokerCodes",
        "broker_codes",
        "items",
        "results"
    ]:
        if isinstance(broker_data.get(key), list):
            broker_records = broker_data[key]
            break

    # Jika struktur dict tunggal
    if not broker_records:
        meaningful = {
            k: v for k, v in broker_data.items()
            if not isinstance(v, (dict, list))
        }

        if meaningful:
            broker_records = [meaningful]


# ============================================================
# FOREX IDR IMPACT
# ============================================================

forex_records = []

if isinstance(forex_data, list):

    forex_records = forex_data

elif isinstance(forex_data, dict):

    # Forex IDR biasanya berupa satu object nested
    flattened_forex = pd.json_normalize(forex_data).to_dict("records")

    if flattened_forex:
        forex_records = flattened_forex


# ============================================================
# US STOCKS PARENT
# ============================================================

us_parent_records = []

if isinstance(us_parent_data, list):

    us_parent_records = us_parent_data

elif isinstance(us_parent_data, dict):

    for key in [
        "parentCompanies",
        "parent_companies",
        "companies",
        "stocks",
        "items",
        "results"
    ]:
        if isinstance(us_parent_data.get(key), list):
            us_parent_records = us_parent_data[key]
            break

    # Hindari timestamp dianggap sebagai record saham
    if not us_parent_records:

        meaningful = {
            k: v for k, v in us_parent_data.items()
            if k.lower() not in ["timestamp", "success", "message"]
        }

        if meaningful:
            if any(isinstance(v, (dict, list)) for v in meaningful.values()):
                us_parent_records = []
            else:
                us_parent_records = [meaningful]


print("=" * 90)
print("HASIL NORMALISASI")
print("=" * 90)

print("Broker Records    :", len(broker_records))
print("Forex Records     :", len(forex_records))
print("US Parent Records :", len(us_parent_records))

print("\nSTRUKTUR RESPONSE")
print("-" * 90)

print("Broker Type    :", type(broker_data).__name__)
print("Forex Type     :", type(forex_data).__name__)
print("US Parent Type :", type(us_parent_data).__name__)

if isinstance(broker_data, dict):
    print("Broker Keys    :", list(broker_data.keys()))

if isinstance(forex_data, dict):
    print("Forex Keys     :", list(forex_data.keys()))

if isinstance(us_parent_data, dict):
    print("US Parent Keys :", list(us_parent_data.keys()))

The extract_data() function safely handles None, lists, dictionaries, and responses containing a nested data object. Each dataset is then processed according to its expected structure.

For Broker Codes, the notebook searches several possible list keys. Forex IDR Impact is flattened with pd.json_normalize(), while US Stocks Parent specifically searches for parent-company collections and prevents metadata such as timestamp from being interpreted as a stock record.

Finally, the cell reports the number of normalized records, response types, and available keys for all three datasets.

Cell 4 — Create DataFrames and Preview the Data

Cell 4 converts the normalized records into pandas DataFrames and displays the available data from each endpoint.

# ============================================================
# CELL 4 - MEMBUAT DATAFRAME DAN PREVIEW
# ============================================================

def safe_dataframe(records):
    if not records:
        return pd.DataFrame()

    try:
        return pd.json_normalize(records)
    except Exception:
        try:
            return pd.DataFrame(records)
        except Exception:
            return pd.DataFrame()


broker_df = safe_dataframe(broker_records)
forex_df = safe_dataframe(forex_records)
us_parent_df = safe_dataframe(us_parent_records)


# ============================================================
# BROKER CODES
# ============================================================

print("\n" + "=" * 100)
print("BROKER CODES")
print("=" * 100)

if not broker_df.empty:

    print("Jumlah Broker :", len(broker_df))
    print("\nKolom:")
    print(list(broker_df.columns))

    display(broker_df.head(20))

else:
    print("Tidak ada data Broker Codes.")


# ============================================================
# FOREX IDR IMPACT
# ============================================================

print("\n" + "=" * 100)
print("FOREX IDR IMPACT")
print("=" * 100)

if not forex_df.empty:

    print("Jumlah Record :", len(forex_df))
    print("\nKolom:")
    print(list(forex_df.columns))

    display(forex_df.head())

else:
    print("Tidak ada data Forex IDR Impact.")


# ============================================================
# US STOCKS PARENT
# ============================================================

print("\n" + "=" * 100)
print("US STOCKS PARENT - GOTO & BUKA")
print("=" * 100)

if not us_parent_df.empty:

    print("Jumlah Record :", len(us_parent_df))
    print("\nKolom:")
    print(list(us_parent_df.columns))

    display(us_parent_df.head(20))

else:
    print("Tidak ada data US Stocks Parent.")

The safe_dataframe() function first attempts to normalize each dataset using pd.json_normalize(). If that fails, it falls back to pd.DataFrame(). If neither method succeeds, an empty DataFrame is returned instead of interrupting the notebook.

The cell then displays each dataset separately. Broker Codes shows up to 20 records, Forex IDR Impact displays its flattened record, and US Stocks Parent displays up to 20 records for GOTO and BUKA. The available column names are also printed to make the response structure easier to inspect.

At this point, broker_df, forex_df, and us_parent_df are ready to be used by the final dashboard in Cell 5.

Cell 5 — IDX Main Market Data Dashboard

# ============================================================
# CELL 5 - IDX MARKET DATA DASHBOARD
# ============================================================

def first_existing(df, columns, default="-"):

    if df is None or df.empty:
        return default

    for col in columns:
        if col in df.columns:
            value = df.iloc[0][col]

            if pd.notna(value):
                return value

    return default


print("=" * 100)
print("IDX MAIN MARKET DATA DASHBOARD")
print("=" * 100)


# ============================================================
# BROKER CODES
# ============================================================

print("\n🏦 BROKER CODES")
print("-" * 100)

if not broker_df.empty:

    print(f"Jumlah Broker : {len(broker_df)}\n")

    code_columns = [
        "code",
        "brokerCode",
        "broker_code",
        "Kode",
        "kode"
    ]

    name_columns = [
        "name",
        "brokerName",
        "broker_name",
        "Nama",
        "nama"
    ]

    for i, (_, row) in enumerate(broker_df.head(15).iterrows(), 1):

        code = "-"
        name = "-"

        for col in code_columns:
            if col in broker_df.columns and pd.notna(row[col]):
                code = row[col]
                break

        for col in name_columns:
            if col in broker_df.columns and pd.notna(row[col]):
                name = row[col]
                break

        print(f"{i:02d}. {str(code):8} | {name}")

else:
    print("Tidak ada data Broker Codes.")


# ============================================================
# FOREX IDR IMPACT
# ============================================================

print("\n💱 FOREX IDR IMPACT")
print("-" * 100)

if not forex_df.empty:

    symbol = first_existing(
        forex_df,
        [
            "forex.symbol",
            "symbol"
        ]
    )

    name = first_existing(
        forex_df,
        [
            "forex.name",
            "name"
        ]
    )

    price = first_existing(
        forex_df,
        [
            "forex.price",
            "price",
            "currentPrice"
        ]
    )

    change = first_existing(
        forex_df,
        [
            "forex.change",
            "change"
        ]
    )

    change_percent = first_existing(
        forex_df,
        [
            "forex.changePercent",
            "changePercent",
            "change_percent"
        ]
    )

    idr_strengthening = first_existing(
        forex_df,
        [
            "idrStrengthening",
            "idr_strengthening"
        ]
    )

    summary = first_existing(
        forex_df,
        [
            "summary",
            "impactDescription",
            "description"
        ]
    )

    print(f"Symbol          : {symbol}")
    print(f"Forex           : {name}")
    print(f"Price           : {price}")
    print(f"Change          : {change}")
    print(f"Change Percent  : {change_percent}")
    print(f"IDR Strengthen  : {idr_strengthening}")

    if summary != "-":
        print(f"\nSummary:")
        print(summary)

else:
    print("Tidak ada data Forex IDR Impact.")


# ============================================================
# US STOCKS PARENT
# ============================================================

print("\n🇺🇸 US STOCKS PARENT")
print("-" * 100)

if not us_parent_df.empty:

    print(f"Jumlah Data : {len(us_parent_df)}\n")

    symbol_cols = [
        "symbol",
        "stockSymbol",
        "idxSymbol",
        "code"
    ]

    parent_cols = [
        "parent",
        "parentCompany",
        "parentName",
        "company",
        "name"
    ]

    for i, (_, row) in enumerate(us_parent_df.head(10).iterrows(), 1):

        symbol = "-"
        parent = "-"

        for col in symbol_cols:
            if col in us_parent_df.columns and pd.notna(row[col]):
                symbol = row[col]
                break

        for col in parent_cols:
            if col in us_parent_df.columns and pd.notna(row[col]):
                parent = row[col]
                break

        print(f"{i:02d}. {str(symbol):8} | {parent}")

else:
    print("Tidak ada data US Stocks Parent.")


# ============================================================
# RINGKASAN
# ============================================================

print("\n" + "=" * 100)
print("RINGKASAN")
print("=" * 100)

print(f"🏦 Broker Codes       : {len(broker_df)}")
print(f"💱 Forex IDR Impact   : {len(forex_df)}")
print(f"🇺🇸 US Stocks Parent  : {len(us_parent_df)}")

print()

print(
    "Status Broker Codes      :",
    "Berhasil" if broker_result["status"] == 200 else f"Gagal ({broker_result['status']})"
)

print(
    "Status Forex IDR Impact  :",
    "Berhasil" if forex_result["status"] == 200 else f"Gagal ({forex_result['status']})"
)

print(
    "Status US Stocks Parent  :",
    "Berhasil" if us_parent_result["status"] == 200 else f"Gagal ({us_parent_result['status']})"
)

print("\n" + "-" * 100)
print(
    "Selesai diproses :",
    datetime.now().strftime("%d-%m-%Y %H:%M:%S")
)
print("=" * 100)

The first_existing() helper safely retrieves the first available value from several possible column names. This is especially useful for Forex IDR Impact, where fields may appear as nested columns such as forex.symbol, forex.price, or forex.changePercent.

The Broker Codes section displays up to 15 broker records using flexible code and name columns. The Forex IDR Impact section summarizes the currency symbol, forex name, price, change, percentage movement, IDR strengthening status, and API-provided summary when available. The US Stocks Parent section displays up to 10 records and attempts to match each IDX symbol with its parent company.

The final summary reports the number of records available for all three datasets together with the HTTP processing status of each endpoint and a completion timestamp.

Final Result

result

After executing all five cells, this project can retrieve and process three different types of IDX market data in one workflow.

The notebook can:

  • Retrieve Broker Codes from the IDX API.

  • Retrieve Forex IDR Impact information.

  • Retrieve US Stocks Parent data for GOTO and BUKA.

  • Handle HTTP errors, timeouts, and rate limits.

  • Normalize list, dictionary, and nested response structures.

  • Prevent metadata such as timestamps from being interpreted as stock records.

  • Convert normalized records into pandas DataFrames.

  • Display broker, forex, and parent company information.

  • Generate a final market dashboard.

  • Report the processing status for all three endpoints.

Conclusion

This project demonstrates how to build an IDX Market Data Dashboard using Python and RapidAPI by combining Broker Codes Forex IDR Impact and US Stocks Parent into a single workflow.

Each endpoint provides a different perspective on market information. Broker Codes helps identify securities company references, Forex IDR Impact provides insight into currency movement and its potential effect on the Indonesian market, while US Stocks Parent connects selected IDX-listed companies with their related parent-company information.

The notebook also handles different API response structures by applying dedicated normalization logic for each dataset. This makes the workflow more reliable when working with nested JSON responses or temporarily unavailable endpoints.

By combining request handling normalization DataFrame creation and dashboard output in five cells the project provides a practical foundation for building broader IDX market monitoring systems automated reports or custom financial analysis tools.