OHLC.dev editorialIDX

Sector Stock Screener with Python and IDX Company Data

This article explains how to build an IDX sector stock screener with Python by combining getAllSectors and getSectorCompanies, retrieving IDX sector data and company data, cleaning both datasets, matching selected sector and subsector information, and producing a structured project output for sector-based stock analysis.

May 24, 202612 min readRafatar
Sector Stock Screener with Python and IDX Company Data

By the end of this workflow, the result is an IDX sector stock screener that can be used as a foundation for watchlists, dashboards, sector mapping, or deeper stock analysis.

An IDX sector stock screener is useful when investors want to filter listed companies by sector and subsector. Instead of analyzing all IDX-listed companies at once, this project helps organize sector data and company data into a cleaner structure using Python.

In this tutorial, we will build an IDX sector stock screener using getAllSectors and getSectorCompanies. The workflow retrieves IDX sector reference data, collects companies from a selected sector and subsector, cleans the result, and creates a structured output for sector-based stock analysis.

This tutorial shows how to build an IDX sector stock screener using Python, getAllSectors, and getSectorCompanies. The goal is to collect sector data, retrieve companies from a selected sector and subsector, clean the result, and create a structured output for sector-based stock analysis.

Instead of analyzing all listed companies at once, an IDX sector stock screener helps users focus only on companies that belong to a specific sector category. This makes the analysis more targeted and easier to extend into dashboards, watchlists, or deeper stock screening workflows.

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 several libraries. os is prepared for environment variable usage. json is used to preview API responses in readable JSON format. time is used to add delays between API requests. requests is used to call API endpoints. pandas is used to process API responses into dataframes. matplotlib.pyplot is used later for visualization.

The typing module imports Any, Dict, List, Optional, and Union. These are used in helper functions to make the code structure clearer.

The API setup 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. In this article, the real key is replaced with a placeholder for security.

The notebook also provides an alternative option using an environment variable:

# RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY")

This is useful if the notebook is used in a safer or production-like environment.

The validation block checks whether the API key is empty or still using the placeholder. If so, 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 in 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", "events", "rups", "economic"
        ]

        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 contains helper functions that make the notebook more flexible when working with API responses.

The first function is request_get. It sends a GET request to an API endpoint. The endpoint is joined with BASE_URL, then the request is sent with requests.get.

This function also includes retry handling. If the API returns status code 429, which means rate limit, the notebook waits and retries the request. This is useful when calling multiple endpoints from the same API service.

If the request succeeds, the function returns the payload. If another error occurs, it raises a RuntimeError that includes the status code, request URL, and response payload.

The second function is find_first_list. It searches through a nested JSON object and tries to find the first list of dictionaries. This is important because API responses may store event records inside different keys such as data, calendar, events, rups, or economic.

The third function is json_to_dataframe. It converts an API response into a pandas dataframe. If the helper function finds a list of records, it normalizes that list using pd.json_normalize. If not, it normalizes the dictionary directly.

The fourth function is detect_column. It detects column names based on possible keywords. This helps when API response fields vary across endpoints.

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

Together, these helper functions make the notebook more robust when handling different response structures from getRupsCalendar and getEconomicCalendar.

Cell 3 — Ambil Data dari Dua API

print("Mengambil data getRupsCalendar...")
rups_payload = request_get(
    endpoint="/api/calendar/rups",
    wait_seconds=3
)

time.sleep(5)

print("Mengambil data getEconomicCalendar...")
economic_payload = request_get(
    endpoint="/api/calendar/economic",
    wait_seconds=3
)


rups_df = json_to_dataframe(rups_payload)
economic_df = json_to_dataframe(economic_payload)

rups_df["source_api"] = "getRupsCalendar"
economic_df["source_api"] = "getEconomicCalendar"

print("RUPS Calendar shape:", rups_df.shape)
print("Economic Calendar shape:", economic_df.shape)

display(rups_df.head())
display(economic_df.head())

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

print("\nPreview Economic Calendar JSON:")
print(json.dumps(economic_payload, indent=2, ensure_ascii=False)[:1500])

Explanation

Cell 3 retrieves data from the two main APIs.

The first request calls:

/api/calendar/rups

This endpoint represents getRupsCalendar. It retrieves RUPS calendar data from the IDX API.

After that, the notebook waits for 5 seconds using:

time.sleep(5)

This manual delay helps reduce the possibility of hitting the API rate limit.

The second request calls:

/api/calendar/economic

This endpoint represents getEconomicCalendar. It retrieves economic calendar data.

Both API responses are converted into dataframes using json_to_dataframe.

The resulting dataframes are:

rups_df

and

economic_df

The notebook then adds a source_api column to each dataframe. For RUPS data, the value is getRupsCalendar. For economic calendar data, the value is getEconomicCalendar.

Finally, the notebook prints the shape of both dataframes, displays the first rows, and prints a raw JSON preview for each payload.

This cell is the main data collection step for the IDX market event calendar.

Cell 4 — Cleaning Data RUPS dan Economic Calendar

# Deteksi kolom RUPS Calendar
rups_ticker_col = detect_column(
    rups_df,
    ["ticker", "symbol", "stockCode", "stock_code", "code", "securityCode"]
)

rups_name_col = detect_column(
    rups_df,
    ["name", "stockName", "stock_name", "companyName", "company_name", "issuer"]
)

rups_date_col = detect_column(
    rups_df,
    ["date", "rupsDate", "rups_date", "eventDate", "event_date", "scheduleDate"]
)

rups_time_col = detect_column(
    rups_df,
    ["time", "rupsTime", "eventTime", "event_time"]
)

rups_type_col = detect_column(
    rups_df,
    ["type", "rupsType", "rups_type", "eventType", "event_type"]
)

rups_location_col = detect_column(
    rups_df,
    ["location", "place", "venue", "address"]
)


# Deteksi kolom Economic Calendar
economic_date_col = detect_column(
    economic_df,
    ["date", "eventDate", "event_date", "calendarDate", "calendar_date"]
)

economic_time_col = detect_column(
    economic_df,
    ["time", "eventTime", "event_time"]
)

economic_title_col = detect_column(
    economic_df,
    ["title", "event", "eventName", "event_name", "name", "indicator"]
)

economic_country_col = detect_column(
    economic_df,
    ["country", "region", "currency"]
)

economic_actual_col = detect_column(
    economic_df,
    ["actual", "actualValue", "actual_value"]
)

economic_forecast_col = detect_column(
    economic_df,
    ["forecast", "forecastValue", "forecast_value", "consensus"]
)

economic_previous_col = detect_column(
    economic_df,
    ["previous", "previousValue", "previous_value"]
)

economic_importance_col = detect_column(
    economic_df,
    ["importance", "impact", "priority", "level"]
)


print("Detected RUPS columns:")
print("ticker   :", rups_ticker_col)
print("name     :", rups_name_col)
print("date     :", rups_date_col)
print("time     :", rups_time_col)
print("type     :", rups_type_col)
print("location :", rups_location_col)

print("\nDetected Economic columns:")
print("date       :", economic_date_col)
print("time       :", economic_time_col)
print("title      :", economic_title_col)
print("country    :", economic_country_col)
print("actual     :", economic_actual_col)
print("forecast   :", economic_forecast_col)
print("previous   :", economic_previous_col)
print("importance :", economic_importance_col)


rups_clean = pd.DataFrame({
    "event_category": "RUPS",
    "ticker": (
        safe_series(rups_df, rups_ticker_col, "")
        .astype(str)
        .str.upper()
        .str.strip()
    ),
    "company_name": safe_series(rups_df, rups_name_col, ""),
    "event_date": safe_series(rups_df, rups_date_col, ""),
    "event_time": safe_series(rups_df, rups_time_col, ""),
    "event_title": safe_series(rups_df, rups_type_col, "RUPS"),
    "location": safe_series(rups_df, rups_location_col, ""),
    "country": "",
    "actual": "",
    "forecast": "",
    "previous": "",
    "importance": "",
    "source_api": "getRupsCalendar"
})


economic_clean = pd.DataFrame({
    "event_category": "Economic Calendar",
    "ticker": "",
    "company_name": "",
    "event_date": safe_series(economic_df, economic_date_col, ""),
    "event_time": safe_series(economic_df, economic_time_col, ""),
    "event_title": safe_series(economic_df, economic_title_col, ""),
    "location": "",
    "country": safe_series(economic_df, economic_country_col, ""),
    "actual": safe_series(economic_df, economic_actual_col, ""),
    "forecast": safe_series(economic_df, economic_forecast_col, ""),
    "previous": safe_series(economic_df, economic_previous_col, ""),
    "importance": safe_series(economic_df, economic_importance_col, ""),
    "source_api": "getEconomicCalendar"
})


# Ubah tanggal menjadi datetime agar bisa diurutkan
rups_clean["event_date_parsed"] = pd.to_datetime(
    rups_clean["event_date"],
    errors="coerce"
)

economic_clean["event_date_parsed"] = pd.to_datetime(
    economic_clean["event_date"],
    errors="coerce"
)


display(rups_clean.head(20))
display(economic_clean.head(20))

Explanation

Cell 4 cleans and standardizes both datasets.

The first section detects columns from rups_df. The notebook searches for ticker, company name, event date, event time, RUPS type, and location columns.

The second section detects columns from economic_df. The notebook searches for event date, event time, title, country, actual value, forecast value, previous value, and importance.

After detecting columns, the notebook prints the detected field names. This helps users confirm whether the notebook has identified the correct columns from each API response.

Next, the notebook creates rups_clean. This dataframe standardizes RUPS data into a common calendar structure:

event_category

ticker

company_name

event_date

event_time

event_title

location

country

actual

forecast

previous

importance

source_api

For RUPS data, event_category is set to "RUPS", and source_api is set to "getRupsCalendar".

Then, the notebook creates economic_clean using the same column structure. For economic events, fields such as ticker, company_name, and location are filled with empty strings because economic events are not tied to a specific company ticker in this workflow.

For economic calendar data, event_category is set to "Economic Calendar", and source_api is set to "getEconomicCalendar".

Finally, both datasets receive an event_date_parsed column. The notebook converts event_date into datetime format using pd.to_datetime. This makes it possible to sort events chronologically in the next step.

This cell is important because it turns two different calendar formats into one standardized event structure.

Cell 5 — Gabungkan Data Menjadi Kalender Terpadu

project_output = pd.concat(
    [rups_clean, economic_clean],
    ignore_index=True
)

project_output = project_output.sort_values(
    by=["event_date_parsed", "event_category"],
    ascending=[True, True]
).reset_index(drop=True)

project_output["project_note"] = (
    "Data getRupsCalendar dan getEconomicCalendar digabungkan menjadi "
    "satu kalender event pasar modal dan ekonomi."
)


summary_df = pd.DataFrame({
    "description": [
        "Total event RUPS",
        "Total event Economic Calendar",
        "Total seluruh event",
        "Jumlah ticker unik pada kalender RUPS",
        "Jumlah tanggal unik seluruh event"
    ],
    "value": [
        len(rups_clean),
        len(economic_clean),
        len(project_output),
        rups_clean["ticker"].replace("", pd.NA).dropna().nunique(),
        project_output["event_date"].replace("", pd.NA).dropna().nunique()
    ]
})


events_by_category = (
    project_output
    .groupby("event_category", dropna=False)
    .size()
    .reset_index(name="total_events")
)


display(project_output.head(50))
display(summary_df)
display(events_by_category)

Explanation

Cell 5 combines the cleaned RUPS calendar and economic calendar data into one unified output.

The notebook uses pd.concat to merge rups_clean and economic_clean vertically. This works because both datasets already have the same column structure from the previous cell.

The combined dataframe is stored as:

project_output

After combining the data, the notebook sorts the events by:

event_date_parsed

and

event_category

This makes the calendar easier to read because the events are arranged chronologically.

The notebook also adds a project_note column explaining that getRupsCalendar and getEconomicCalendar are combined into one capital market and economic event calendar.

Then, the notebook creates summary_df. This summary includes:

Total event RUPS

Total event Economic Calendar

Total seluruh event

Jumlah ticker unik pada kalender RUPS

Jumlah tanggal unik seluruh event

After that, the notebook creates events_by_category. This dataframe counts how many events exist in each event category.

At the end, the notebook displays the first 50 rows of the final calendar, the summary table, and the category-level event count.

This cell creates the main output of the IDX market event calendar project.

Cell 6 — Visualisasi dan Export

plt.figure(figsize=(8, 5))
plt.bar(events_by_category["event_category"], events_by_category["total_events"])
plt.xlabel("Event Category")
plt.ylabel("Total Events")
plt.title("Total Events by Calendar Category")
plt.tight_layout()
plt.show()


rups_clean.to_csv("idx_rups_calendar_clean.csv", index=False)
economic_clean.to_csv("idx_economic_calendar_clean.csv", index=False)
project_output.to_csv("idx_rups_economic_calendar_project.csv", index=False)
summary_df.to_csv("idx_rups_economic_calendar_summary.csv", index=False)

with pd.ExcelWriter("idx_rups_economic_calendar_project.xlsx", engine="openpyxl") as writer:
    rups_df.to_excel(writer, sheet_name="rups_raw", index=False)
    economic_df.to_excel(writer, sheet_name="economic_raw", index=False)
    rups_clean.to_excel(writer, sheet_name="rups_clean", index=False)
    economic_clean.to_excel(writer, sheet_name="economic_clean", index=False)
    project_output.to_excel(writer, sheet_name="final_calendar", index=False)
    summary_df.to_excel(writer, sheet_name="summary", index=False)
    events_by_category.to_excel(writer, sheet_name="events_by_category", index=False)

print("Export selesai:")
print("- idx_rups_calendar_clean.csv")
print("- idx_economic_calendar_clean.csv")
print("- idx_rups_economic_calendar_project.csv")
print("- idx_rups_economic_calendar_summary.csv")
print("- idx_rups_economic_calendar_project.xlsx")

Explanation

Cell 6 visualizes and exports the final calendar data.

The first part creates a bar chart using matplotlib. The X-axis shows the event category, while the Y-axis shows the total number of events.

The chart title is:

Total Events by Calendar Category

This visualization helps users quickly compare the number of RUPS events and economic calendar events.

After visualization, the notebook exports several CSV files:

idx_rups_calendar_clean.csv

idx_economic_calendar_clean.csv

idx_rups_economic_calendar_project.csv

idx_rups_economic_calendar_summary.csv

Then, the notebook creates an Excel file:

idx_rups_economic_calendar_project.xlsx

The Excel file contains multiple sheets:

rups_raw

economic_raw

rups_clean

economic_clean

final_calendar

summary

events_by_category

This export step makes the project easier to review, share, or continue in another workflow.

Finally, the notebook prints all exported filenames.

Result:

result

Overall Workflow

This notebook follows a simple but practical workflow.

First, it prepares the API configuration and required Python libraries. Second, it defines helper functions for API requests, rate limit handling, JSON parsing, dataframe conversion, column detection, and safe column selection.

Third, it retrieves sector reference data from getAllSectors. Fourth, it retrieves company data from getSectorCompanies using selected sector and subsector IDs. Fifth, it cleans and standardizes both datasets.

Finally, it combines company data with sector master information and creates a summary output.

The result is an IDX sector stock screener that can be used as a foundation for more advanced stock analysis projects.

Why This Case Matters

Sector-based analysis is one of the most practical ways to understand a stock market.

Instead of looking at all listed companies at once, analysts can focus on a specific sector or subsector. This helps reduce noise and makes the analysis more targeted.

An IDX sector stock screener can help users organize companies based on sector and subsector data before moving into deeper analysis such as valuation, liquidity filtering, sector rotation, or watchlist creation.

By combining getAllSectors and getSectorCompanies, this notebook builds the first layer of a sector-based stock analysis system.

Conclusion

This tutorial shows how to build an IDX sector stock screener using Python, getAllSectors, and getSectorCompanies.

The notebook starts by configuring the API connection and preparing helper functions. It then retrieves all sector data, requests companies from a selected sector and subsector, cleans both datasets, and combines company data with master sector information.

By the end of this workflow, we have a simple IDX sector stock screener that organizes companies by selected sector and subsector. This output can become the foundation for stock screening, watchlist creation, dashboard development, or deeper market analysis.