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_responseand
broker_activity_responseThese 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:
dataresultsitemsstocksgainerstopGainerscontent
For Broker Activity, it searches keys including:
activitiesbrokerActivitytransactionsresultsitemscontent
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_dfand
broker_activity_dfFinally, 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:
symboltickerstockCodestock_code
Broker information may also vary between:
brokerCodebroker_codebroker
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

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.
