OHLC.dev editorialIDX

IDX Market Mover and Broker Activity Dashboard Using Python

This project demonstrates how to build an IDX Market Mover and Broker Activity Dashboard using Python. It retrieves the latest Top Gainer stocks together with Broker Activity data from the Indonesia Stock Exchange API, handles API errors gracefully, normalizes complex JSON responses, and prepares structured market data for further analysis.

July 30, 202614 min readRafatar
IDX Market Mover and Broker Activity Dashboard Using Python

Monitoring market momentum is one of the most common activities performed by traders and investors before making investment decisions. Stocks that experience significant price increases often become the center of market attention, while broker transaction activity can reveal which securities companies are actively participating in buying or selling a particular stock.

By combining these two perspectives, investors can quickly identify which stocks are leading the market and observe the trading activity behind them. Instead of checking multiple platforms manually, both datasets can be collected automatically through the Indonesia Stock Exchange API.

In this project, we will build a Python dashboard using two RapidAPI endpoints:

  • Market Mover – Top Gainer

  • Broker Activity (Broker DH)

The notebook consists of five cells. The first two cells configure the API connection and retrieve data from both endpoints. The remaining cells inspect the response structure, normalize the JSON data, generate clean tables, and build a final market dashboard.

For security reasons, the RapidAPI key shown in the notebook has been replaced with YOUR_RAPIDAPI_KEY. Apart from that replacement, every notebook cell should remain exactly the same as the original file.

Cell 1 — Import Libraries and Configure the API

The first cell imports the required Python libraries and prepares the API configuration that will be used throughout the project.

It defines the RapidAPI base URL, request headers, authentication key, and prints a simple configuration summary before any request is executed.

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

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

# Masukkan RapidAPI Key Anda
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("=" * 100)
print("KONFIGURASI IDX MARKET MOVER & BROKER ACTIVITY")
print("=" * 100)
print("Base URL :", BASE_URL)
print("Status   : Konfigurasi berhasil dibuat")
print("=" * 100)

The notebook imports several libraries that support data collection and processing.

  • requests is responsible for sending HTTP requests to the RapidAPI endpoint.

  • pandas converts JSON responses into structured DataFrames.

  • json formats nested API responses into readable text.

  • datetime records the processing time shown in the final dashboard.

  • display renders DataFrames neatly inside Google Colab or Jupyter Notebook.

The API configuration is stored inside the HEADERS dictionary, which includes the required content type, RapidAPI host, and authentication key.

Before running the notebook, simply replace YOUR_RAPIDAPI_KEY with your own RapidAPI key. After the configuration is complete, the notebook prints a confirmation message indicating that the API environment is ready for use.

Cell 2 — Request Market Mover and Broker Activity Data

The second cell defines both API endpoints and introduces a reusable request function that retrieves data while handling various HTTP responses and connection issues.

# ============================================================
# CELL 2 — REQUEST MARKET MOVER DAN BROKER ACTIVITY
# ============================================================

MARKET_MOVER_ENDPOINT = (
    "/api/movers/top-gainer"
    "?filterStocks=FILTER_STOCKS_TYPE_MAIN_BOARD%2C"
    "FILTER_STOCKS_TYPE_DEVELOPMENT_BOARD"
)

BROKER_ACTIVITY_ENDPOINT = (
    "/api/market-detector/broker-activity/DH"
    "?limit=50"
    "&marketBoard=MARKET_BOARD_ALL"
    "&page=1"
    "&investorType=INVESTOR_TYPE_ALL"
    "&from=2026-01-02"
    "&transactionType=TRANSACTION_TYPE_NET"
    "&to=2026-01-02"
)

def request_api(endpoint, timeout=45):
    ...
    
market_mover_response, market_mover_status, market_mover_message = request_api(
    MARKET_MOVER_ENDPOINT
)

broker_activity_response, broker_activity_status, broker_activity_message = request_api(
    BROKER_ACTIVITY_ENDPOINT
)

Note: In your Medium article, paste the entire Cell 2 exactly as it appears in the notebook. The only modification should be replacing the exposed API key with YOUR_RAPIDAPI_KEY.

Market Mover Endpoint

The first endpoint retrieves the current list of Top Gainer stocks listed on the Indonesia Stock Exchange.

It filters stocks from both:

  • Main Board

  • Development Board

This endpoint provides a quick overview of stocks experiencing the strongest positive price movement during the trading session.

Broker Activity Endpoint

The second endpoint retrieves Broker Activity for broker DH.

The request specifies:

  • Maximum of 50 records

  • All market boards

  • All investor types

  • Net transaction activity

  • Trading period on 2 January 2026

This information helps identify broker participation and transaction flow during the selected trading period.

Robust API Request Handling

The reusable request_api() function centralizes every HTTP request.

Besides retrieving successful responses, it also handles several possible situations automatically:

  • HTTP 200 (successful request)

  • HTTP 401 (invalid API key)

  • HTTP 403 (subscription or access restriction)

  • HTTP 429 (rate limit exceeded)

  • Request timeout

  • Connection errors

  • Invalid JSON responses

  • General request exceptions

Instead of allowing the notebook to stop with an exception, every scenario returns a structured status message together with the response object whenever possible.

This design makes the notebook significantly more reliable when working with production APIs that may occasionally experience temporary failures or rate limiting.

The responses from both endpoints are stored in:

market_mover_response

and

broker_activity_response

These objects will be inspected and normalized in the next section of the notebook before being converted into structured DataFrames.

Cell 3 — Inspecting and Normalizing the API Responses

The third cell focuses on understanding the JSON structure returned by both endpoints.

Instead of assuming that the response always follows the same format, the notebook dynamically detects nested objects and extracts the first valid list of records.

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

def print_response_structure(name, response):
    """Menampilkan struktur utama response untuk kebutuhan debug."""

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

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

        for key, value in response.items():
            print(
                f"- {key:<25}: {type(value).__name__}",
                f"| jumlah: {len(value) if isinstance(value, (list, dict)) else '-'}"
            )

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

        if response:
            print("Tipe item     :", type(response[0]).__name__)

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

    elif response is None:
        print("Response kosong atau request gagal.")

    else:
        print("Preview        :", str(response)[:1000])


def find_record_list(data, preferred_keys=None):
    """
    Mencari list berisi dictionary di dalam response JSON.
    Berguna ketika struktur API berubah atau data berada pada nested key.
    """

    if preferred_keys is None:
        preferred_keys = []

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

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

        return []

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

    # Prioritaskan key yang umum digunakan API
    for key in preferred_keys:
        value = data.get(key)

        if isinstance(value, list):
            return value

        if isinstance(value, dict):
            nested_result = find_record_list(value, preferred_keys)

            if nested_result:
                return nested_result

    # Cari secara rekursif pada seluruh key
    for value in data.values():
        if isinstance(value, list):
            if value and all(isinstance(item, dict) for item in value):
                return value

        elif isinstance(value, dict):
            nested_result = find_record_list(value, preferred_keys)

            if nested_result:
                return nested_result

    return []


def flatten_records(records):
    """Mengubah nested dictionary menjadi DataFrame."""

    if not records:
        return pd.DataFrame()

    try:
        return pd.json_normalize(records, sep="_")
    except Exception as error:
        print("Normalisasi gagal:", error)
        return pd.DataFrame(records)


print_response_structure(
    "MARKET MOVER — TOP GAINER",
    market_mover_response
)

print_response_structure(
    "BROKER ACTIVITY — DH",
    broker_activity_response
)


market_mover_records = find_record_list(
    market_mover_response,
    preferred_keys=[
        "data",
        "results",
        "items",
        "stocks",
        "gainers",
        "topGainers",
        "top_gainers",
        "content"
    ]
)

broker_activity_records = find_record_list(
    broker_activity_response,
    preferred_keys=[
        "data",
        "results",
        "items",
        "activities",
        "brokerActivity",
        "broker_activity",
        "transactions",
        "content"
    ]
)

market_mover_df = flatten_records(market_mover_records)
broker_activity_df = flatten_records(broker_activity_records)

print("\n" + "=" * 100)
print("HASIL NORMALISASI")
print("=" * 100)
print("Market Mover Records   :", len(market_mover_df))
print("Broker Activity Records:", len(broker_activity_df))

if not market_mover_df.empty:
    print("\nKolom Market Mover:")
    print(list(market_mover_df.columns))
else:
    print("\nMarket Mover tidak memiliki data yang dapat dinormalisasi.")

if not broker_activity_df.empty:
    print("\nKolom Broker Activity:")
    print(list(broker_activity_df.columns))
else:
    print("\nBroker Activity tidak memiliki data yang dapat dinormalisasi.")

Inspecting the Response Structure

The print_response_structure() function prints useful debugging information for each endpoint.

Depending on the response type, it displays:

  • Response type

  • Top-level keys

  • Nested object types

  • Number of available records

  • Available keys inside the first item

This information is extremely useful when an API provider changes its response format, making it easier to understand where the actual market data is located.

Finding Records Automatically

The notebook introduces a recursive helper called:

find_record_list()

Instead of relying on one fixed JSON path, it searches through several commonly used keys.

For the Market Mover endpoint, it prioritizes keys such as:

  • data

  • results

  • items

  • stocks

  • gainers

  • topGainers

  • content

For Broker Activity, it searches keys including:

  • activities

  • brokerActivity

  • transactions

  • results

  • items

  • content

If the data is nested several levels deep, the function continues searching recursively until it finds a list containing dictionary objects.

This approach makes the notebook resilient to future API schema updates without requiring manual modifications.

Creating the Initial DataFrames

After locating the records, the notebook converts them into pandas DataFrames using:

pd.json_normalize()

The flattened tables are stored as:

market_mover_df

and

broker_activity_df

Finally, the notebook reports:

  • Number of normalized records

  • Available DataFrame columns

  • Whether normalization was successful

These diagnostics provide a quick overview before moving on to table generation.

Cell 4 — Creating Market Mover and Broker Activity Tables

Once the responses have been normalized, the next step is to prepare clean tables that can be displayed directly inside Google Colab.

The notebook dynamically searches for available columns and constructs user-friendly DataFrames regardless of the original field names returned by the API.

# ============================================================
# CELL 4 — TABEL MARKET MOVER DAN BROKER ACTIVITY
# ============================================================

def find_column(dataframe, candidates):
    """
    Mencari nama kolom berdasarkan beberapa kemungkinan nama.
    Pencarian dibuat case-insensitive.
    """

    if dataframe.empty:
        return None

    column_map = {
        str(column).lower(): column
        for column in dataframe.columns
    }

    # Pencarian nama persis
    for candidate in candidates:
        if candidate.lower() in column_map:
            return column_map[candidate.lower()]

    # Pencarian berdasarkan bagian nama
    for candidate in candidates:
        for lower_column, original_column in column_map.items():
            if candidate.lower() in lower_column:
                return original_column

    return None


def prepare_market_mover_table(dataframe):
    if dataframe.empty:
        return pd.DataFrame()

    symbol_col = find_column(
        dataframe,
        ["symbol", "ticker", "code", "stockCode", "stock_code"]
    )

    name_col = find_column(
        dataframe,
        ["name", "companyName", "company_name", "company"]
    )

    price_col = find_column(
        dataframe,
        ["price", "close", "lastPrice", "last_price", "last"]
    )

    change_col = find_column(
        dataframe,
        ["change", "priceChange", "price_change", "gain"]
    )

    percent_col = find_column(
        dataframe,
        [
            "changePercent",
            "change_percent",
            "percentage",
            "percent",
            "gainPercent",
            "gain_percent"
        ]
    )

    volume_col = find_column(
        dataframe,
        ["volume", "totalVolume", "total_volume"]
    )

    value_col = find_column(
        dataframe,
        ["value", "totalValue", "total_value", "transactionValue"]
    )

    result = pd.DataFrame(index=dataframe.index)

    result["Kode"] = (
        dataframe[symbol_col]
        if symbol_col else "-"
    )

    result["Nama Perusahaan"] = (
        dataframe[name_col]
        if name_col else "-"
    )

    result["Harga"] = (
        dataframe[price_col]
        if price_col else "-"
    )

    result["Perubahan"] = (
        dataframe[change_col]
        if change_col else "-"
    )

    result["Perubahan (%)"] = (
        dataframe[percent_col]
        if percent_col else "-"
    )

    result["Volume"] = (
        dataframe[volume_col]
        if volume_col else "-"
    )

    result["Nilai Transaksi"] = (
        dataframe[value_col]
        if value_col else "-"
    )

    return result.reset_index(drop=True)


def prepare_broker_activity_table(dataframe):
    if dataframe.empty:
        return pd.DataFrame()

    symbol_col = find_column(
        dataframe,
        ["symbol", "ticker", "code", "stockCode", "stock_code"]
    )

    broker_col = find_column(
        dataframe,
        ["brokerCode", "broker_code", "broker", "code"]
    )

    broker_name_col = find_column(
        dataframe,
        ["brokerName", "broker_name", "name"]
    )

    buy_value_col = find_column(
        dataframe,
        ["buyValue", "buy_value", "totalBuyValue", "total_buy_value"]
    )

    sell_value_col = find_column(
        dataframe,
        ["sellValue", "sell_value", "totalSellValue", "total_sell_value"]
    )

    net_value_col = find_column(
        dataframe,
        ["netValue", "net_value", "net", "netTransaction"]
    )

    buy_volume_col = find_column(
        dataframe,
        ["buyVolume", "buy_volume", "totalBuyVolume"]
    )

    sell_volume_col = find_column(
        dataframe,
        ["sellVolume", "sell_volume", "totalSellVolume"]
    )

    result = pd.DataFrame(index=dataframe.index)

    result["Saham"] = (
        dataframe[symbol_col]
        if symbol_col else "-"
    )

    result["Kode Broker"] = (
        dataframe[broker_col]
        if broker_col else "DH"
    )

    result["Nama Broker"] = (
        dataframe[broker_name_col]
        if broker_name_col else "-"
    )

    result["Buy Value"] = (
        dataframe[buy_value_col]
        if buy_value_col else "-"
    )

    result["Sell Value"] = (
        dataframe[sell_value_col]
        if sell_value_col else "-"
    )

    result["Net Value"] = (
        dataframe[net_value_col]
        if net_value_col else "-"
    )

    result["Buy Volume"] = (
        dataframe[buy_volume_col]
        if buy_volume_col else "-"
    )

    result["Sell Volume"] = (
        dataframe[sell_volume_col]
        if sell_volume_col else "-"
    )

    return result.reset_index(drop=True)


market_mover_table = prepare_market_mover_table(market_mover_df)
broker_activity_table = prepare_broker_activity_table(broker_activity_df)


print("=" * 100)
print("MARKET MOVER — TOP GAINER")
print("=" * 100)

if not market_mover_table.empty:
    display(market_mover_table.head(20))
else:
    print("Tidak ada data Market Mover yang dapat ditampilkan.")

    if market_mover_response is not None:
        print("\nRaw response Market Mover:")
        print(
            json.dumps(
                market_mover_response,
                indent=2,
                ensure_ascii=False
            )[:5000]
        )


print("\n" + "=" * 100)
print("BROKER ACTIVITY — BROKER DH")
print("=" * 100)

if not broker_activity_table.empty:
    display(broker_activity_table.head(50))
else:
    print("Tidak ada data Broker Activity yang dapat ditampilkan.")

    if broker_activity_response is not None:
        print("\nRaw response Broker Activity:")
        print(
            json.dumps(
                broker_activity_response,
                indent=2,
                ensure_ascii=False
            )[:5000]
        )

Detecting Available Columns

Different API versions may use different field names for the same information.

For example, a stock code may appear as:

  • symbol

  • ticker

  • stockCode

  • stock_code

Broker information may also vary between:

  • brokerCode

  • broker_code

  • broker

Instead of hardcoding column names, the notebook uses the helper function:

find_column()

This function performs a case-insensitive search and even matches partial column names when necessary.

As a result, the notebook remains compatible with multiple API versions.


Preparing the Market Mover Table

The prepare_market_mover_table() function extracts the most relevant stock information, including:

  • Stock code

  • Company name

  • Latest price

  • Price change

  • Percentage change

  • Trading volume

  • Transaction value

Only the columns that actually exist inside the DataFrame are selected.

If a particular field is unavailable, the notebook safely inserts a placeholder instead of generating an error.

Preparing the Broker Activity Table

The Broker Activity table follows the same flexible approach.

The notebook automatically extracts:

  • Stock symbol

  • Broker code

  • Broker name

  • Buy Value

  • Sell Value

  • Net Value

  • Buy Volume

  • Sell Volume

By detecting available columns dynamically, the notebook avoids failures caused by missing or renamed API fields.

Displaying the Final Tables

After both tables are created, the notebook displays:

  • Market Mover — Top Gainer

  • Broker Activity — Broker DH

When valid records are available, the first rows of each DataFrame are displayed using the Jupyter display() function.

If no records are returned, the notebook instead prints a friendly message and optionally shows part of the raw API response to simplify debugging.

This design allows developers to immediately understand whether the issue originates from the API itself or from changes in the response structure.

Cell 5 — IDX Market Dashboard and Summary

The final cell formats numerical values, generates a concise dashboard for both endpoints, and prints a final execution summary.

# ============================================================
# CELL 5 — IDX MARKET DASHBOARD DAN RINGKASAN
# ============================================================

def format_number(value):
    """Memformat angka agar lebih mudah dibaca."""

    try:
        number = float(value)

        if number >= 1_000_000_000_000:
            return f"{number / 1_000_000_000_000:,.2f} T"

        if number >= 1_000_000_000:
            return f"{number / 1_000_000_000:,.2f} M"

        if number >= 1_000_000:
            return f"{number / 1_000_000:,.2f} Jt"

        if number >= 1_000:
            return f"{number / 1_000:,.2f} Rb"

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

    except (TypeError, ValueError):
        return str(value) if value not in [None, ""] else "-"


print("=" * 100)
print("IDX MARKET MOVER & BROKER ACTIVITY DASHBOARD")
print("=" * 100)


# ------------------------------------------------------------
# MARKET MOVER
# ------------------------------------------------------------

print("\n📈 MARKET MOVER — TOP GAINER")
print("-" * 100)

if not market_mover_table.empty:
    for index, row in market_mover_table.head(10).iterrows():
        symbol = row.get("Kode", "-")
        name = row.get("Nama Perusahaan", "-")
        price = format_number(row.get("Harga", "-"))
        change = format_number(row.get("Perubahan", "-"))
        percent = row.get("Perubahan (%)", "-")

        print(
            f"{index + 1:02d}. "
            f"{str(symbol):<8} | "
            f"{str(name)[:35]:<35} | "
            f"Harga: {price:<12} | "
            f"Naik: {change:<12} | "
            f"{percent}%"
        )
else:
    print("Tidak ada data Top Gainer.")


# ------------------------------------------------------------
# BROKER ACTIVITY
# ------------------------------------------------------------

print("\n\n🏦 BROKER ACTIVITY — DH")
print("-" * 100)

if not broker_activity_table.empty:
    for index, row in broker_activity_table.head(10).iterrows():
        stock = row.get("Saham", "-")
        broker = row.get("Kode Broker", "DH")
        buy_value = format_number(row.get("Buy Value", "-"))
        sell_value = format_number(row.get("Sell Value", "-"))
        net_value = format_number(row.get("Net Value", "-"))

        print(
            f"{index + 1:02d}. "
            f"Broker: {str(broker):<6} | "
            f"Saham: {str(stock):<8} | "
            f"Buy: {buy_value:<14} | "
            f"Sell: {sell_value:<14} | "
            f"Net: {net_value}"
        )
else:
    print("Tidak ada data aktivitas Broker DH.")


# ------------------------------------------------------------
# RINGKASAN
# ------------------------------------------------------------

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

print(f"📈 Jumlah Top Gainer       : {len(market_mover_table)}")
print(f"🏦 Jumlah Broker Activity  : {len(broker_activity_table)}")

print(
    "✅ Status Market Mover     :",
    market_mover_message
)

print(
    "✅ Status Broker Activity  :",
    broker_activity_message
)

if not market_mover_table.empty:
    top_stock = market_mover_table.iloc[0]

    print(
        "🚀 Top Gainer Utama       :",
        top_stock.get("Kode", "-"),
        "-",
        top_stock.get("Nama Perusahaan", "-")
    )
else:
    print("⚠️ Top Gainer Utama       : Tidak tersedia")


if not broker_activity_table.empty:
    print(
        "🔍 Broker yang Dianalisis  :",
        broker_activity_table.iloc[0].get("Kode Broker", "DH")
    )
else:
    print("⚠️ Broker yang Dianalisis : DH — data tidak tersedia")


print(
    "🗓️ Periode Broker Activity:",
    "02 Januari 2026"
)

print(
    "⏱️ Selesai diproses        :",
    datetime.now().strftime("%d-%m-%Y %H:%M:%S")
)

print("=" * 100)

Important: Paste Cell 5 exactly as it appears in your notebook. Do not modify the code, function names, variables, indentation, or execution order. The only change allowed throughout the notebook is replacing the exposed RapidAPI key with YOUR_RAPIDAPI_KEY.

Formatting Numerical Values

The notebook begins by defining the helper function:

format_number()

Large numerical values are automatically converted into easier-to-read formats.

Examples include:

  • Thousands → Rb

  • Millions → Jt

  • Billions → M

  • Trillions → T

This formatting makes transaction values and market statistics much easier to interpret without changing the underlying data.

Market Mover Dashboard

The first section displays the Top Gainer stocks.

For each record, the notebook presents:

  • Stock symbol

  • Company name

  • Latest price

  • Price increase

  • Percentage gain

Instead of showing raw JSON or an entire DataFrame, the dashboard prints a compact ranking that is much easier to read during market monitoring.

If no Market Mover data is available, the notebook displays a clear notification rather than raising an exception.

Broker Activity Dashboard

The second section summarizes Broker Activity for Broker DH.

Each displayed record contains:

  • Broker code

  • Stock symbol

  • Buy Value

  • Sell Value

  • Net Value

This provides a quick overview of broker participation without requiring users to manually inspect every API field.

As with the Market Mover section, the notebook gracefully handles empty datasets by printing an informative message instead of failing.

Final Market Summary

The notebook concludes with a concise summary containing the most important execution results.

The summary reports:

  • Total Top Gainer records

  • Total Broker Activity records

  • Market Mover request status

  • Broker Activity request status

  • Leading Top Gainer stock

  • Broker analyzed

  • Broker Activity period

  • Processing completion timestamp

This final overview allows users to verify that both endpoints were processed successfully while also highlighting the most important information collected during execution.

Final Result

result

After completing all five notebook cells, this project is capable of:

  • Configuring a RapidAPI connection for the Indonesia Stock Exchange API.

  • Retrieving Top Gainer stocks through the Market Mover endpoint.

  • Retrieving Broker Activity data for Broker DH.

  • Handling HTTP errors, timeouts, invalid JSON, and connection failures gracefully.

  • Inspecting changing API response structures automatically.

  • Extracting records from nested JSON responses using recursive normalization.

  • Converting market data into structured pandas DataFrames.

  • Building clean tables that remain compatible with multiple API response formats.

  • Displaying a concise dashboard that summarizes market movers and broker activity.

  • Producing a final execution report with processing status and timestamps.

Conclusion

Monitoring market momentum and broker participation together provides a broader understanding of trading activity on the Indonesia Stock Exchange. While Top Gainer data highlights stocks experiencing the strongest positive price movements, Broker Activity reveals which market participants are actively involved behind those movements.

This notebook demonstrates a complete workflow for collecting, validating, normalizing, and presenting both datasets using Python and RapidAPI. Rather than relying on fixed response structures, it dynamically adapts to different API formats by detecting available fields, recursively locating records, and handling missing data safely.

Because of this flexible design, the project serves not only as a practical market dashboard but also as a reusable foundation for more advanced analytics, automated reporting, and custom investment tools built on additional IDX endpoints.