Keeping track of daily corporate events while understanding sector rotation trends is essential for investors who want to monitor the Indonesian stock market more effectively. Corporate actions such as dividends, rights issues, stock splits, and RUPS schedules can directly influence individual stocks, while sector rotation helps identify which industries are attracting or losing market momentum.
In this tutorial, we'll build an IDX Sector Rotation and Today Corporate Actions Dashboard using Python and the Indonesia Stock Exchange (IDX) API available through RapidAPI.
The notebook combines two different endpoints into a single workflow. The first endpoint retrieves today's corporate actions, while the second provides sector rotation analysis. Because both endpoints may return different JSON structures, the notebook includes a robust normalization process capable of handling nested objects, lists, and varying response formats before converting the results into structured pandas DataFrames.
To keep the project modular and easy to maintain, the notebook is organized into five logical cells covering API configuration, data retrieval, response normalization, table generation, and dashboard visualization. This structure makes it easy to extend the notebook with additional IDX endpoints while preserving a consistent data processing workflow.
Cell 1 — Import Library and API Configuration
The first cell imports the required libraries and configures the RapidAPI connection. Replace YOUR_RAPIDAPI_KEY with your own RapidAPI key before running the notebook.
# ============================================================
# CELL 1 - IMPORT LIBRARY & KONFIGURASI API
# ============================================================
import requests
import pandas as pd
import json
import time
from datetime import datetime
from IPython.display import display
# ============================================================
# RAPIDAPI CONFIG
# ============================================================
RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"
BASE_URL = "https://indonesia-stock-exchange-idx.p.rapidapi.com"
HEADERS = {
"Content-Type": "application/json",
"x-rapidapi-key": RAPIDAPI_KEY,
"x-rapidapi-host": "indonesia-stock-exchange-idx.p.rapidapi.com"
}
print("=" * 90)
print("KONFIGURASI BERHASIL DIMUAT")
print("=" * 90)
if RAPIDAPI_KEY == "MASUKKAN_API_KEY_ANDA":
print("⚠️ Silakan masukkan RapidAPI Key Anda.")
else:
print("✅ RapidAPI Key sudah diisi.")
print("Base URL :", BASE_URL)Cell 2 — Request API
This cell creates a reusable API request function with timeout handling, HTTP status checks, and automatic retries for rate-limit errors. It then requests today’s corporate actions and sector rotation data.
# ============================================================
# CELL 2 - REQUEST API
# ============================================================
def get_api(endpoint, max_retry=2, delay=3):
"""
Request API dengan:
- timeout
- penanganan status HTTP
- retry otomatis ketika status 429
"""
url = BASE_URL + endpoint
for attempt in range(1, max_retry + 1):
try:
response = requests.get(
url,
headers=HEADERS,
timeout=30
)
print("=" * 90)
print("Endpoint :", endpoint)
print("Status :", response.status_code)
if response.status_code == 200:
try:
return response.json()
except ValueError:
print("Response bukan JSON yang valid.")
print(response.text[:1000])
return None
elif response.status_code == 429:
print("Keterangan: Rate limit API terlampaui.")
print(response.text[:1000])
if attempt < max_retry:
print(f"Retry setelah {delay} detik...")
time.sleep(delay)
continue
return None
else:
print("Keterangan: Request gagal.")
print(response.text[:1000])
return None
except requests.exceptions.Timeout:
print("=" * 90)
print("Endpoint :", endpoint)
print("ERROR : Request timeout.")
return None
except requests.exceptions.RequestException as error:
print("=" * 90)
print("Endpoint :", endpoint)
print("ERROR :", error)
return None
except Exception as error:
print("=" * 90)
print("Endpoint :", endpoint)
print("ERROR :", error)
return None
return None
# ============================================================
# REQUEST ENDPOINT
# ============================================================
corporate_response = get_api(
"/api/calendar/today"
)
# Jeda untuk menghindari rate limit paket BASIC
time.sleep(3)
sector_response = get_api(
"/api/analysis/retail/sector-rotation"
)Cell 3 — Data Normalization and Debugging
The third cell normalizes the different API response structures into consistent record lists. It also displays response structures and sample records for debugging.
# ============================================================
# CELL 3 - NORMALISASI & DEBUG DATA
# ============================================================
def to_records(value, category=None):
"""
Mengubah list atau dictionary menjadi list record.
"""
records = []
if value is None:
return records
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
record = item.copy()
if category and "_category" not in record:
record["_category"] = category
records.append(record)
else:
records.append({
"_category": category or "-",
"value": item
})
return records
if isinstance(value, dict):
record = value.copy()
if category and "_category" not in record:
record["_category"] = category
return [record]
return [{
"_category": category or "-",
"value": value
}]
def extract_corporate_actions(response):
"""
Struktur yang ditemukan:
response
└── data
└── data
├── bonus
├── dividend
├── economic
├── ipo
├── pubex
├── rightissue
├── rups
├── stock_reverse
├── stocksplit
├── tender
├── warrant
├── stock_dividend
└── today
"""
if not isinstance(response, dict):
return []
level_1 = response.get("data", {})
if not isinstance(level_1, dict):
return []
level_2 = level_1.get("data", {})
if not isinstance(level_2, dict):
return []
today_data = level_2.get("today")
if today_data is None:
return []
records = []
if isinstance(today_data, list):
return to_records(today_data, "today")
if isinstance(today_data, dict):
# Jika today berisi beberapa kategori
for category, value in today_data.items():
if value in [None, [], {}]:
continue
records.extend(
to_records(value, category)
)
# Jika today adalah satu record langsung
if not records and today_data:
records.extend(
to_records(today_data, "today")
)
return records
def find_sector_records(value, depth=0):
"""
Mencari list record Sector Rotation secara rekursif.
"""
if depth > 8 or value is None:
return []
if isinstance(value, list):
dict_items = [
item for item in value
if isinstance(item, dict)
]
if dict_items:
return value
return []
if not isinstance(value, dict):
return []
priority_keys = [
"sectorRotation",
"sector_rotation",
"sectors",
"rotation",
"rankings",
"items",
"results",
"records",
"data"
]
for key in priority_keys:
if key in value:
result = find_sector_records(
value.get(key),
depth + 1
)
if result:
return result
for nested_value in value.values():
result = find_sector_records(
nested_value,
depth + 1
)
if result:
return result
sector_keys = {
"sector",
"sectorName",
"sector_name",
"name",
"momentum",
"score",
"trend",
"rank",
"performance"
}
if sector_keys.intersection(value.keys()):
return [value]
return []
def extract_sector_rotation(response):
"""
Normalisasi Sector Rotation dari berbagai kemungkinan
struktur response.
"""
if response is None:
return []
if isinstance(response, list):
return response
if not isinstance(response, dict):
return []
return find_sector_records(response)
# ============================================================
# PROSES NORMALISASI
# ============================================================
corporate = extract_corporate_actions(
corporate_response
)
sector_rotation = extract_sector_rotation(
sector_response
)
print("=" * 100)
print("HASIL NORMALISASI")
print("=" * 100)
print(f"Corporate Action Records : {len(corporate)}")
print(f"Sector Rotation Records : {len(sector_rotation)}")
# ============================================================
# DEBUG CORPORATE ACTIONS
# ============================================================
print("\n" + "=" * 100)
print("DEBUG STRUKTUR RESPONSE")
print("=" * 100)
print("\nCORPORATE ACTIONS")
print("-" * 100)
print(
"Tipe response :",
type(corporate_response).__name__
)
if isinstance(corporate_response, dict):
print(
"Key utama :",
list(corporate_response.keys())
)
level_1 = corporate_response.get("data")
print(
"Tipe data :",
type(level_1).__name__
)
if isinstance(level_1, dict):
print(
"Key data :",
list(level_1.keys())
)
level_2 = level_1.get("data")
print(
"Tipe data.data:",
type(level_2).__name__
)
if isinstance(level_2, dict):
print(
"Key data.data :",
list(level_2.keys())
)
today_data = level_2.get("today")
print(
"Tipe today :",
type(today_data).__name__
)
if isinstance(today_data, dict):
print(
"Key today :",
list(today_data.keys())
)
elif isinstance(today_data, list):
print(
"Jumlah today :",
len(today_data)
)
message = level_1.get("message")
if message:
print("Message :", message)
elif corporate_response is None:
print("Status : Response kosong.")
# ============================================================
# DEBUG SECTOR ROTATION
# ============================================================
print("\nSECTOR ROTATION")
print("-" * 100)
print(
"Tipe response :",
type(sector_response).__name__
)
if isinstance(sector_response, dict):
print(
"Key utama :",
list(sector_response.keys())
)
sector_data = sector_response.get("data")
print(
"Tipe data :",
type(sector_data).__name__
)
if isinstance(sector_data, dict):
print(
"Key data :",
list(sector_data.keys())
)
elif isinstance(sector_data, list):
print(
"Jumlah data :",
len(sector_data)
)
elif sector_response is None:
print(
"Status : Response kosong atau request Cell 2 gagal."
)
# ============================================================
# PREVIEW
# ============================================================
print("\n" + "=" * 100)
print("PREVIEW HASIL NORMALISASI")
print("=" * 100)
print("\nCorporate Actions Preview:")
if corporate:
for index, item in enumerate(
corporate[:3],
start=1
):
print(f"\nRecord {index}")
print(
json.dumps(
item,
indent=2,
ensure_ascii=False,
default=str
)[:3000]
)
else:
print(
"Tidak ada Corporate Actions untuk hari ini."
)
print("\nSector Rotation Preview:")
if sector_rotation:
for index, item in enumerate(
sector_rotation[:3],
start=1
):
print(f"\nRecord {index}")
print(
json.dumps(
item,
indent=2,
ensure_ascii=False,
default=str
)[:3000]
)
else:
print(
"Tidak ada Sector Rotation yang berhasil dinormalisasi."
)
Cell 4 — Data Analysis and Table Display
This cell converts the normalized records into pandas DataFrames. It automatically selects relevant columns and safely handles missing or empty API data.
# ============================================================
# CELL 4 - ANALISIS DAN TABEL DATA
# ============================================================
def first_available(data, keys, default="-"):
"""
Mengambil nilai pertama dari key yang tersedia.
Mendukung nested key dengan format: data.key
"""
if not isinstance(data, dict):
return default
for key in keys:
value = data
for part in key.split("."):
if not isinstance(value, dict):
value = None
break
value = value.get(part)
if value not in [None, "", [], {}]:
return value
return default
def safe_number(value, decimals=2):
"""
Format angka tanpa menimbulkan error.
"""
try:
return f"{float(value):,.{decimals}f}"
except (TypeError, ValueError):
return (
str(value)
if value not in [None, ""]
else "-"
)
# ============================================================
# ANALISIS CORPORATE ACTIONS
# ============================================================
print("=" * 110)
print("TODAY CORPORATE ACTIONS")
print("=" * 110)
if corporate:
corporate_df = pd.json_normalize(corporate)
print("Jumlah Data :", len(corporate_df))
print("Kolom tersedia:")
print(corporate_df.columns.tolist())
preferred_columns = [
"_category",
"category",
"type",
"actionType",
"eventType",
"symbol",
"ticker",
"code",
"stockCode",
"companyName",
"company_name",
"company",
"name",
"date",
"eventDate",
"cumDate",
"exDate",
"recordingDate",
"paymentDate",
"description"
]
display_columns = [
column
for column in preferred_columns
if column in corporate_df.columns
]
print()
if display_columns:
display(
corporate_df[
display_columns
].head(30)
)
else:
display(
corporate_df.head(30)
)
else:
corporate_df = pd.DataFrame()
print(
"Tidak ada Corporate Actions untuk hari ini."
)
if isinstance(corporate_response, dict):
level_1 = corporate_response.get(
"data",
{}
)
if isinstance(level_1, dict):
message = level_1.get("message")
if message:
print("Pesan API :", message)
# ============================================================
# ANALISIS SECTOR ROTATION
# ============================================================
print("\n")
print("=" * 110)
print("SECTOR ROTATION")
print("=" * 110)
if sector_rotation:
sector_df = pd.json_normalize(
sector_rotation
)
print("Jumlah Data :", len(sector_df))
print("Kolom tersedia:")
print(sector_df.columns.tolist())
preferred_sector_columns = [
"rank",
"ranking",
"sector",
"sectorName",
"sector_name",
"name",
"score",
"rotationScore",
"rotation_score",
"momentum",
"trend",
"status",
"signal",
"performance",
"change",
"changePercent",
"return",
"netForeign",
"net_foreign",
"foreignFlow",
"foreign_flow",
"volume",
"value"
]
sector_display_columns = [
column
for column in preferred_sector_columns
if column in sector_df.columns
]
print()
if sector_display_columns:
display(
sector_df[
sector_display_columns
].head(30)
)
else:
display(
sector_df.head(30)
)
else:
sector_df = pd.DataFrame()
print("Tidak ada data Sector Rotation.")
if sector_response is None:
print(
"Keterangan: response Sector Rotation kosong "
"atau request pada Cell 2 gagal."
)Cell 5 — Dashboard and Final Summary
The final cell presents the corporate actions and sector rotation records in a readable dashboard. It also calculates active corporate-action categories and prints the final processing summary.
# ============================================================
# CELL 5 - DASHBOARD DAN RINGKASAN
# ============================================================
def get_corporate_categories(response):
"""
Menghitung jumlah Corporate Actions per kategori
dari response['data']['data'].
"""
categories = {}
if not isinstance(response, dict):
return categories
level_1 = response.get("data", {})
if not isinstance(level_1, dict):
return categories
level_2 = level_1.get("data", {})
if not isinstance(level_2, dict):
return categories
for category, value in level_2.items():
if category == "today":
continue
if isinstance(value, list):
categories[category] = len(value)
elif isinstance(value, dict):
categories[category] = (
1 if value else 0
)
else:
categories[category] = 0
return categories
def get_sector_value(item, keys, default="-"):
"""
Mengambil nilai Sector Rotation secara aman.
"""
return first_available(
item,
keys,
default
)
category_counts = get_corporate_categories(
corporate_response
)
active_categories = {
category: count
for category, count in category_counts.items()
if count > 0
}
print("=" * 115)
print("IDX CORPORATE ACTIONS & SECTOR ROTATION DASHBOARD")
print("=" * 115)
# ============================================================
# CORPORATE ACTIONS DASHBOARD
# ============================================================
print("\n📅 TODAY CORPORATE ACTIONS")
print("-" * 115)
print(
"Jumlah Corporate Actions Hari Ini :",
len(corporate)
)
if corporate:
for index, item in enumerate(
corporate[:15],
start=1
):
if not isinstance(item, dict):
print(f"{index:02d}. {item}")
continue
category = first_available(
item,
[
"_category",
"category",
"type",
"actionType",
"eventType"
]
)
symbol = first_available(
item,
[
"symbol",
"ticker",
"code",
"stockCode"
]
)
company = first_available(
item,
[
"companyName",
"company_name",
"company",
"name"
]
)
event_date = first_available(
item,
[
"date",
"eventDate",
"cumDate",
"exDate",
"recordingDate",
"paymentDate"
]
)
print(
f"{index:02d}. "
f"{str(symbol):<10} | "
f"{str(company):<35} | "
f"{str(category):<18} | "
f"{event_date}"
)
else:
print(
"Tidak ada agenda Corporate Actions untuk hari ini."
)
# ============================================================
# CORPORATE ACTION CATEGORY SUMMARY
# ============================================================
print("\n📂 CORPORATE ACTIONS PER KATEGORI")
print("-" * 115)
if active_categories:
for category, count in active_categories.items():
category_label = (
category
.replace("_", " ")
.title()
)
print(
f"{category_label:<25}: {count} data"
)
else:
print(
"Tidak ada kategori Corporate Actions "
"yang berisi data."
)
# ============================================================
# SECTOR ROTATION DASHBOARD
# ============================================================
print("\n📈 SECTOR ROTATION")
print("-" * 115)
print(
"Jumlah Sector Rotation :",
len(sector_rotation)
)
if sector_rotation:
for index, item in enumerate(
sector_rotation[:15],
start=1
):
if not isinstance(item, dict):
print(f"{index:02d}. {item}")
continue
rank = get_sector_value(
item,
["rank", "ranking"],
index
)
sector_name = get_sector_value(
item,
[
"sector",
"sectorName",
"sector_name",
"name"
]
)
score = get_sector_value(
item,
[
"score",
"rotationScore",
"rotation_score"
]
)
momentum = get_sector_value(
item,
[
"momentum",
"momentumScore",
"momentum_score"
]
)
trend = get_sector_value(
item,
[
"trend",
"status",
"signal"
]
)
performance = get_sector_value(
item,
[
"performance",
"changePercent",
"change_percent",
"return"
]
)
print(
f"{str(rank):>2}. "
f"{str(sector_name):<25} | "
f"Score: {str(score):<10} | "
f"Momentum: {str(momentum):<10} | "
f"Trend: {str(trend):<12} | "
f"Performance: {performance}"
)
else:
print("Tidak ada data Sector Rotation.")
# ============================================================
# RINGKASAN AKHIR
# ============================================================
print("\n")
print("=" * 115)
print("RINGKASAN")
print("=" * 115)
print(
f"📅 Total Corporate Actions : {len(corporate)}"
)
print(
f"📂 Kategori Aktif : {len(active_categories)}"
)
print(
f"📈 Total Sector Rotation : {len(sector_rotation)}"
)
if corporate:
print(
"✅ Corporate Actions berhasil diproses."
)
else:
print(
"ℹ️ Tidak ada Corporate Actions untuk hari ini."
)
if sector_rotation:
print(
"✅ Sector Rotation berhasil diproses."
)
else:
print(
"⚠️ Sector Rotation tidak tersedia "
"atau response kosong."
)
print(
"\nSelesai diproses :",
datetime.now().strftime(
"%d-%m-%Y %H:%M:%S"
)
)Final Result

After running all five cells, the project will:
Retrieve today’s IDX corporate actions.
Retrieve IDX sector rotation data.
Handle API errors, timeouts, and rate limits.
Normalize nested and varying JSON structures.
Convert the results into pandas DataFrames.
Display relevant corporate-action and sector-rotation columns.
Generate a readable dashboard and final summary.
Conclusion
This project demonstrates how Python and RapidAPI can be used to build an IDX Sector Rotation and corporate-actions monitoring dashboard. The notebook is designed to handle different JSON structures and unavailable data safely, making it suitable for regular Indonesia Stock Exchange market monitoring.
By combining today’s corporate actions with sector rotation analysis, investors can monitor scheduled company events while also observing which market sectors show stronger momentum, trends, or performance.
