OHLC.dev editorialIDX

Build an IDX Trending Stocks and US Stocks Parent Dashboard Using Python

This project demonstrates how to build an IDX Trending Stocks and US Stocks Parent Dashboard using Python. The notebook connects to the Indonesia Stock Exchange API through RapidAPI, retrieves trending stock information together with US Stocks Parent data, normalizes different JSON response structures, converts them into structured DataFrames, and produces a comprehensive dashboard for monitoring market momentum and international company relationships.

July 20, 202611 min readRafatar
Build an IDX Trending Stocks and US Stocks Parent Dashboard Using Python

Monitoring trending stocks helps investors quickly identify companies attracting significant market attention. At the same time, understanding whether an Indonesian company has a related US-listed parent company provides additional context when evaluating international market influence.

The Indonesia Stock Exchange API offers dedicated endpoints that allow both datasets to be retrieved automatically. Rather than checking multiple sources manually, developers can integrate both endpoints into a single analytical workflow.

In this tutorial, we build an IDX Trending Stocks and US Stocks Parent Dashboard using Python. The notebook authenticates through RapidAPI, retrieves trending stock data together with US Stocks Parent information, normalizes different JSON response structures, converts the responses into structured pandas DataFrames, and generates a concise dashboard summarizing both datasets for daily market monitoring.

The notebook is organized into five sections covering API configuration, secure API requests, JSON normalization, data presentation, and dashboard generation.


CELL 1 — Import Library & Konfigurasi API

import requests
import pandas as pd
from IPython.display import display

# ==========================================================
# RAPID API CONFIG
# ==========================================================

API_KEY = "YOUR_RAPIDAPI_KEY"

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

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

Catatan: API Key asli pada file diganti menjadi YOUR_RAPIDAPI_KEY agar kredensial RapidAPI tidak dipublikasikan di artikel. Struktur kode lainnya tetap mengikuti file.

Explanation

The first cell prepares the Python environment and configures the connection to the Indonesia Stock Exchange API through RapidAPI.

The notebook imports three components:

  • requests for sending HTTP requests to the API,

  • pandas for converting JSON responses into structured DataFrames,

  • display from IPython.display for presenting the resulting tables inside Google Colab or Jupyter Notebook.

The API configuration is then stored in three variables.

API_KEY contains the RapidAPI credential, while BASE_URL stores the main address of the Indonesia Stock Exchange API. The headers dictionary combines the API key, RapidAPI host, and JSON content type required by every request.

By defining these values at the beginning of the notebook, the following cells can reuse the same configuration without repeating authentication details.

CELL 2 — Request API

def request_api(endpoint):
    url = BASE_URL + endpoint

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

        print(f"\nEndpoint : {endpoint}")
        print("Status   :", response.status_code)

        if response.status_code == 200:
            return response.json()

        elif response.status_code == 429:
            print("⚠️ Rate Limit RapidAPI.")
            return None

        else:
            print(response.text[:500])
            return None

    except Exception as e:
        print("❌ Error :", e)
        return None


# ==========================================================
# REQUEST
# ==========================================================

trending_response = request_api("/api/main/trending")

parent_response = request_api(
    "/api/main/us-stocks-parent?symbols=GOTO,BUKA"
)

Explanation

The second cell creates the API request function and retrieves data from both endpoints used by the project.

The request_api() function receives an endpoint path and joins it with BASE_URL to produce the complete request URL.

The request is sent using requests.get() with the headers created in Cell 1 and a timeout limit of 30 seconds. For every request, the function prints the endpoint and HTTP status code so users can immediately see whether the API call succeeded.

When the API returns HTTP status 200, the response is converted into JSON and returned to the notebook.

When the API returns status 429, the notebook prints a warning that the RapidAPI rate limit has been reached and returns None.

Other unsuccessful status codes display the first 500 characters of the response body. This helps reveal messages related to invalid credentials, unavailable endpoints, subscription restrictions, or server errors.

The try-except block also prevents unexpected connection problems from terminating the notebook.

After the function is defined, the notebook sends two requests.

The first request accesses:

/api/main/trending

Its result is stored in trending_response.

The second request accesses:

/api/main/us-stocks-parent?symbols=GOTO,BUKA

This endpoint requests US Stocks Parent information for the Indonesian stock symbols GOTO and BUKA. Its result is stored in parent_response.

CELL 3 — Normalisasi Data

def normalize(data):

    if data is None:
        return []

    if isinstance(data, list):
        return data

    if isinstance(data, dict):

        for key in [
            "data",
            "results",
            "stocks",
            "items",
            "list",
            "trending"
        ]:

            value = data.get(key)

            if isinstance(value, list):
                return value

        return [data]

    return []


trending = normalize(trending_response)
parents = normalize(parent_response)

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

print("Trending Records :", len(trending))
print("US Parent Records:", len(parents))

print("\nDEBUG")

print("Trending Type :", type(trending_response).__name__)
print("US Parent Type:", type(parent_response).__name__)

Explanation

The third cell standardizes the two API responses before they are converted into DataFrames.

API endpoints do not always return data using the same JSON structure. One endpoint may return a list directly, while another may place the records inside a dictionary field such as data, results, or stocks.

The normalize() function handles these common variations.

When the input is None, the function returns an empty list. This prevents later cells from attempting to process an unavailable API response.

When the response is already a list, it is returned directly because it is already in the expected record format.

When the response is a dictionary, the function searches several possible wrapper keys:

  • data

  • results

  • stocks

  • items

  • list

  • trending

If one of these fields contains a list, that list is returned as the normalized dataset.

When no list is found, the complete dictionary is placed inside a list. This makes a single dictionary response compatible with pd.json_normalize() in the next cell.

The two API responses are then normalized separately:

trending = normalize(trending_response)
parents = normalize(parent_response)

Finally, the notebook prints the number of normalized records and the original response type for each endpoint.

The debug information is especially useful when an endpoint returns an unexpected structure. It allows users to determine whether the response was a dictionary, list, or None before continuing to the data-display stage.

CELL 4 — Menampilkan Data Trending Stocks dan US Stocks Parent

print("="*100)
print("TRENDING STOCKS")
print("="*100)

if trending:

    df = pd.json_normalize(trending)

    display(df)

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


print("\n")

print("="*100)
print("US STOCKS PARENT")
print("="*100)

if parents:

    df_parent = pd.json_normalize(parents)

    display(df_parent)

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

Explanation

The fourth cell converts the normalized records into pandas DataFrames and displays them inside the notebook.

The Trending Stocks section first checks whether the normalized list contains any records. When data is available, the notebook uses pd.json_normalize() to transform the JSON objects into a structured DataFrame before displaying the result. If no records are available, the notebook prints a message indicating that no trending stock data was returned.

The same workflow is then applied to the US Stocks Parent dataset. The normalized records are converted into another DataFrame and displayed. When the response is empty, the notebook reports that no parent-company data is available.

Using pd.json_normalize() allows nested JSON fields to be flattened automatically, making the data easier to inspect and analyze in later stages.

CELL 5 — Dashboard Ringkasan

import json
import pandas as pd
from IPython.display import display

# ==========================================================
# FUNGSI BANTU
# ==========================================================

def first_value(data, keys, default="-"):
    """Mengambil nilai pertama yang tersedia dari beberapa kemungkinan key."""
    if not isinstance(data, dict):
        return default

    for key in keys:
        value = data.get(key)

        if value is not None and value != "":
            return value

    return default


def find_stock_records(data, depth=0, max_depth=8):
    """
    Mencari list berisi dictionary saham di dalam respons JSON bertingkat.
    """
    if data is None or depth > max_depth:
        return []

    if isinstance(data, list):
        dict_items = [item for item in data if isinstance(item, dict)]

        # List dianggap sebagai data saham jika berisi dictionary
        if dict_items:
            return dict_items

        records = []

        for item in data:
            records.extend(
                find_stock_records(item, depth + 1, max_depth)
            )

        return records

    if isinstance(data, dict):
        preferred_keys = [
            "stocks",
            "trending",
            "results",
            "items",
            "list",
            "records",
            "companies",
            "parents",
            "us_stocks_parent",
            "usStocksParent",
            "data"
        ]

        # Prioritaskan key yang umum dipakai API
        for key in preferred_keys:
            if key in data:
                records = find_stock_records(
                    data[key],
                    depth + 1,
                    max_depth
                )

                if records:
                    return records

        # Cari pada seluruh value dictionary
        for value in data.values():
            records = find_stock_records(
                value,
                depth + 1,
                max_depth
            )

            if records:
                return records

    return []


def format_number(value):
    """Format angka agar lebih mudah dibaca."""
    if value in [None, "", "-"]:
        return "-"

    try:
        number = float(value)

        if abs(number) >= 1_000_000_000:
            return f"{number / 1_000_000_000:,.2f} B"

        if abs(number) >= 1_000_000:
            return f"{number / 1_000_000:,.2f} M"

        if abs(number) >= 1_000:
            return f"{number:,.0f}"

        return f"{number:,.2f}"

    except (TypeError, ValueError):
        return str(value)


# ==========================================================
# AMBIL RECORD LANGSUNG DARI RESPONS ASLI
# ==========================================================

trending_records = find_stock_records(trending_response)
parent_records = find_stock_records(parent_response)


# ==========================================================
# DASHBOARD
# ==========================================================

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

# ==========================================================
# TRENDING STOCKS
# ==========================================================

print("\n📈 TRENDING STOCKS")
print("-" * 100)

if trending_records:
    trending_rows = []

    for index, item in enumerate(trending_records, 1):
        symbol = first_value(
            item,
            [
                "symbol",
                "stockCode",
                "stock_code",
                "code",
                "ticker"
            ]
        )

        company = first_value(
            item,
            [
                "companyName",
                "company_name",
                "company",
                "name",
                "stockName"
            ]
        )

        price = first_value(
            item,
            [
                "price",
                "lastPrice",
                "last_price",
                "close",
                "currentPrice"
            ]
        )

        change = first_value(
            item,
            [
                "change",
                "priceChange",
                "price_change",
                "changeValue"
            ]
        )

        change_percent = first_value(
            item,
            [
                "changePercent",
                "change_percent",
                "percentage",
                "percentChange",
                "pct_change"
            ]
        )

        volume = first_value(
            item,
            [
                "volume",
                "totalVolume",
                "total_volume",
                "transactionVolume"
            ]
        )

        score = first_value(
            item,
            [
                "score",
                "trendScore",
                "trend_score",
                "rank",
                "ranking"
            ]
        )

        trending_rows.append({
            "No": index,
            "Kode": symbol,
            "Perusahaan": company,
            "Harga": format_number(price),
            "Perubahan": format_number(change),
            "Perubahan (%)": format_number(change_percent),
            "Volume": format_number(volume),
            "Score/Rank": score
        })

    df_trending_dashboard = pd.DataFrame(trending_rows)

    display(df_trending_dashboard)

    print(f"\n✅ Total saham trending: {len(trending_records)}")

else:
    print("Tidak ada record saham yang berhasil ditemukan.")

    print("\nDEBUG STRUKTUR TRENDING")
    print("-" * 100)
    print("Tipe respons:", type(trending_response).__name__)

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

    print("\nRaw response:")
    print(
        json.dumps(
            trending_response,
            indent=2,
            ensure_ascii=False,
            default=str
        )[:5000]
    )


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

print("\n\n🌎 US STOCKS PARENT — GOTO & BUKA")
print("-" * 100)

if parent_records:
    parent_rows = []

    for index, item in enumerate(parent_records, 1):
        local_symbol = first_value(
            item,
            [
                "symbol",
                "localSymbol",
                "local_symbol",
                "stockCode",
                "stock_code",
                "code"
            ]
        )

        local_company = first_value(
            item,
            [
                "companyName",
                "company_name",
                "company",
                "name",
                "localCompany"
            ]
        )

        parent_symbol = first_value(
            item,
            [
                "usSymbol",
                "us_symbol",
                "parentSymbol",
                "parent_symbol",
                "parentTicker",
                "parent_ticker",
                "ticker"
            ]
        )

        parent_company = first_value(
            item,
            [
                "usCompany",
                "us_company",
                "parentCompany",
                "parent_company",
                "parentName",
                "parent_name"
            ]
        )

        price = first_value(
            item,
            [
                "price",
                "lastPrice",
                "last_price",
                "currentPrice"
            ]
        )

        change_percent = first_value(
            item,
            [
                "changePercent",
                "change_percent",
                "percentChange",
                "percentage"
            ]
        )

        relationship = first_value(
            item,
            [
                "relationship",
                "relation",
                "correlation",
                "description"
            ]
        )

        parent_rows.append({
            "No": index,
            "Saham IDX": local_symbol,
            "Perusahaan IDX": local_company,
            "Saham AS": parent_symbol,
            "Perusahaan AS": parent_company,
            "Harga AS": format_number(price),
            "Perubahan (%)": format_number(change_percent),
            "Hubungan": relationship
        })

    df_parent_dashboard = pd.DataFrame(parent_rows)

    display(df_parent_dashboard)

    print(f"\n✅ Total data US Stocks Parent: {len(parent_records)}")

else:
    print("Tidak ada record US Stocks Parent yang berhasil ditemukan.")

    print("\nDEBUG STRUKTUR US STOCKS PARENT")
    print("-" * 100)
    print("Tipe respons:", type(parent_response).__name__)

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

    print("\nRaw response:")
    print(
        json.dumps(
            parent_response,
            indent=2,
            ensure_ascii=False,
            default=str
        )[:5000]
    )


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

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

print(f"📈 Record Trending Stocks  : {len(trending_records)}")
print(f"🌎 Record US Stocks Parent : {len(parent_records)}")

if trending_records:
    top_stock = trending_records[0]

    top_symbol = first_value(
        top_stock,
        ["symbol", "stockCode", "stock_code", "code", "ticker"]
    )

    top_company = first_value(
        top_stock,
        ["companyName", "company_name", "company", "name"]
    )

    print(f"🔥 Trending pertama        : {top_symbol} — {top_company}")
else:
    print("⚠️ Trending Stocks         : Struktur data belum dikenali")

if parent_records:
    print("✅ US Stocks Parent         : Data berhasil diproses")
else:
    print("⚠️ US Stocks Parent         : Respons kosong atau struktur belum dikenali")

Explanation

The fifth cell combines the original API responses into a single market dashboard.

Instead of relying only on the normalized DataFrames, the notebook searches directly through the original JSON responses to locate stock records regardless of their nesting level. Several helper functions are used to locate the first available value, recursively extract stock records, and format numerical values into a more readable representation.

The dashboard is divided into three main sections.

The first section summarizes Trending Stocks, displaying important information such as stock code, company name, latest price, price movement, trading volume, and trend score.

The second section presents US Stocks Parent information for the selected Indonesian companies. It highlights the relationship between IDX-listed companies and their related US-listed parent companies whenever the information is available.

Finally, the notebook prints a concise summary showing:

  • the total number of Trending Stocks records,

  • the total number of US Stocks Parent records,

  • the first trending stock detected,

  • the processing status of both API responses.

The dashboard also includes debugging information whenever the API returns an unexpected response structure, making it easier to diagnose empty or incomplete responses.

Result

After the notebook finishes running, two normalized datasets are displayed.

The first table contains the current Trending Stocks returned by the Indonesia Stock Exchange API, while the second table displays US Stocks Parent information for the selected Indonesian companies.

The notebook concludes by generating a dashboard that summarizes both datasets, highlights the first detected trending stock, reports the number of processed records, and indicates whether each API response was successfully interpreted.

Overall Workflow

The notebook follows these steps:

  1. Import the required Python libraries.

  2. Configure RapidAPI authentication.

  3. Request Trending Stocks data.

  4. Request US Stocks Parent information.

  5. Normalize different JSON response structures.

  6. Convert the responses into pandas DataFrames.

  7. Display both datasets.

  8. Extract important stock information from the original responses.

  9. Generate a market dashboard summarizing the results.

Conclusion

The IDX Trending Stocks and US Stocks Parent Dashboard demonstrates how Python can combine market momentum information with international company relationships using the Indonesia Stock Exchange API.

The notebook retrieves data from two different endpoints, normalizes varying JSON response structures, converts the results into structured DataFrames, and presents a dashboard that summarizes trending Indonesian stocks together with their related US parent companies.

By combining reusable request functions, flexible JSON normalization, DataFrame processing, and a concise dashboard, this project provides a practical foundation for building automated market-monitoring applications with the Indonesia Stock Exchange API.