OHLC.dev editorialIDX

IDX Dividend Calendar and Insider Trading Dashboard Using Python

This project demonstrates how to build an IDX Dividend Calendar and Insider Trading Dashboard using Python. It retrieves dividend schedules together with BBCA insider trading transactions from the Indonesia Stock Exchange API, handles request errors, normalizes nested JSON responses, and prepares structured market data for further analysis.

August 2, 202612 min readRafatar
IDX Dividend Calendar and Insider Trading Dashboard Using Python

Dividend distributions and insider trading activities are two important sources of information for investors. A dividend calendar helps investors identify upcoming corporate actions such as cum dates, ex dates, recording dates, and payment dates. Meanwhile, insider trading disclosures provide additional transparency by showing transactions made by company insiders within a selected period.

Instead of collecting this information manually, both datasets can be obtained automatically through the Indonesia Stock Exchange API available on RapidAPI.

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

  • Dividend Calendar

  • BBCA Insider Trading

The notebook consists of five cells. The first two cells prepare the API configuration and retrieve data from both endpoints. The following cells normalize different JSON response structures, clean and format the data, and finally generate an interactive dashboard that summarizes dividend events and insider transactions.

For security reasons, the RapidAPI key in the notebook should be replaced with YOUR_RAPIDAPI_KEY. Apart from that replacement, the notebook code 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 RapidAPI configuration used throughout the notebook.

It defines the API host, base URL, authentication headers, and prints a short configuration summary before any request is executed.

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

import requests
import pandas as pd
import json
import time

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"
API_HOST = "indonesia-stock-exchange-idx.p.rapidapi.com"

headers = {
    "Content-Type": "application/json",
    "x-rapidapi-host": API_HOST,
    "x-rapidapi-key": RAPIDAPI_KEY
}

print("=" * 100)
print("IDX DIVIDEND CALENDAR & INSIDER TRADING PROJECT")
print("=" * 100)
print("Konfigurasi API berhasil dibuat.")
print("Emiten Insider Trading : BBCA")
print("Periode                : 01-11-2025 sampai 31-12-2025")

The notebook imports several libraries to support API communication and data processing.

  • requests sends HTTP requests to the RapidAPI endpoint.

  • pandas converts API responses into structured DataFrames.

  • json formats nested JSON data for debugging.

  • time creates a short delay between requests to reduce the possibility of rate limiting.

  • datetime records the processing timestamp.

  • display renders DataFrames neatly inside Google Colab.

The API configuration is stored in the headers dictionary and reused throughout the notebook for every request.

Cell 2 — Request Dividend Calendar and Insider Trading Data

The second cell defines the two API endpoints and introduces a reusable request function for retrieving data from RapidAPI.

The function validates successful responses, handles HTTP errors, connection problems, and request timeouts before returning structured results.

# ============================================================
# CELL 2 — REQUEST DATA DARI DUA ENDPOINT
# ============================================================

DIVIDEND_ENDPOINT = "/api/calendar/dividend"

INSIDER_ENDPOINT = (
    "/api/emiten/BBCA/insider"
    "?date_end=2025-12-31"
    "&date_start=2025-11-01"
    "&limit=20"
    "&source_type=SOURCE_TYPE_UNSPECIFIED"
    "&action_type=ACTION_TYPE_UNSPECIFIED"
    "&page=1"
)

def request_api(endpoint, timeout=30):
    ...
    
# Request Dividend Calendar
dividend_response, dividend_status, dividend_error = request_api(
    DIVIDEND_ENDPOINT
)

time.sleep(1)

# Request Insider Trading
insider_response, insider_status, insider_error = request_api(
    INSIDER_ENDPOINT
)

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

Dividend Calendar Endpoint

The first endpoint retrieves the current dividend calendar from the Indonesia Stock Exchange.

The returned data may include:

  • Dividend per share

  • Cum Date

  • Ex Date

  • Recording Date

  • Payment Date

  • Fiscal Year

This information allows investors to monitor upcoming dividend distributions for listed companies.

Insider Trading Endpoint

The second endpoint retrieves insider trading transactions for BBCA.

The request uses the following parameters:

  • Period: 1 November 2025 – 31 December 2025

  • Maximum of 20 records

  • All source types

  • All action types

  • First result page

This endpoint helps investors monitor transactions made by company insiders during the selected reporting period.

Robust API Request Handling

The reusable request_api() function centralizes all API communication.

It automatically handles:

  • Successful HTTP responses (200)

  • Invalid JSON responses

  • Timeout exceptions

  • Connection failures

  • General request exceptions

  • API error messages returned by the server

Rather than stopping execution when an error occurs, the notebook returns the response object together with its HTTP status and any available error message.

Finally, both API responses are stored in:

dividend_response

and

insider_response

These variables will be normalized and cleaned in the following notebook cells before being converted into structured DataFrames.

Cell 3 — Inspecting and Normalizing API Responses

The third cell analyzes the structure of both API responses and automatically extracts the list of records containing dividend and insider trading information.

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

# Paste Cell 3 exactly as it appears in your notebook.
# Do not modify any variables, functions, indentation,
# or execution order.
#
# The only change allowed is replacing RAPIDAPI_KEY
# with YOUR_RAPIDAPI_KEY.

Understanding the Response Structure

Before processing the data, the notebook first inspects the response returned by each endpoint.

It prints useful debugging information such as:

  • Response type

  • Available top-level keys

  • Nested object types

  • Number of records

  • Sample record preview

This makes it much easier to identify structural changes introduced by future API updates.

Automatic Record Extraction

Rather than depending on one fixed JSON path, the notebook uses a recursive search function to locate the first valid list of records.

For the Dividend Calendar endpoint, the search prioritizes keys such as:

  • data

  • dividend

  • calendar

  • items

  • results

  • records

For Insider Trading, the function searches keys including:

  • insider

  • transactions

  • activities

  • records

  • results

  • items

If the records are nested several levels deep, the function continues searching until a list of dictionaries is found.

This flexible approach allows the notebook to remain compatible with different API versions without requiring manual code changes.

Normalization Results

Once the records are located, the notebook stores them inside two normalized variables:

dividend_records

and

insider_records

It then reports:

  • Total dividend records

  • Total insider trading records

  • Preview of the first record

  • Available fields for each endpoint

These diagnostics verify that the normalization process completed successfully before continuing to the next stage.

Cell 4 — Creating DataFrames and Preparing Tables

After the responses have been normalized, Cell 4 converts the extracted records into pandas DataFrames.

The notebook also flattens nested JSON objects, selects meaningful columns, and prepares clean tables for visualization inside Google Colab.

# ============================================================
# CELL 4 — PEMBERSIHAN DAN ANALISIS DATA
# ============================================================

def clean_number(value):
    """
    Mengubah berbagai format angka menjadi float.
    """

    if value in [None, "", "-"]:
        return None

    if isinstance(value, (int, float)):
        return float(value)

    if isinstance(value, dict):
        for key in ["value", "amount", "total", "price"]:
            if key in value:
                return clean_number(value[key])

        return None

    text = str(value).strip()

    text = (
        text.replace("Rp", "")
        .replace("IDR", "")
        .replace("%", "")
        .replace(" ", "")
    )

    # Menangani format Indonesia dan internasional
    if "." in text and "," in text:
        text = text.replace(".", "").replace(",", ".")

    elif text.count(".") > 1:
        text = text.replace(".", "")

    elif "," in text:
        text = text.replace(",", ".")

    try:
        return float(text)

    except (ValueError, TypeError):
        return None


def format_rupiah(value):
    """
    Mengubah angka menjadi format Rupiah.
    """

    number = clean_number(value)

    if number is None:
        return "-"

    return f"Rp {number:,.0f}".replace(",", ".")


def format_quantity(value):
    """
    Mengubah angka menjadi format kuantitas.
    """

    number = clean_number(value)

    if number is None:
        return "-"

    return f"{number:,.0f}".replace(",", ".")


def format_date(value):
    """
    Mengubah tanggal ke format DD-MM-YYYY.
    """

    if value in [None, "", "-"]:
        return "-"

    parsed = pd.to_datetime(value, errors="coerce")

    if pd.isna(parsed):
        return str(value)

    return parsed.strftime("%d-%m-%Y")


# ============================================================
# PEMBERSIHAN DIVIDEND
# ============================================================

if not df_dividend.empty:
    df_dividend_clean = df_dividend.copy()

    date_columns_dividend = [
        "Cum Date",
        "Ex Date",
        "Recording Date",
        "Payment Date"
    ]

    for column in date_columns_dividend:
        if column in df_dividend_clean.columns:
            df_dividend_clean[column] = (
                df_dividend_clean[column].apply(format_date)
            )

    df_dividend_clean["Dividen Numerik"] = (
        df_dividend_clean["Dividen per Saham"].apply(clean_number)
    )

    df_dividend_clean["Dividen per Saham"] = (
        df_dividend_clean["Dividen per Saham"].apply(format_rupiah)
    )

    df_dividend_clean = (
        df_dividend_clean
        .drop_duplicates()
        .reset_index(drop=True)
    )

else:
    df_dividend_clean = pd.DataFrame()


# ============================================================
# PEMBERSIHAN INSIDER TRADING
# ============================================================

if not df_insider.empty:
    df_insider_clean = df_insider.copy()

    df_insider_clean["Tanggal Transaksi"] = (
        df_insider_clean["Tanggal Transaksi"].apply(format_date)
    )

    df_insider_clean["Jumlah Numerik"] = (
        df_insider_clean["Jumlah Saham"].apply(clean_number)
    )

    df_insider_clean["Harga Numerik"] = (
        df_insider_clean["Harga"].apply(clean_number)
    )

    df_insider_clean["Nilai Numerik"] = (
        df_insider_clean["Nilai Transaksi"].apply(clean_number)
    )

    # Menghitung nilai transaksi jika API tidak menyediakannya
    calculated_value = (
        df_insider_clean["Jumlah Numerik"]
        * df_insider_clean["Harga Numerik"]
    )

    df_insider_clean["Nilai Numerik"] = (
        df_insider_clean["Nilai Numerik"]
        .fillna(calculated_value)
    )

    df_insider_clean["Jumlah Saham"] = (
        df_insider_clean["Jumlah Numerik"].apply(format_quantity)
    )

    df_insider_clean["Harga"] = (
        df_insider_clean["Harga Numerik"].apply(format_rupiah)
    )

    df_insider_clean["Nilai Transaksi"] = (
        df_insider_clean["Nilai Numerik"].apply(format_rupiah)
    )

    df_insider_clean = (
        df_insider_clean
        .drop_duplicates()
        .reset_index(drop=True)
    )

else:
    df_insider_clean = pd.DataFrame()


print("=" * 100)
print("HASIL PEMBERSIHAN DATA")
print("=" * 100)
print(f"Dividend Calendar : {len(df_dividend_clean)} records")
print(f"Insider Trading   : {len(df_insider_clean)} records")

if not df_dividend_clean.empty:
    valid_dividend = df_dividend_clean["Dividen Numerik"].dropna()

    if not valid_dividend.empty:
        print(
            f"Dividen tertinggi : "
            f"{format_rupiah(valid_dividend.max())} per saham"
        )

if not df_insider_clean.empty:
    valid_value = df_insider_clean["Nilai Numerik"].dropna()

    if not valid_value.empty:
        print(
            f"Total nilai transaksi insider : "
            f"{format_rupiah(valid_value.sum())}"
        )

Converting JSON into DataFrames

The notebook uses:

pd.json_normalize()

to flatten nested JSON structures into tabular format.

Nested objects are expanded into columns, making the data easier to analyze using pandas.

If a column still contains dictionaries or lists, those values are converted into JSON strings so they can be displayed correctly inside the DataFrame.

Selecting Relevant Columns

Different API versions may expose different field names.

For the Dividend Calendar, the notebook automatically searches for fields such as:

  • Stock Symbol

  • Company Name

  • Dividend

  • Cum Date

  • Ex Date

  • Recording Date

  • Payment Date

For Insider Trading, it searches for fields including:

  • Insider Name

  • Position

  • Transaction Type

  • Transaction Date

  • Price

  • Volume

  • Transaction Value

Only the columns that exist in the current response are selected, ensuring the notebook remains compatible with multiple API formats.

Displaying the Final Tables

Finally, the notebook displays two structured tables:

  • Dividend Calendar

  • BBCA Insider Trading

Before displaying the tables, it reports:

  • Total number of records

  • Number of DataFrame columns

  • Available column names

If one of the endpoints returns no usable data, the notebook prints a clear message instead of producing an exception.

This provides a cleaner debugging experience while keeping the notebook stable even when API responses are incomplete or temporarily unavailable


Cell 5 — Dividend Calendar & Insider Trading Dashboard

The last cell prepares the final dashboard by formatting values, displaying dividend schedules, summarizing insider transactions, and generating a concise execution report.

# ============================================================
# CELL 5 — DASHBOARD DIVIDEND DAN INSIDER TRADING
# ============================================================

print("=" * 110)
print("IDX DIVIDEND CALENDAR & BBCA INSIDER TRADING DASHBOARD")
print("=" * 110)


# ============================================================
# DIVIDEND CALENDAR
# ============================================================

print("\n💰 DIVIDEND CALENDAR")
print("-" * 110)

if not df_dividend_clean.empty:
    dividend_display_columns = [
        "Kode Saham",
        "Nama Perusahaan",
        "Dividen per Saham",
        "Cum Date",
        "Ex Date",
        "Recording Date",
        "Payment Date",
        "Tahun Buku"
    ]

    dividend_display_columns = [
        column
        for column in dividend_display_columns
        if column in df_dividend_clean.columns
    ]

    display(
        df_dividend_clean[
            dividend_display_columns
        ].head(30)
    )

    valid_dividend = df_dividend_clean.dropna(
        subset=["Dividen Numerik"]
    )

    if not valid_dividend.empty:
        highest_dividend_index = (
            valid_dividend["Dividen Numerik"].idxmax()
        )

        highest_dividend_row = (
            valid_dividend.loc[highest_dividend_index]
        )

        print(
            f"\nDividen tertinggi : "
            f"{highest_dividend_row.get('Kode Saham', '-')} — "
            f"{highest_dividend_row.get('Dividen per Saham', '-')}"
        )

else:
    print("Tidak ada data Dividend Calendar.")

    if dividend_error:
        print(f"Pesan API: {dividend_error}")


# ============================================================
# INSIDER TRADING
# ============================================================

print("\n\n👤 INSIDER TRADING BBCA")
print("-" * 110)

if not df_insider_clean.empty:
    insider_display_columns = [
        "Kode Saham",
        "Nama Insider",
        "Jabatan",
        "Tanggal Transaksi",
        "Aksi",
        "Jumlah Saham",
        "Harga",
        "Nilai Transaksi",
        "Kepemilikan Sebelum",
        "Kepemilikan Sesudah",
        "Sumber"
    ]

    insider_display_columns = [
        column
        for column in insider_display_columns
        if column in df_insider_clean.columns
    ]

    display(
        df_insider_clean[
            insider_display_columns
        ].head(20)
    )

    total_shares = (
        df_insider_clean["Jumlah Numerik"]
        .dropna()
        .sum()
    )

    total_value = (
        df_insider_clean["Nilai Numerik"]
        .dropna()
        .sum()
    )

    print(
        f"\nTotal saham ditransaksikan : "
        f"{format_quantity(total_shares)} saham"
    )

    print(
        f"Total nilai transaksi      : "
        f"{format_rupiah(total_value)}"
    )

    if "Aksi" in df_insider_clean.columns:
        action_summary = (
            df_insider_clean["Aksi"]
            .astype(str)
            .replace("-", "Tidak diketahui")
            .value_counts()
        )

        print("\nRingkasan aksi insider:")

        for action, total in action_summary.items():
            print(f"- {action}: {total} transaksi")

else:
    print("Tidak ada data Insider Trading BBCA.")

    if insider_error:
        print(f"Pesan API: {insider_error}")


# ============================================================
# RINGKASAN AKHIR
# ============================================================

print("\n")
print("=" * 110)
print("RINGKASAN")
print("=" * 110)

print(
    f"💰 Dividend Calendar Records : "
    f"{len(df_dividend_clean)}"
)

print(
    f"👤 Insider Trading Records   : "
    f"{len(df_insider_clean)}"
)

print(
    f"📡 Status Dividend API       : "
    f"{dividend_status if dividend_status is not None else 'Gagal terhubung'}"
)

print(
    f"📡 Status Insider API        : "
    f"{insider_status if insider_status is not None else 'Gagal terhubung'}"
)

dividend_condition = (
    "Data berhasil diproses"
    if not df_dividend_clean.empty
    else "Tidak ada data"
)

insider_condition = (
    "Data berhasil diproses"
    if not df_insider_clean.empty
    else "Tidak ada data"
)

print(f"✅ Kondisi Dividend          : {dividend_condition}")
print(f"✅ Kondisi Insider           : {insider_condition}")

print(
    f"\nSelesai diproses: "
    f"{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}"
)

Dividend Calendar Dashboard

The first section presents the dividend calendar in a concise format.

Depending on the fields returned by the API, the dashboard may include:

  • Stock symbol

  • Company name

  • Dividend amount

  • Cum Date

  • Ex Date

  • Recording Date

  • Payment Date

The notebook dynamically selects available fields so that it remains compatible even when the API response changes.

If no dividend schedule is returned, the dashboard displays a clear notification instead of generating an exception.

Insider Trading Dashboard

The second section summarizes insider trading activity for BBCA.

The dashboard automatically displays the most relevant information, including:

  • Insider name

  • Position

  • Transaction type

  • Transaction date

  • Transaction price

  • Transaction volume

  • Transaction value

Only fields that exist in the current API response are displayed, allowing the notebook to adapt to different response structures without additional code changes.

Final Summary

The notebook concludes with a compact execution summary containing:

  • Total Dividend Calendar records

  • Total Insider Trading records

  • Dividend endpoint status

  • Insider Trading endpoint status

  • Number of processed DataFrame rows

  • Processing completion timestamp

This summary confirms that both endpoints have been processed successfully while providing a quick overview of the retrieved market data.

Final Result

After completing all five notebook cells, this project is able to:

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

  • Retrieve the latest Dividend Calendar data.

  • Retrieve BBCA Insider Trading transactions for the selected period.

  • Handle HTTP errors, invalid JSON responses, connection failures, and request timeouts gracefully.

  • Inspect changing API response structures automatically.

  • Normalize nested JSON responses into structured record lists.

  • Convert the normalized records into pandas DataFrames.

  • Display clean dividend and insider trading tables dynamically.

  • Generate an informative dashboard summarizing corporate actions and insider transactions.

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

Conclusion

Dividend schedules and insider trading disclosures provide complementary insights into corporate activity on the Indonesia Stock Exchange. While the Dividend Calendar helps investors monitor upcoming corporate actions, Insider Trading data offers additional transparency regarding transactions performed by company insiders during a selected reporting period.

This notebook demonstrates a complete workflow for retrieving, validating, normalizing, and presenting both datasets using Python and RapidAPI. By dynamically detecting response structures, selecting available fields, and handling missing data safely, the project remains resilient to future API changes without requiring major code modifications.

The resulting dashboard can serve as a practical monitoring tool for investors, analysts, and developers who want to automate the collection of corporate action information and insider trading activity. It also provides a strong foundation for building more advanced investment dashboards, scheduled market reports, or broader Indonesia Stock Exchange analytics applications