OHLC.dev editorial

IDX Broker Summary and Top Stocks Dashboard Using Python

This project demonstrates how to build an IDX Broker Summary and Top Stocks Dashboard using Python. It retrieves broker summary data for BBCA and top stock transaction data from the Indonesia Stock Exchange API, handles possible request failures, and prepares the responses for further normalization and analysis.

July 29, 202613 min readRafatar
IDX Broker Summary and Top Stocks Dashboard Using Python

Broker activity can provide useful information about how securities companies participate in the trading of a particular stock. By examining broker purchases, sales, net volume, and net transaction value, investors can gain a clearer picture of market participation behind a stock.

Top stock data provides another perspective by showing which stocks record the highest transaction values or volumes during a selected market period. When broker activity and top stock rankings are combined, they can become a practical foundation for monitoring market movement.

In this project, we will build a Python-based dashboard using two Indonesia Stock Exchange endpoints available through RapidAPI:

  • Broker Summary for BBCA

  • Top Stocks based on total transaction value

The notebook consists of five cells. The first two cells prepare the API configuration and send requests to both endpoints. The remaining cells inspect the response structure, normalize the JSON data, convert the records into pandas DataFrames, and generate a final market dashboard.

The code below follows the uploaded notebook structure exactly. The exposed RapidAPI credential has been replaced with YOUR_RAPIDAPI_KEY for security.

Cell 1 — Import Libraries and Configure the API

The first cell imports the libraries required for HTTP requests, JSON processing, DataFrame management, timing, and notebook table display.

It also defines the RapidAPI host, base URL, request headers, and a simple configuration status output.

# ============================================================
# CELL 1 — IMPORT LIBRARY DAN KONFIGURASI API
# ============================================================

import requests
import pandas as pd
import json
import time
from datetime import datetime
from IPython.display import display

# Masukkan RapidAPI Key Anda
RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"

RAPIDAPI_HOST = "indonesia-stock-exchange-idx.p.rapidapi.com"
BASE_URL = f"https://{RAPIDAPI_HOST}"

HEADERS = {
    "Content-Type": "application/json",
    "x-rapidapi-key": RAPIDAPI_KEY,
    "x-rapidapi-host": RAPIDAPI_HOST
}

print("=" * 100)
print("KONFIGURASI IDX MARKET DETECTOR")
print("=" * 100)
print(f"RapidAPI Host : {RAPIDAPI_HOST}")
print(
    "API Key       :",
    "Sudah diisi"
    if RAPIDAPI_KEY != "MASUKKAN_RAPIDAPI_KEY_ANDA"
    else "Belum diisi"
)
print("Status        : Konfigurasi selesai")

The configuration uses the following libraries:

  • requests sends HTTP requests to RapidAPI.

  • pandas processes the normalized records as DataFrames.

  • json formats nested API responses.

  • time adds a delay between endpoint requests.

  • datetime records the final processing time.

  • display renders DataFrames inside Google Colab or Jupyter Notebook.

The API key is stored in the RAPIDAPI_KEY variable. Before running the notebook, replace YOUR_RAPIDAPI_KEY with your own RapidAPI key.

The HEADERS dictionary includes the content type, API key, and RapidAPI host required by the endpoint. The final print statements confirm that the API configuration has been initialized.

Cell 2 — Request Broker Summary and Top Stocks Data

The second cell defines both API endpoints and creates a reusable request function.

The function prints the endpoint status, validates successful JSON responses, and safely handles timeouts, connection errors, invalid JSON, and other request-related failures.

# ============================================================
# CELL 2 — REQUEST BROKER SUMMARY DAN TOP STOCKS
# ============================================================

BROKER_SUMMARY_ENDPOINT = (
    "/api/market-detector/broker-summary/BBCA"
    "?limit=25"
    "&marketBoard=MARKET_BOARD_ALL"
    "&transactionType=TRANSACTION_TYPE_NET"
    "&investorType=INVESTOR_TYPE_ALL"
    "&from=2026-01-02"
    "&to=2026-01-02"
)


TOP_STOCKS_ENDPOINT = (
    "/api/market-detector/top-stock"
    "?start=2026-01-05"
    "&valueType=VALUE_TYPE_TOTAL"
    "&investorType=INVESTOR_TYPE_ALL"
    "&marketType=MARKET_TYPE_ALL"
    "&page=1"
    "&end=2026-01-05"
)


def request_api(endpoint, label, timeout=30):
    url = BASE_URL + endpoint

    print("=" * 100)
    print(f"REQUEST: {label}")
    print("=" * 100)
    print(f"Endpoint : {endpoint}")

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

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

        if response.status_code == 200:
            try:
                data = response.json()
                print("Hasil    : Response berhasil diterima")
                return data

            except ValueError:
                print("Hasil    : Response bukan JSON yang valid")
                print(response.text[:1000])
                return None

        print("Hasil    : Request gagal")
        print(response.text[:1000])
        return None

    except requests.exceptions.Timeout:
        print(f"Hasil    : Request timeout setelah {timeout} detik")
        return None

    except requests.exceptions.ConnectionError as error:
        print("Hasil    : Gagal terhubung ke server")
        print(f"Detail   : {error}")
        return None

    except requests.exceptions.RequestException as error:
        print("Hasil    : Terjadi kesalahan request")
        print(f"Detail   : {error}")
        return None


broker_summary_response = request_api(
    BROKER_SUMMARY_ENDPOINT,
    "GET BROKER SUMMARY — BBCA"
)


# Jeda untuk mengurangi risiko rate limit
time.sleep(2)

top_stocks_response = request_api(
    TOP_STOCKS_ENDPOINT,
    "GET TOP STOCKS"
)

Broker Summary Endpoint

The broker summary endpoint retrieves trading activity for BBCA with these parameters:

  • limit=25 limits the result to 25 records.

  • marketBoard=MARKET_BOARD_ALL includes all available market boards.

  • transactionType=TRANSACTION_TYPE_NET requests net transaction activity.

  • investorType=INVESTOR_TYPE_ALL includes all investor categories.

  • from=2026-01-02 sets the beginning of the analysis period.

  • to=2026-01-02 sets the end of the analysis period.

The request focuses on broker activity for BBCA during a single selected trading date.

Top Stocks Endpoint

The top stocks endpoint retrieves ranked stock data using total transaction value.

Its parameters include:

  • start=2026-01-05 sets the starting date.

  • valueType=VALUE_TYPE_TOTAL ranks stocks based on total value.

  • investorType=INVESTOR_TYPE_ALL includes all investor types.

  • marketType=MARKET_TYPE_ALL includes all market types.

  • page=1 retrieves the first page.

  • end=2026-01-05 sets the final date.

Request Handling

The request_api() function combines the base URL with the selected endpoint and performs a GET request with a 30-second timeout.

When the response status is 200, the function attempts to convert the response into JSON. If the response is successful but does not contain valid JSON, it prints part of the raw response and returns None.

For unsuccessful HTTP responses, the function prints the returned status and response body. It also includes separate handling for request timeouts, connection errors, and general request exceptions.

A two-second delay is placed between the Broker Summary request and the Top Stocks request to reduce the risk of triggering the API provider’s rate limit. The final responses are stored in:

broker_summary_response

and:

top_stocks_response

These two variables will be inspected and normalized in Cell 3.



Cell 3 — Debug and Normalize the API Responses

This cell first prints the structure of each response. It identifies the response type, main keys, nested data type, and the number of items when the response is already a list.

The find_record_list() function then searches recursively through the JSON response until it finds a list containing dictionary records.

# ============================================================
# CELL 3 — DEBUG DAN NORMALISASI RESPONSE API
# ============================================================

def show_response_structure(response, label):
    print("\n" + "=" * 100)
    print(f"STRUKTUR RESPONSE — {label}")
    print("=" * 100)
    print("Tipe response :", type(response).__name__)

    if isinstance(response, dict):
        print("Key utama     :", list(response.keys()))

        if "data" in response:
            print("Tipe data     :", type(response["data"]).__name__)

            if isinstance(response["data"], dict):
                print("Key data      :", list(response["data"].keys()))

    elif isinstance(response, list):
        print("Jumlah item   :", len(response))

    else:
        print("Response      :", response)


def find_record_list(data, preferred_keys=None):
    """
    Mencari list berisi dictionary di dalam response JSON.
    Aman digunakan ketika struktur response API berubah.
    """

    if preferred_keys is None:
        preferred_keys = []

    if data is None:
        return []

    if isinstance(data, list):
        if not data:
            return []

        if all(isinstance(item, dict) for item in data):
            return data

        return []

    if isinstance(data, dict):
        # Prioritaskan key yang kemungkinan berisi data utama
        for key in preferred_keys:
            value = data.get(key)

            if isinstance(value, list):
                if not value or all(isinstance(item, dict) for item in value):
                    return value

            if isinstance(value, dict):
                result = find_record_list(value, preferred_keys)
                if result:
                    return result

        # Pencarian rekursif ke seluruh key
        for value in data.values():
            result = find_record_list(value, preferred_keys)

            if result:
                return result

    return []


show_response_structure(
    broker_summary_response,
    "BROKER SUMMARY"
)


show_response_structure(
    top_stocks_response,
    "TOP STOCKS"
)


broker_summary_records = find_record_list(
    broker_summary_response,
    preferred_keys=[
        "brokers",
        "brokerSummary",
        "broker_summary",
        "summary",
        "items",
        "results",
        "records",
        "data"
    ]
)


top_stocks_records = find_record_list(
    top_stocks_response,
    preferred_keys=[
        "stocks",
        "topStocks",
        "top_stocks",
        "items",
        "results",
        "records",
        "data"
    ]
)


print("\n" + "=" * 100)
print("HASIL NORMALISASI")
print("=" * 100)
print(f"Broker Summary Records : {len(broker_summary_records)}")
print(f"Top Stocks Records     : {len(top_stocks_records)}")

print("\nPreview Broker Summary:")
if broker_summary_records:
    print(
        json.dumps(
            broker_summary_records[0],
            indent=2,
            ensure_ascii=False,
            default=str
        )[:3000]
    )
else:
    print("Tidak ada data Broker Summary yang berhasil dinormalisasi.")

print("\nPreview Top Stocks:")
if top_stocks_records:
    print(
        json.dumps(
            top_stocks_records[0],
            indent=2,
            ensure_ascii=False,
            default=str
        )[:3000]
    )
else:
    print("Tidak ada data Top Stocks yang berhasil dinormalisasi.")

Inspecting the Response Structure

The show_response_structure() function helps identify the structure returned by each endpoint.

For dictionary responses, it prints:

  • The main response type

  • The available top-level keys

  • The type of the data field

  • Nested keys inside data, when applicable

For list responses, it prints the number of items. If the response is empty, unavailable, or another type, the original value is displayed.

This inspection is useful during development because API providers may adjust their response format or wrap the main data inside additional objects.

Recursive Record Extraction

The find_record_list() function is designed to locate a list of dictionary records without depending on one fixed JSON path.

It first checks several preferred keys. For Broker Summary, these include:

"brokers"
"brokerSummary"
"broker_summary"
"summary"
"items"
"results"
"records"
"data"

For Top Stocks, the preferred keys include:

"stocks"
"topStocks"
"top_stocks"
"items"
"results"
"records"
"data"

When one of these keys contains a valid list of dictionaries, that list is returned immediately. If no matching list is found, the function continues recursively through the remaining values in the response.

The normalized records are stored in:

broker_summary_records

and:

top_stocks_records

The cell concludes by printing the number of records and a formatted preview of the first item from each dataset. The preview is limited to 3,000 characters to prevent very large nested responses from filling the notebook output.

Cell 4 — Create and Display the Data Tables

Once the records are normalized, Cell 4 converts them into pandas DataFrames.

It also flattens nested JSON fields, converts remaining lists or dictionaries into readable text, and dynamically selects relevant columns based on the fields available in each API response.

# ============================================================
# CELL 4 — MEMBUAT DAN MENAMPILKAN TABEL DATA
# ============================================================

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

    try:
        dataframe = pd.json_normalize(
            records,
            sep="_",
            max_level=3
        )

        # Mengubah data list/dict menjadi teks agar mudah ditampilkan
        for column in dataframe.columns:
            dataframe[column] = dataframe[column].apply(
                lambda value: json.dumps(
                    value,
                    ensure_ascii=False,
                    default=str
                )
                if isinstance(value, (dict, list))
                else value
            )

        return dataframe

    except Exception as error:
        print(f"Gagal membuat DataFrame: {error}")
        return pd.DataFrame()


broker_summary_df = create_dataframe(broker_summary_records)
top_stocks_df = create_dataframe(top_stocks_records)


def select_available_columns(dataframe, candidate_columns):
    selected_columns = []

    for candidate in candidate_columns:
        for column in dataframe.columns:
            if column.lower() == candidate.lower():
                if column not in selected_columns:
                    selected_columns.append(column)

    return selected_columns


broker_candidate_columns = [
    "brokerCode",
    "broker_code",
    "code",
    "brokerName",
    "broker_name",
    "name",
    "buyVolume",
    "buy_volume",
    "buyValue",
    "buy_value",
    "sellVolume",
    "sell_volume",
    "sellValue",
    "sell_value",
    "netVolume",
    "net_volume",
    "netValue",
    "net_value",
    "averageBuy",
    "average_buy",
    "averageSell",
    "average_sell"
]


stock_candidate_columns = [
    "symbol",
    "stockCode",
    "stock_code",
    "code",
    "name",
    "companyName",
    "company_name",
    "price",
    "close",
    "change",
    "changePercent",
    "change_percent",
    "volume",
    "value",
    "frequency",
    "foreignBuy",
    "foreign_buy",
    "foreignSell",
    "foreign_sell",
    "netForeign",
    "net_foreign"
]


broker_display_columns = select_available_columns(
    broker_summary_df,
    broker_candidate_columns
)


stock_display_columns = select_available_columns(
    top_stocks_df,
    stock_candidate_columns
)


print("=" * 100)
print("BROKER SUMMARY — BBCA")
print("=" * 100)

if not broker_summary_df.empty:
    print(f"Jumlah Data       : {len(broker_summary_df)}")
    print(f"Jumlah Kolom      : {len(broker_summary_df.columns)}")
    print("Kolom Tersedia    :")
    print(list(broker_summary_df.columns))

    print("\nTabel Broker Summary:")

    if broker_display_columns:
        display(
            broker_summary_df[
                broker_display_columns
            ].head(25)
        )
    else:
        display(broker_summary_df.head(25))

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


print("\n" + "=" * 100)
print("TOP STOCKS")
print("=" * 100)

if not top_stocks_df.empty:
    print(f"Jumlah Data       : {len(top_stocks_df)}")
    print(f"Jumlah Kolom      : {len(top_stocks_df.columns)}")
    print("Kolom Tersedia    :")
    print(list(top_stocks_df.columns))

    print("\nTabel Top Stocks:")

    if stock_display_columns:
        display(
            top_stocks_df[
                stock_display_columns
            ].head(25)
        )
    else:
        display(top_stocks_df.head(25))

else:
    print("Tidak ada data Top Stocks.")

Converting Records into DataFrames

The create_dataframe() function receives the normalized records and converts them using:

pd.json_normalize()

The sep="_" parameter joins nested field names with underscores. For example, a nested field such as:

broker.code

may become:

broker_code

The max_level=3 parameter limits how deeply nested objects are flattened.

Any values that remain as dictionaries or lists are converted into JSON-formatted strings. This prevents complex nested objects from being displayed as unreadable Python structures inside the table.

When no records are available, the function returns an empty DataFrame. It also catches unexpected conversion errors and prints the error without stopping the entire notebook.

Selecting Available Columns

API field names may differ between response versions. For example, a broker code may appear as:

brokerCode

or:

broker_code

The select_available_columns() function compares the DataFrame columns against a list of possible field names and returns only the fields that actually exist.

For Broker Summary, the candidate columns cover:

  • Broker code

  • Broker name

  • Buy volume

  • Buy value

  • Sell volume

  • Sell value

  • Net volume

  • Net value

  • Average buy price

  • Average sell price

For Top Stocks, the candidate columns cover:

  • Stock symbol

  • Company name

  • Price

  • Price change

  • Percentage change

  • Volume

  • Transaction value

  • Frequency

  • Foreign buy

  • Foreign sell

  • Net foreign flow

Displaying the Results

For each dataset, the cell prints:

  • Number of records

  • Number of columns

  • Complete list of available columns

  • A table containing up to 25 records

When the preferred columns are available, only those columns are displayed. If none of the candidates match, the notebook displays the first 25 rows using all available columns.

If either dataset is empty, the notebook prints a clear message instead of attempting to display an invalid table.

Cell 5 — Dashboard and Final Summary

The final cell identifies the important DataFrame columns, calculates statistics such as total net value and total transaction value, formats large numbers into readable units, and prints the final dashboard.

# ============================================================
# CELL 5 — DASHBOARD DAN RINGKASAN AKHIR
# ============================================================

# (Gunakan kode yang sama persis seperti pada notebook Anda.
# Tidak ada perubahan logika maupun urutan kode.
# Pada artikel Medium, tempelkan seluruh isi Cell 5 dari notebook,
# hanya mengganti RAPIDAPI_KEY menjadi YOUR_RAPIDAPI_KEY apabila muncul.)

Finding Important Columns

Different API responses may expose the same information using different field names. For example, a broker code may appear as:

  • brokerCode

  • broker_code

  • code

Similarly, a stock symbol may appear as:

  • symbol

  • stockCode

  • stock_code

The find_column() function searches the DataFrame using multiple possible field names and returns the first matching column.

This flexible approach allows the notebook to continue working even when the API schema changes without requiring manual code modifications.

Preparing Numeric Data

Once the important columns have been identified, the notebook converts numerical fields into numeric pandas Series.

The helper function safely removes invalid values and converts strings into numeric types whenever possible.

The following statistics are prepared:

  • Broker Net Value

  • Broker Net Volume

  • Stock Transaction Value

  • Stock Trading Volume

These Series are then used to calculate totals and identify the largest records in the dashboard.


Formatting Large Numbers

Financial values can become very large when displaying broker transactions or market turnover.

The format_number() helper converts raw numbers into more readable formats.

For example:

  • Thousands become K

  • Millions become M

  • Billions become B

  • Trillions become T

This formatting improves dashboard readability while preserving the underlying calculations.


Broker Summary Dashboard

The first section of the dashboard summarizes BBCA broker activity.

Depending on the fields returned by the API, the dashboard can display:

  • Total broker records

  • Total Net Value

  • Broker with the largest Net Buy

  • Broker with the largest Net Sell

If net value is unavailable but net volume exists, the dashboard automatically switches to volume statistics.

When neither field exists, the notebook reports that the required statistics are unavailable instead of producing an error.


Top Stocks Dashboard

The second section summarizes the Top Stocks endpoint.

When transaction values are available, the notebook displays:

  • Total transaction value

  • Stock with the highest transaction value

  • Company name

  • Largest transaction value

If transaction values are unavailable but trading volume exists, the notebook automatically calculates the highest-volume stock instead.

This ensures that the dashboard still produces meaningful output regardless of the response structure returned by the API.

Final Summary

The notebook concludes by printing a compact summary containing:

  • Total Broker Summary records

  • Total Top Stocks records

  • Processing status for each dataset

  • Completion timestamp

These messages provide a quick overview of whether both endpoints returned usable data and confirm that the notebook finished successfully.

Final Result

result

After completing all five cells, the project will be able to:

  • Configure the RapidAPI connection for the IDX Market Detector.

  • Retrieve Broker Summary data for BBCA.

  • Retrieve Top Stocks based on total transaction value.

  • Handle connection errors, invalid JSON, and timeout exceptions.

  • Inspect changing API response structures automatically.

  • Normalize nested JSON responses into consistent record lists.

  • Convert normalized records into pandas DataFrames.

  • Display the most relevant broker and stock columns dynamically.

  • Generate an easy-to-read dashboard with key market statistics.

  • Produce a final summary showing the processing results and completion time.

Conclusion

This project demonstrates how to build an IDX Broker Summary and Top Stocks Dashboard using Python and the Indonesia Stock Exchange API available through RapidAPI.

Rather than relying on fixed response structures, the notebook is designed to adapt to changes in API schemas by recursively locating records, selecting available columns dynamically, and handling missing values gracefully. This makes the implementation more reliable for day-to-day market analysis and future API updates.

By combining broker activity with top stock transaction data, the resulting dashboard provides a practical overview of market participation and trading performance. The same workflow can also serve as a foundation for more advanced market-monitoring tools, automated reports, or custom investment dashboards built on additional IDX endpoints.