OHLC.dev editorial

IDX Sector Rotation and Today Corporate Actions Dashboard Using Python

This project demonstrates how to build an IDX Sector Rotation and Today Corporate Actions Dashboard using Python. The notebook connects to the Indonesia Stock Exchange (IDX) API through RapidAPI, retrieves today's corporate actions together with sector rotation data, automatically normalizes multiple API response structures, converts them into pandas DataFrames, and generates a comprehensive dashboard for market monitoring and investment analysis

July 28, 202611 min readRafatar
IDX Sector Rotation and Today Corporate Actions Dashboard Using Python

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

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.