OHLC.dev editorialIDX

IDX Corporate Action Screener with Python and Dividend Calendar Data

This article explains how to build an IDX corporate action screener with Python by combining getDividendCalendar and getTodayCorporateActions, cleaning both datasets, merging them by ticker, creating priority scores, visualizing event categories, and exporting the result into CSV and Excel files.

May 24, 202612 min readRafatar
IDX Corporate Action Screener with Python and Dividend Calendar Data

Corporate actions often become one of the most important signals in stock market monitoring.

A dividend schedule can attract investors who are looking for income opportunities. At the same time, today’s corporate actions can help traders and analysts identify stocks that may receive market attention during the current trading day.

Instead of checking dividend calendars and daily corporate action lists separately, this notebook builds a practical case: an IDX corporate action screener using Python.

The project combines two data sources from the Indonesia Stock Exchange API. The first is getDividendCalendar, which retrieves dividend calendar information. The second is getTodayCorporateActions, which retrieves corporate actions happening today.

By merging both datasets by ticker, the notebook creates a structured output that helps users monitor stocks with dividend schedules, today’s corporate actions, or both.

Cell 1 — Import Library + Config API

import os
import json
import time
from typing import Any, Dict, List, Optional, Union

import requests
import pandas as pd
import matplotlib.pyplot as plt


RAPIDAPI_HOST = "indonesia-stock-exchange-idx.p.rapidapi.com"
BASE_URL = f"https://{RAPIDAPI_HOST}"

# Ganti dengan RapidAPI Key Anda
RAPIDAPI_KEY = "ISI_RAPIDAPI_KEY_ANDA_DI_SINI"

# Alternatif jika memakai environment variable:
# RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY")

if not RAPIDAPI_KEY or RAPIDAPI_KEY == "ISI_RAPIDAPI_KEY_ANDA_DI_SINI":
    raise ValueError("Masukkan RapidAPI Key Anda dulu pada variabel RAPIDAPI_KEY.")

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

print("API configuration ready.")

Explanation

Cell 1 prepares the notebook environment and API configuration.

The notebook imports the required libraries. os is prepared for environment variable usage. json is used to preview API responses in a readable format. time is used to add delays between API requests. requests is used to call the API endpoints. pandas is used to process API data into dataframes. matplotlib.pyplot is used later for visualization.

The typing module imports Any, Dict, List, Optional, and Union. These type hints make the helper functions easier to read.

The API configuration starts with RAPIDAPI_HOST, which stores the RapidAPI host for the Indonesia Stock Exchange IDX API. BASE_URL is then created from that host.

The RAPIDAPI_KEY variable stores the API key. For public articles, the real API key should not be exposed, so it is shown here as a placeholder.

The notebook also provides an alternative approach using environment variables:

# RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY")

This is useful for safer credential management.

The validation block checks whether the API key is empty or still using the placeholder. If it is, the notebook raises a ValueError.

Finally, the HEADERS dictionary is created. It contains Content-Type, x-rapidapi-host, and x-rapidapi-key. These headers are used for every API request.

Cell 2 — Helper Function

def request_get(
    endpoint: str,
    params: Optional[Dict[str, Any]] = None,
    timeout: int = 30,
    max_retries: int = 5,
    wait_seconds: int = 3
) -> Union[Dict[str, Any], List[Any]]:

    url = f"{BASE_URL}{endpoint}"

    for attempt in range(1, max_retries + 1):
        time.sleep(wait_seconds)

        response = requests.get(
            url,
            headers=HEADERS,
            params=params,
            timeout=timeout
        )

        try:
            payload = response.json()
        except Exception:
            payload = response.text

        if response.ok:
            return payload

        if response.status_code == 429:
            print(
                f"Rate limit 429. Percobaan {attempt}/{max_retries}. "
                f"Tunggu {wait_seconds} detik..."
            )
            time.sleep(wait_seconds)
            continue

        raise RuntimeError(
            f"Request gagal. Status: {response.status_code}\n"
            f"URL: {response.url}\n"
            f"Response: {payload}"
        )

    raise RuntimeError(
        f"Request gagal karena rate limit 429 setelah {max_retries} percobaan."
    )


def find_first_list(obj: Any) -> List[Dict[str, Any]]:
    if isinstance(obj, list):
        return obj if all(isinstance(item, dict) for item in obj) else []

    if isinstance(obj, dict):
        priority_keys = [
            "data", "result", "results", "items", "rows",
            "content", "calendar", "dividend", "dividends",
            "corporateActions", "corporate_actions", "today", "actions"
        ]

        for key in priority_keys:
            if key in obj:
                found = find_first_list(obj[key])
                if found:
                    return found

        for value in obj.values():
            found = find_first_list(value)
            if found:
                return found

    return []


def json_to_dataframe(payload: Union[Dict[str, Any], List[Any]]) -> pd.DataFrame:
    records = find_first_list(payload)

    if records:
        return pd.json_normalize(records)

    if isinstance(payload, dict):
        return pd.json_normalize(payload)

    return pd.DataFrame()


def detect_column(df: pd.DataFrame, keywords: List[str]) -> Optional[str]:
    if df.empty:
        return None

    lower_map = {col.lower(): col for col in df.columns}

    for keyword in keywords:
        if keyword.lower() in lower_map:
            return lower_map[keyword.lower()]

    for col in df.columns:
        col_lower = col.lower()
        if any(keyword.lower() in col_lower for keyword in keywords):
            return col

    return None


def safe_series(df: pd.DataFrame, col: Optional[str], default_value=None) -> pd.Series:
    if col is None or col not in df.columns:
        return pd.Series([default_value] * len(df), index=df.index)

    return df[col]

Explanation

Cell 2 defines helper functions for requesting and processing API data.

The first function is request_get. It sends a GET request to an API endpoint, handles the response, and returns the payload. The function also includes retry handling for status code 429, which usually indicates rate limiting. If the request keeps failing after the maximum retry attempts, it raises a RuntimeError.

The second function is find_first_list. It searches through nested JSON data and tries to find the first list of dictionaries. This is useful because API responses can have different structures, such as records stored inside data, calendar, dividend, corporateActions, or actions.

The third function is json_to_dataframe. It converts a JSON payload into a pandas dataframe. If a list of records is found, the function normalizes it using pd.json_normalize. If not, it normalizes the dictionary directly.

The fourth function is detect_column. It detects dataframe columns based on possible keyword names. This helps the notebook handle changing or inconsistent API field names.

The final function is safe_series. It safely returns a dataframe column. If the column does not exist, it returns a default series with the same index length.

Together, these helper functions make the IDX corporate action screener more flexible when working with different API response formats.

Cell 3 — Ambil Data dari Dua API

print("Mengambil data getDividendCalendar...")
dividend_payload = request_get(
    endpoint="/api/calendar/dividend",
    wait_seconds=3
)

time.sleep(5)

print("Mengambil data getTodayCorporateActions...")
today_ca_payload = request_get(
    endpoint="/api/calendar/today",
    wait_seconds=3
)


dividend_df = json_to_dataframe(dividend_payload)
today_ca_df = json_to_dataframe(today_ca_payload)

dividend_df["source_api"] = "getDividendCalendar"
today_ca_df["source_api"] = "getTodayCorporateActions"

print("Dividend Calendar shape:", dividend_df.shape)
print("Today Corporate Actions shape:", today_ca_df.shape)

display(dividend_df.head())
display(today_ca_df.head())

print("Preview Dividend Calendar JSON:")
print(json.dumps(dividend_payload, indent=2, ensure_ascii=False)[:1500])

print("\nPreview Today Corporate Actions JSON:")
print(json.dumps(today_ca_payload, indent=2, ensure_ascii=False)[:1500])

Explanation

Cell 3 retrieves data from the two APIs.

The first request calls:

/api/calendar/dividend

This endpoint represents getDividendCalendar, which retrieves dividend calendar data.

After the first request, the notebook pauses for 5 seconds using time.sleep(5). This delay helps reduce the chance of hitting rate limits.

The second request calls:

/api/calendar/today

This endpoint represents getTodayCorporateActions, which retrieves today’s corporate action data.

Both API responses are converted into dataframes using json_to_dataframe.

The resulting dataframes are:

dividend_df

and

today_ca_df

The notebook then adds a source_api column to each dataframe. This makes it easier to identify where each dataset comes from.

At the end of the cell, the notebook prints the shape of both dataframes, displays the first few rows, and prints a preview of the raw JSON responses.

This cell is the main data collection step for the IDX corporate action screener.

Cell 4 — Cleaning Data Dividend dan Corporate Actions

# Deteksi kolom Dividend Calendar
dividend_ticker_col = detect_column(
    dividend_df,
    ["ticker", "symbol", "stockCode", "stock_code", "code", "securityCode"]
)

dividend_name_col = detect_column(
    dividend_df,
    ["name", "stockName", "stock_name", "companyName", "company_name", "issuer"]
)

cum_date_col = detect_column(
    dividend_df,
    ["cumDate", "cum_date", "cum", "cumDividendDate"]
)

ex_date_col = detect_column(
    dividend_df,
    ["exDate", "ex_date", "ex", "exDividendDate"]
)

recording_date_col = detect_column(
    dividend_df,
    ["recordingDate", "recording_date", "recordDate"]
)

payment_date_col = detect_column(
    dividend_df,
    ["paymentDate", "payment_date", "payDate", "distributionDate"]
)

dividend_value_col = detect_column(
    dividend_df,
    ["dividend", "dividendValue", "dividend_value", "cashDividend", "cash_dividend", "amount"]
)


# Deteksi kolom Today Corporate Actions
ca_ticker_col = detect_column(
    today_ca_df,
    ["ticker", "symbol", "stockCode", "stock_code", "code", "securityCode"]
)

ca_name_col = detect_column(
    today_ca_df,
    ["name", "stockName", "stock_name", "companyName", "company_name", "issuer"]
)

ca_date_col = detect_column(
    today_ca_df,
    ["date", "eventDate", "event_date", "calendarDate", "calendar_date"]
)

ca_action_col = detect_column(
    today_ca_df,
    ["action", "actionType", "action_type", "type", "event", "eventType", "title"]
)

ca_description_col = detect_column(
    today_ca_df,
    ["description", "desc", "detail", "remarks", "information"]
)


print("Detected Dividend columns:")
print("ticker         :", dividend_ticker_col)
print("name           :", dividend_name_col)
print("cum date       :", cum_date_col)
print("ex date        :", ex_date_col)
print("recording date :", recording_date_col)
print("payment date   :", payment_date_col)
print("dividend value :", dividend_value_col)

print("\nDetected Corporate Action columns:")
print("ticker      :", ca_ticker_col)
print("name        :", ca_name_col)
print("date        :", ca_date_col)
print("action type :", ca_action_col)
print("description :", ca_description_col)


dividend_clean = pd.DataFrame({
    "ticker": (
        safe_series(dividend_df, dividend_ticker_col, "")
        .astype(str)
        .str.upper()
        .str.strip()
    ),
    "stock_name_dividend": safe_series(dividend_df, dividend_name_col, ""),
    "cum_date": safe_series(dividend_df, cum_date_col, ""),
    "ex_date": safe_series(dividend_df, ex_date_col, ""),
    "recording_date": safe_series(dividend_df, recording_date_col, ""),
    "payment_date": safe_series(dividend_df, payment_date_col, ""),
    "dividend_value": pd.to_numeric(
        safe_series(dividend_df, dividend_value_col, None),
        errors="coerce"
    ),
    "source_api_dividend": "getDividendCalendar"
})

dividend_clean = dividend_clean[dividend_clean["ticker"].ne("")].copy()


today_ca_clean = pd.DataFrame({
    "ticker": (
        safe_series(today_ca_df, ca_ticker_col, "")
        .astype(str)
        .str.upper()
        .str.strip()
    ),
    "stock_name_ca": safe_series(today_ca_df, ca_name_col, ""),
    "corporate_action_date": safe_series(today_ca_df, ca_date_col, ""),
    "corporate_action_type": safe_series(today_ca_df, ca_action_col, ""),
    "corporate_action_description": safe_series(today_ca_df, ca_description_col, ""),
    "source_api_ca": "getTodayCorporateActions"
})

today_ca_clean = today_ca_clean[today_ca_clean["ticker"].ne("")].copy()


display(dividend_clean.head(20))
display(today_ca_clean.head(20))

Explanation

Cell 4 cleans and standardizes both datasets.

The first part detects important columns from dividend_df. The notebook searches for ticker, stock name, cum date, ex date, recording date, payment date, and dividend value columns.

The second part detects important columns from today_ca_df. It searches for ticker, company name, corporate action date, action type, and description.

After detecting the columns, the notebook prints the detected column names so users can verify which fields were found.

Next, the notebook creates dividend_clean. This dataframe standardizes the dividend calendar data into:

ticker

stock_name_dividend

cum_date

ex_date

recording_date

payment_date

dividend_value

source_api_dividend

The ticker is converted into uppercase and stripped of extra spaces. The dividend value is converted into numeric format. Rows with empty tickers are removed.

Then, the notebook creates today_ca_clean. This dataframe standardizes today’s corporate action data into:

ticker

stock_name_ca

corporate_action_date

corporate_action_type

corporate_action_description

source_api_ca

The ticker is also converted into uppercase and cleaned from extra spaces. Rows with empty ticker values are removed.

This cell prepares both datasets so they can be merged by ticker in the next step.

Cell 5 — Gabungkan Data dan Buat Output Project

project_output = dividend_clean.merge(
    today_ca_clean,
    on="ticker",
    how="outer"
)

project_output["stock_name"] = project_output["stock_name_dividend"].fillna(
    project_output["stock_name_ca"]
)

project_output["has_dividend_calendar"] = project_output["source_api_dividend"].notna()
project_output["has_today_corporate_action"] = project_output["source_api_ca"].notna()

project_output["dividend_value"] = pd.to_numeric(
    project_output["dividend_value"],
    errors="coerce"
)

project_output["priority_score"] = 0
project_output.loc[project_output["has_dividend_calendar"], "priority_score"] += 50
project_output.loc[project_output["has_today_corporate_action"], "priority_score"] += 50
project_output["dividend_value_score"] = project_output["dividend_value"].fillna(0)

project_output["final_score"] = (
    project_output["priority_score"]
    + project_output["dividend_value_score"]
)

project_output["project_note"] = (
    "Data getDividendCalendar dan getTodayCorporateActions digabungkan berdasarkan ticker "
    "untuk memantau saham yang memiliki jadwal dividen dan aksi korporasi hari ini."
)

project_output = project_output.sort_values(
    ["priority_score", "dividend_value"],
    ascending=[False, False]
)


summary_df = pd.DataFrame({
    "description": [
        "Total saham dari getDividendCalendar",
        "Total saham dari getTodayCorporateActions",
        "Total saham setelah digabung",
        "Saham yang muncul di kedua API",
        "Saham hanya punya jadwal dividen",
        "Saham hanya punya corporate action hari ini"
    ],
    "value": [
        len(dividend_clean),
        len(today_ca_clean),
        len(project_output),
        len(project_output[
            (project_output["has_dividend_calendar"] == True) &
            (project_output["has_today_corporate_action"] == True)
        ]),
        len(project_output[
            (project_output["has_dividend_calendar"] == True) &
            (project_output["has_today_corporate_action"] == False)
        ]),
        len(project_output[
            (project_output["has_dividend_calendar"] == False) &
            (project_output["has_today_corporate_action"] == True)
        ])
    ]
})


display(project_output.head(30))
display(summary_df)

Explanation

Cell 5 merges the cleaned dividend calendar data and today’s corporate action data.

The merge uses the ticker column and applies how="outer". This means the final output keeps all tickers from both datasets, whether they appear in only the dividend calendar, only today’s corporate action list, or both.

After merging, the notebook creates a unified stock name column:

stock_name

This column uses stock_name_dividend first. If it is missing, it uses stock_name_ca.

Then, the notebook creates two boolean indicators:

has_dividend_calendar

and

has_today_corporate_action

These columns show whether a stock appears in the dividend calendar dataset, the today corporate action dataset, or both.

The notebook then converts dividend_value into numeric format.

Next, it creates a simple scoring system. The priority_score starts from 0. If a stock appears in the dividend calendar, it receives 50 points. If it appears in today’s corporate actions, it receives another 50 points. Therefore, a stock that appears in both datasets can receive 100 priority points.

The notebook also creates dividend_value_score, which uses the dividend value and fills missing values with 0.

Then, final_score is calculated by adding:

priority_score

and

dividend_value_score

This gives priority to stocks that appear in both corporate action sources, while still considering dividend value.

The final output also receives a project_note explaining that getDividendCalendar and getTodayCorporateActions are combined by ticker to monitor stocks with dividend schedules and today’s corporate actions.

After that, project_output is sorted by priority_score and dividend_value.

Finally, the notebook creates summary_df, which summarizes total stocks from each API, total merged stocks, stocks appearing in both APIs, stocks only in dividend calendar, and stocks only in today’s corporate action list.

This cell is the core of the IDX corporate action screener.

Cell 6 — Visualisasi dan Export

status_summary = pd.DataFrame({
    "category": [
        "Dividend Calendar",
        "Today Corporate Action",
        "Both"
    ],
    "total": [
        project_output["has_dividend_calendar"].sum(),
        project_output["has_today_corporate_action"].sum(),
        len(project_output[
            (project_output["has_dividend_calendar"] == True) &
            (project_output["has_today_corporate_action"] == True)
        ])
    ]
})


plt.figure(figsize=(8, 5))
plt.bar(status_summary["category"], status_summary["total"])
plt.xlabel("Category")
plt.ylabel("Total Stocks")
plt.title("Dividend Calendar vs Today Corporate Actions")
plt.tight_layout()
plt.show()


dividend_clean.to_csv("idx_dividend_calendar_clean.csv", index=False)
today_ca_clean.to_csv("idx_today_corporate_actions_clean.csv", index=False)
project_output.to_csv("idx_dividend_today_ca_project.csv", index=False)
summary_df.to_csv("idx_dividend_today_ca_summary.csv", index=False)

with pd.ExcelWriter("idx_dividend_today_ca_project.xlsx", engine="openpyxl") as writer:
    dividend_df.to_excel(writer, sheet_name="dividend_raw", index=False)
    today_ca_df.to_excel(writer, sheet_name="today_ca_raw", index=False)
    dividend_clean.to_excel(writer, sheet_name="dividend_clean", index=False)
    today_ca_clean.to_excel(writer, sheet_name="today_ca_clean", index=False)
    project_output.to_excel(writer, sheet_name="final_output", index=False)
    summary_df.to_excel(writer, sheet_name="summary", index=False)
    status_summary.to_excel(writer, sheet_name="status_summary", index=False)

print("Export selesai:")
print("- idx_dividend_calendar_clean.csv")
print("- idx_today_corporate_actions_clean.csv")
print("- idx_dividend_today_ca_project.csv")
print("- idx_dividend_today_ca_summary.csv")
print("- idx_dividend_today_ca_project.xlsx")

Explanation

Cell 6 visualizes and exports the final project.

The first part creates status_summary. This dataframe contains three categories:

Dividend Calendar

Today Corporate Action

Both

For each category, the notebook calculates the total number of stocks.

Then, the notebook creates a bar chart using matplotlib. The chart compares how many stocks appear in the dividend calendar, today’s corporate action list, and both datasets.

The chart title is:

Dividend Calendar vs Today Corporate Actions

After visualization, the notebook exports several CSV files:

idx_dividend_calendar_clean.csv

idx_today_corporate_actions_clean.csv

idx_dividend_today_ca_project.csv

idx_dividend_today_ca_summary.csv

Then, the notebook creates an Excel file:

idx_dividend_today_ca_project.xlsx

The Excel file contains multiple sheets:

dividend_raw

today_ca_raw

dividend_clean

today_ca_clean

final_output

summary

status_summary

This makes the final result easy to inspect, share, and reuse in future analysis.

Result:

result

Overall Workflow

This notebook follows a practical workflow for building an IDX corporate action screener.

First, it prepares the API configuration and required Python libraries.

Second, it creates helper functions for API requests, retry handling, JSON parsing, dataframe conversion, column detection, and safe column selection.

Third, it retrieves dividend calendar data using getDividendCalendar.

Fourth, it retrieves today’s corporate action data using getTodayCorporateActions.

Fifth, it cleans and standardizes both datasets.

Sixth, it merges the datasets by ticker.

Seventh, it creates a simple priority-based scoring system.

Finally, it visualizes the category summary and exports the output into CSV and Excel files.

The final output helps users monitor stocks that have dividend schedules, today’s corporate actions, or both.

Why This Case Matters

Many investors monitor dividend schedules separately from other corporate action events. This makes the process slower and less structured.

An IDX corporate action screener helps bring both sources into one workflow. Dividend data gives a view of potential income-related events, while today’s corporate action data helps identify stocks with current event activity.

When both signals appear on the same ticker, the stock may deserve closer attention for further analysis.

This notebook does not create a buy or sell signal. Instead, it builds a clean monitoring layer for corporate action data. The output can be used as a starting point for watchlists, event tracking, dividend monitoring, or market research.

Conclusion

This tutorial shows how to build an IDX corporate action screener using Python, getDividendCalendar, and getTodayCorporateActions.

The notebook retrieves dividend calendar data and today’s corporate action data, converts both API responses into dataframes, detects important columns, cleans both datasets, merges them by ticker, calculates a priority-based score, visualizes the event categories, and exports the final result.

The value of this workflow is its simplicity. It turns two separate event sources into one structured screener that is easier to read and reuse.

For traders, investors, analysts, and data enthusiasts, this project can become a useful foundation for monitoring corporate action events in the Indonesian stock market.