The IDX Insider and Whale Activity Dashboard provides a practical way to examine insider activity and large transactions in Indonesian stocks using Python.
Instead of relying on a single dataset, this project combines three different IDX API endpoints.
The getInsiderScreening endpoint screens insider activity for BBCA, BUMI, and ADRO for December 2025. The getWhaleTransactions endpoint analyzes large BBCA transactions with a minimum threshold of 500 lots. The getInsiderNetSummary endpoint summarizes insider activity for BBCA, BUMI, and ADRO between November 1 and December 31, 2025.
The project is organized into five Google Colab cells covering API configuration, data requests, response normalization, data inspection, and a final human-readable dashboard.
Cell 1 — Import Libraries API Configuration and Helper
# ============================================================
# CELL 1 — IMPORT, KONFIGURASI API & HELPER
# ============================================================
import requests
import pandas as pd
import json
import time
from datetime import datetime
# ============================================================
# RAPIDAPI CONFIG
# ============================================================
RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"
BASE_URL = "https://indonesia-stock-exchange-idx.p.rapidapi.com"
RAPIDAPI_HOST = "indonesia-stock-exchange-idx.p.rapidapi.com"
headers = {
"Content-Type": "application/json",
"x-rapidapi-host": RAPIDAPI_HOST,
"x-rapidapi-key": RAPIDAPI_KEY
}
# ============================================================
# HELPER REQUEST
# ============================================================
def api_request(name, endpoint, params=None):
"""
Melakukan request API dengan error handling.
"""
url = BASE_URL + endpoint
print("=" * 100)
print(name)
print("-" * 100)
print("Endpoint :", endpoint)
print("URL :", url)
try:
response = requests.get(
url,
headers=headers,
params=params,
timeout=30
)
print("Status :", response.status_code)
try:
result = response.json()
except Exception:
result = {
"success": False,
"message": "Response bukan JSON",
"raw": response.text[:1000]
}
success = (
response.status_code == 200
and isinstance(result, (dict, list))
)
print("Success :", success)
if isinstance(result, dict):
message = (
result.get("message")
or result.get("error")
or ("Berhasil" if success else "Request gagal")
)
else:
message = "Berhasil" if success else "Request gagal"
print("Pesan :", message)
return result
except requests.exceptions.Timeout:
print("Success : False")
print("Pesan : Request timeout")
return {
"success": False,
"message": "Request timeout",
"data": None
}
except requests.exceptions.RequestException as e:
print("Success : False")
print("Pesan :", str(e))
return {
"success": False,
"message": str(e),
"data": None
}
print("✅ Konfigurasi API dan helper berhasil dibuat.")
Cell 1 imports the required libraries, prepares the RapidAPI configuration, and defines the reusable api_request() helper.
The helper handles GET requests, HTTP status information, JSON parsing, timeouts, and request exceptions before returning the API response for later processing.
Cell 2 — Request Data from Three IDX API Endpoints
# ============================================================
# CELL 2 — REQUEST DATA DARI 3 ENDPOINT
# ============================================================
# ============================================================
# 1. getInsiderScreening
# ============================================================
insider_screening_raw = api_request(
"getInsiderScreening",
"/api/analysis/insider-screening",
params={
"symbols": "BBCA,BUMI,ADRO",
"action_type": "ACTION_TYPE_UNSPECIFIED",
"page": 1,
"limit": 100,
"source_type": "SOURCE_TYPE_UNSPECIFIED",
"period": "2025-12"
}
)
time.sleep(2)
# ============================================================
# 2. getWhaleTransactions
# ============================================================
whale_transactions_raw = api_request(
"getWhaleTransactions",
"/api/analysis/whale-transactions/BBCA",
params={
"min_lot": 500
}
)
time.sleep(2)
# ============================================================
# 3. getInsiderNetSummary
# ============================================================
insider_net_raw = api_request(
"getInsiderNetSummary",
"/api/analysis/insider-net/BBCA,BUMI,ADRO",
params={
"date_start": "2025-11-01",
"date_end": "2025-12-31"
}
)
print("\n" + "=" * 100)
print("SEMUA REQUEST SELESAI")
print("=" * 100)
Cell 2 sends requests to all three endpoints.
Insider Screening analyzes BBCA, BUMI, and ADRO with a December 2025 period and a maximum of 100 results. Whale Transactions focuses specifically on BBCA with min_lot=500, while Insider Net Summary covers BBCA, BUMI, and ADRO from November 1 through December 31, 2025.
A two-second delay is also placed between the first and second and between the second and third API requests.
Cell 3 — Normalize the API Responses
# ============================================================
# CELL 3 — NORMALISASI RESPONSE API
# ============================================================
def extract_data(response):
"""
Mengambil data dari berbagai kemungkinan struktur response API.
"""
if response is None:
return []
# Response langsung berupa list
if isinstance(response, list):
return response
if not isinstance(response, dict):
return []
data = response.get("data")
if data is None:
return []
# ========================================================
# DATA = LIST
# ========================================================
if isinstance(data, list):
return data
# ========================================================
# DATA = DICT
# ========================================================
if isinstance(data, dict):
# Beberapa kemungkinan wrapper API
possible_keys = [
"items",
"results",
"records",
"transactions",
"insiders",
"screening",
"summaries",
"summary",
"data"
]
for key in possible_keys:
value = data.get(key)
if isinstance(value, list):
return value
# Kalau tidak ada list tetapi dict berisi data penting,
# tetap jadikan satu record.
return [data]
return []
def safe_dataframe(records):
"""
Mengubah hasil normalisasi menjadi DataFrame
tanpa menyebabkan error pada nested object.
"""
if not records:
return pd.DataFrame()
try:
return pd.json_normalize(records, sep=".")
except Exception:
try:
return pd.DataFrame(records)
except Exception:
return pd.DataFrame()
# ============================================================
# EXTRACT
# ============================================================
insider_screening_data = extract_data(insider_screening_raw)
whale_transactions_data = extract_data(whale_transactions_raw)
insider_net_data = extract_data(insider_net_raw)
# ============================================================
# DATAFRAME
# ============================================================
insider_screening_df = safe_dataframe(insider_screening_data)
whale_transactions_df = safe_dataframe(whale_transactions_data)
insider_net_df = safe_dataframe(insider_net_data)
# ============================================================
# HASIL
# ============================================================
print("=" * 100)
print("HASIL NORMALISASI")
print("=" * 100)
print(
f"Insider Screening Records : {len(insider_screening_df)}"
)
print(
f"Whale Transactions Records: {len(whale_transactions_df)}"
)
print(
f"Insider Net Records : {len(insider_net_df)}"
)
print("\n" + "=" * 100)
print("STRUKTUR RESPONSE")
print("=" * 100)
for name, raw, df in [
(
"Insider Screening",
insider_screening_raw,
insider_screening_df
),
(
"Whale Transactions",
whale_transactions_raw,
whale_transactions_df
),
(
"Insider Net Summary",
insider_net_raw,
insider_net_df
)
]:
print(f"\n{name}:")
print("Tipe :", type(raw).__name__)
if isinstance(raw, dict):
print("Keys :", list(raw.keys()))
if not df.empty:
print("Columns :", list(df.columns))
Cell 3 normalizes different response structures instead of assuming that every endpoint returns data in exactly the same format.
The extract_data() helper checks common wrappers such as items, results, records, transactions, insiders, screening, summaries, and summary. The resulting records are then converted into pandas DataFrames with safe_dataframe().
The cell finishes by displaying record counts, response types, available keys, and DataFrame columns for the three datasets.
Cell 4 — Preview Data and Inspect the Structure
# ============================================================
# CELL 4 — PREVIEW DATA & PEMERIKSAAN STRUKTUR
# ============================================================
def preview_dataset(title, df, raw, max_rows=10):
print("\n" + "=" * 100)
print(title)
print("=" * 100)
if not df.empty:
print("Jumlah Record :", len(df))
print("Jumlah Kolom :", len(df.columns))
print("\nKolom tersedia:")
print(list(df.columns))
print("\nPreview data:")
display(df.head(max_rows))
else:
print("⚠️ Tidak terdapat record yang dapat dinormalisasi.")
print("\nResponse API:")
try:
print(
json.dumps(
raw,
indent=2,
ensure_ascii=False,
default=str
)[:5000]
)
except Exception:
print(raw)
# ============================================================
# PREVIEW
# ============================================================
preview_dataset(
"👤 INSIDER SCREENING — BBCA, BUMI, ADRO",
insider_screening_df,
insider_screening_raw
)
preview_dataset(
"🐋 WHALE TRANSACTIONS — BBCA",
whale_transactions_df,
whale_transactions_raw
)
preview_dataset(
"📊 INSIDER NET SUMMARY — BBCA, BUMI, ADRO",
insider_net_df,
insider_net_raw
)
Cell 4 provides a more detailed inspection of each dataset.
When normalized records are available, the notebook reports the number of records, available columns, and a preview of up to ten rows. If normalization produces no records, the original API response is displayed instead.
Cell 5 — Build the IDX Insider and Whale Activity Dashboard
# ============================================================
# CELL 5 — IDX INSIDER & WHALE ACTIVITY DASHBOARD
# ============================================================
from datetime import datetime
import pandas as pd
import json
# ============================================================
# HELPER
# ============================================================
def api_status(raw):
if raw is None:
return "Tidak Ada Data"
if isinstance(raw, dict):
if raw.get("success") is False:
return "Gagal"
return "Berhasil"
def get_api_data(raw):
"""
Mengambil bagian data dari response API.
"""
if raw is None:
return None
if isinstance(raw, dict):
return raw.get("data", raw)
return raw
def format_number(value):
"""
Format angka agar lebih mudah dibaca.
"""
if value is None:
return "-"
if isinstance(value, (dict, list)):
return str(value)
try:
if isinstance(value, str):
clean = (
value
.replace("Rp", "")
.replace(",", "")
.replace("%", "")
.strip()
)
num = float(clean)
else:
num = float(value)
abs_num = abs(num)
if abs_num >= 1_000_000_000_000:
return f"{num / 1_000_000_000_000:,.2f} T"
elif abs_num >= 1_000_000_000:
return f"{num / 1_000_000_000:,.2f} B"
elif abs_num >= 1_000_000:
return f"{num / 1_000_000:,.2f} M"
elif abs_num >= 1_000:
return f"{num:,.0f}"
else:
return f"{num:,.2f}"
except Exception:
return str(value)
def is_simple(value):
return not isinstance(value, (dict, list, tuple))
def clean_key(key):
"""
Membuat nama key lebih mudah dibaca.
"""
return (
str(key)
.replace("_", " ")
.replace(".", " ")
.strip()
.title()
)
def flatten_dict(data, parent_key="", result=None):
"""
Flatten dict secara rekursif.
List tidak langsung dihancurkan agar bisa diproses terpisah.
"""
if result is None:
result = {}
if not isinstance(data, dict):
return result
for key, value in data.items():
new_key = (
f"{parent_key}.{key}"
if parent_key
else str(key)
)
if isinstance(value, dict):
flatten_dict(value, new_key, result)
else:
result[new_key] = value
return result
def find_value(data, keywords, default="-"):
"""
Mencari value sampai ke nested dictionary berdasarkan keyword.
"""
if data is None:
return default
if isinstance(data, dict):
# Cari exact / partial match pada level sekarang
for key, value in data.items():
key_lower = str(key).lower()
for keyword in keywords:
keyword_lower = keyword.lower()
if (
key_lower == keyword_lower
or key_lower.endswith("." + keyword_lower)
or keyword_lower in key_lower
):
if value is not None and not isinstance(value, (dict, list)):
return value
# Recursive
for value in data.values():
if isinstance(value, dict):
found = find_value(
value,
keywords,
default=None
)
if found is not None:
return found
return default
def collect_lists(data, path="data", output=None):
"""
Mencari seluruh list di dalam nested response.
"""
if output is None:
output = []
if isinstance(data, list):
if len(data) > 0:
output.append((path, data))
for i, item in enumerate(data):
if isinstance(item, (dict, list)):
collect_lists(
item,
f"{path}[{i}]",
output
)
elif isinstance(data, dict):
for key, value in data.items():
if isinstance(value, (dict, list)):
collect_lists(
value,
f"{path}.{key}",
output
)
return output
def choose_main_list(data):
"""
Memilih list utama yang paling mungkin berisi record transaksi.
"""
lists = collect_lists(data)
if not lists:
return []
candidates = []
for path, items in lists:
if not items:
continue
dict_count = sum(
isinstance(x, dict)
for x in items
)
score = dict_count * 100 + len(items)
path_lower = path.lower()
preferred_words = [
"data",
"items",
"results",
"records",
"transactions",
"insider",
"screening",
"summary",
"whale"
]
for word in preferred_words:
if word in path_lower:
score += 10
candidates.append(
(score, path, items)
)
if not candidates:
return []
candidates.sort(
key=lambda x: x[0],
reverse=True
)
return candidates[0][2]
def compact_value(value, max_length=80):
if value is None:
return "-"
if isinstance(value, dict):
text = json.dumps(
value,
ensure_ascii=False,
default=str
)
elif isinstance(value, list):
text = ", ".join(
str(x)
for x in value[:5]
)
if len(value) > 5:
text += " ..."
else:
text = str(value)
if len(text) > max_length:
return text[:max_length] + "..."
return text
# ============================================================
# AMBIL DATA ASLI DARI RESPONSE
# ============================================================
insider_data = get_api_data(
insider_screening_raw
)
whale_data = get_api_data(
whale_transactions_raw
)
insider_net_data_raw = get_api_data(
insider_net_raw
)
# ============================================================
# CARI RECORD UTAMA
# ============================================================
insider_records = choose_main_list(
insider_data
)
whale_records = choose_main_list(
whale_data
)
insider_net_records = choose_main_list(
insider_net_data_raw
)
# Jika tidak ditemukan list,
# tetapi data berupa dict, jadikan satu record.
if not insider_records and isinstance(insider_data, dict):
insider_records = [insider_data]
if not whale_records and isinstance(whale_data, dict):
whale_records = [whale_data]
if (
not insider_net_records
and isinstance(insider_net_data_raw, dict)
):
insider_net_records = [insider_net_data_raw]
# ============================================================
# HEADER
# ============================================================
print("=" * 100)
print("IDX INSIDER & WHALE ACTIVITY DASHBOARD")
print("=" * 100)
# ============================================================
# 1. INSIDER SCREENING
# ============================================================
print("\n👤 Insider Screening — BBCA, BUMI, ADRO")
print("-" * 100)
print("Periode : Desember 2025")
print("Jumlah Data :", len(insider_records))
print(
"Status :",
api_status(insider_screening_raw)
)
if insider_records:
for i, record in enumerate(
insider_records[:20],
start=1
):
if not isinstance(record, dict):
print(
f"{i:02d}. {compact_value(record)}"
)
continue
symbol = find_value(
record,
[
"symbol",
"ticker",
"stock_code",
"code"
]
)
name = find_value(
record,
[
"insider_name",
"name",
"person_name",
"shareholder_name",
"owner_name"
]
)
action = find_value(
record,
[
"action_type",
"action",
"transaction_type",
"trade_type"
]
)
date = find_value(
record,
[
"transaction_date",
"date",
"trade_date",
"published_at",
"published_date"
]
)
shares = find_value(
record,
[
"shares",
"share",
"quantity",
"volume",
"lot"
]
)
price = find_value(
record,
[
"price",
"transaction_price"
]
)
value = find_value(
record,
[
"transaction_value",
"total_value",
"value",
"amount"
]
)
print(
f"{i:02d}. "
f"{str(symbol):8} | "
f"{str(name)[:28]:28} | "
f"{str(action):16}"
)
print(
f" Tanggal : {date}"
)
if shares != "-":
print(
f" Saham : {format_number(shares)}"
)
if price != "-":
print(
f" Harga : {format_number(price)}"
)
if value != "-":
print(
f" Nilai : {format_number(value)}"
)
else:
print(
"Tidak ada data Insider Screening "
"yang dapat ditampilkan."
)
# ============================================================
# 2. WHALE TRANSACTIONS
# ============================================================
print("\n\n🐋 Whale Transactions — BBCA")
print("-" * 100)
print("Minimum Lot : 500")
print("Jumlah Data :", len(whale_records))
print(
"Status :",
api_status(whale_transactions_raw)
)
if whale_records:
for i, record in enumerate(
whale_records[:20],
start=1
):
if not isinstance(record, dict):
print(
f"{i:02d}. {compact_value(record)}"
)
continue
time_value = find_value(
record,
[
"transaction_time",
"trade_time",
"time",
"datetime",
"timestamp"
]
)
date = find_value(
record,
[
"transaction_date",
"trade_date",
"date"
]
)
action = find_value(
record,
[
"action",
"side",
"transaction_type",
"trade_type",
"type"
]
)
price = find_value(
record,
[
"trade_price",
"average_price",
"price"
]
)
lot = find_value(
record,
[
"total_lot",
"volume_lot",
"lots",
"lot",
"quantity"
]
)
volume = find_value(
record,
[
"volume",
"share_volume",
"shares"
]
)
value = find_value(
record,
[
"transaction_value",
"trade_value",
"total_value",
"value",
"amount"
]
)
broker = find_value(
record,
[
"broker",
"broker_code",
"buyer",
"seller"
]
)
print(
f"{i:02d}. "
f"{date} {time_value}"
)
print(
f" Action : {action}"
)
if price != "-":
print(
f" Harga : {format_number(price)}"
)
if lot != "-":
print(
f" Lot : {format_number(lot)}"
)
if volume != "-":
print(
f" Volume : {format_number(volume)}"
)
if value != "-":
print(
f" Nilai : {format_number(value)}"
)
if broker != "-":
print(
f" Broker : {broker}"
)
else:
print(
"Tidak ada Whale Transaction "
"yang dapat ditampilkan."
)
# ============================================================
# 3. INSIDER NET SUMMARY
# ============================================================
print("\n\n📊 Insider Net Summary — BBCA, BUMI, ADRO")
print("-" * 100)
print(
"Periode : "
"01 November 2025 - 31 Desember 2025"
)
print(
"Jumlah Data :",
len(insider_net_records)
)
print(
"Status :",
api_status(insider_net_raw)
)
if insider_net_records:
for i, record in enumerate(
insider_net_records[:20],
start=1
):
if not isinstance(record, dict):
print(
f"{i:02d}. {compact_value(record)}"
)
continue
symbol = find_value(
record,
[
"symbol",
"ticker",
"stock_code",
"code"
]
)
buy = find_value(
record,
[
"total_buy_value",
"buy_value",
"buyvalue",
"total_buy",
"buy"
]
)
sell = find_value(
record,
[
"total_sell_value",
"sell_value",
"sellvalue",
"total_sell",
"sell"
]
)
net = find_value(
record,
[
"net_value",
"netvalue",
"net_transaction",
"net_buy_sell",
"net"
]
)
buy_count = find_value(
record,
[
"buy_count",
"total_buy_count",
"buy_transactions"
]
)
sell_count = find_value(
record,
[
"sell_count",
"total_sell_count",
"sell_transactions"
]
)
print(
f"{i:02d}. {str(symbol):8}"
)
print(
f" Total Buy : {format_number(buy)}"
)
print(
f" Total Sell : {format_number(sell)}"
)
print(
f" Net : {format_number(net)}"
)
if buy_count != "-":
print(
f" Buy Count : "
f"{format_number(buy_count)}"
)
if sell_count != "-":
print(
f" Sell Count : "
f"{format_number(sell_count)}"
)
else:
print(
"Tidak ada Insider Net Summary "
"yang dapat ditampilkan."
)
# ============================================================
# RINGKASAN
# ============================================================
print(
"\n\n" +
"=" * 100
)
print("RINGKASAN")
print("=" * 100)
print(
f"👤 Insider Screening : "
f"{len(insider_records)} record"
)
print(
f"🐋 Whale Transactions BBCA : "
f"{len(whale_records)} record"
)
print(
f"📊 Insider Net Summary : "
f"{len(insider_net_records)} record"
)
print(
"\nStatus Insider Screening :",
api_status(insider_screening_raw)
)
print(
"Status Whale Transactions:",
api_status(whale_transactions_raw)
)
print(
"Status Insider Net :",
api_status(insider_net_raw)
)
print(
"\nSelesai diproses :",
datetime.now().strftime(
"%d-%m-%Y %H:%M:%S"
)
)
Cell 5 is the main presentation layer of the project. It introduces additional helpers for status detection, number formatting, recursive value discovery, nested-list detection, and automatic selection of the most relevant transaction records.
The Insider Screening section displays available information such as stock symbol, insider name, transaction action, date, shares, price, and transaction value for BBCA, BUMI, and ADRO.
The Whale Transactions section focuses on large BBCA transactions with a minimum threshold of 500 lots. Depending on the fields returned by the API, it can display transaction time, date, action, price, lots, volume, transaction value, and broker information.
The Insider Net Summary section summarizes available buy, sell, and net values for BBCA, BUMI, and ADRO for the period from November 1 to December 31, 2025. It can also display buy and sell transaction counts when those fields are available.
Final Result
The completed IDX Insider and Whale Activity Dashboard combines three perspectives that can help organize insider and large-transaction data from the Indonesian stock market.
Insider Screening provides a structured view of insider-related activity across BBCA, BUMI, and ADRO.
Whale Transactions focuses on large BBCA transactions using a minimum threshold of 500 lots.
Insider Net Summary provides a consolidated view of available insider buy, sell, and net activity for BBCA, BUMI, and ADRO.
The dashboard finishes by displaying record counts for all three datasets, individual API statuses, and the processing timestamp.
Conclusion
The IDX Insider and Whale Activity Dashboard demonstrates how Python and RapidAPI can be used to combine Insider Screening, Whale Transactions, and Insider Net Summary data within a single five-cell Google Colab workflow.
The project does more than simply request API data. It handles different response structures, normalizes nested records, inspects the original responses, identifies relevant transaction lists, formats large numerical values, and converts the results into a more readable dashboard.
With its reusable structure, the same workflow can also serve as a foundation for further analysis of insider activity, large transactions, and market behavior across other Indonesian stocks.
