OHLC.dev editorialIDX

IDX Stock Analysis with Python and RapidAPI

IDX stock analysis can be performed efficiently using Python and RapidAPI. This project builds a compact market analysis dashboard for the Indonesia Stock Exchange, covering Sec...

August 27, 20268 min readRafatar
IDX Stock Analysis with Python and RapidAPI

IDX stock analysis can be performed efficiently using Python and RapidAPI. This project builds a compact market analysis dashboard for the Indonesia Stock Exchange, covering Sector Rotation, Energy Multibagger Scan, and Breakout Alerts.

This project builds a compact Python-based stock market analysis dashboard using data from the Indonesia Stock Exchange through RapidAPI. The analysis focuses on three main components: Sector Rotation, Energy Multibagger Scan, and Breakout Alerts.

The project uses requests for API communication, pandas and NumPy for data processing, and Matplotlib for visualization. The complete workflow is organized into five cells, starting from API configuration and ending with an automated market intelligence dashboard.


Cell 1 - Setup and API Configuration

# ============================================================
# CELL 1 — SETUP & API CONFIGURATION
# ============================================================

import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import json
import warnings

warnings.filterwarnings("ignore")

pd.set_option("display.max_columns", None)
pd.set_option("display.width", 150)
pd.set_option("display.max_colwidth", 80)

# ============================================================
# RAPIDAPI CONFIG
# ============================================================

RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"

BASE_URL = "https://indonesia-stock-exchange-idx.p.rapidapi.com"

HEADERS = {
    "Content-Type": "application/json",
    "x-rapidapi-host": "indonesia-stock-exchange-idx.p.rapidapi.com",
    "x-rapidapi-key": RAPIDAPI_KEY
}

# Endpoint
URL_SECTOR_ROTATION = f"{BASE_URL}/api/analysis/retail/sector-rotation"

URL_MULTIBAGGER = (
    f"{BASE_URL}/api/analysis/retail/multibagger/scan"
    "?sector=Energy&min_score=50&max_results=20"
)

URL_BREAKOUT = f"{BASE_URL}/api/analysis/retail/breakout/alerts"

print("✅ Setup selesai")
print("API Host :", HEADERS["x-rapidapi-host"])

Explanation

Cell 1 prepares the Python environment and configures the connection to the RapidAPI service.

The project imports several libraries:

  • requests for HTTP requests.

  • pandas for DataFrame processing.

  • NumPy for numerical operations.

  • Matplotlib for visualization.

  • json for JSON handling.

  • warnings for controlling warning messages.

Three API endpoints are defined. The first endpoint retrieves sector rotation data. The second performs a multibagger scan specifically for the Energy sector, using a minimum score of 50 and returning up to 20 results. The third endpoint retrieves breakout alerts.

The API key in this article is intentionally replaced with YOUR_RAPIDAPI_KEY for security.


Cell 2 - API Request and Robust JSON Parser

# ============================================================
# CELL 2 — API REQUEST + ROBUST JSON PARSER
# ============================================================

def api_get(url, name="API"):
    """GET request dengan error handling."""

    try:
        response = requests.get(
            url,
            headers=HEADERS,
            timeout=30
        )

        print(f"{name:<20} HTTP {response.status_code}")

        response.raise_for_status()

        data = response.json()

        if isinstance(data, dict):
            print(f"  Top-level keys : {list(data.keys())}")
        else:
            print(f"  Response type  : {type(data).__name__}")

        return data

    except requests.exceptions.RequestException as e:
        print(f"❌ Request error [{name}]: {e}")
        return {}

    except ValueError:
        print(f"❌ [{name}] Response bukan JSON valid.")
        return {}


def find_table_candidates(obj, path="root"):
    """
    Mencari seluruh list-of-dict dalam JSON nested.
    Berguna apabila API mengubah key seperti:
    data/list/result/stocks/alerts/sectors/etc.
    """

    candidates = []

    if isinstance(obj, list):

        if len(obj) > 0 and all(isinstance(x, dict) for x in obj):
            candidates.append({
                "path": path,
                "rows": len(obj),
                "data": obj
            })

        for i, item in enumerate(obj):
            candidates.extend(
                find_table_candidates(item, f"{path}[{i}]")
            )

    elif isinstance(obj, dict):

        for key, value in obj.items():
            candidates.extend(
                find_table_candidates(value, f"{path}.{key}")
            )

    return candidates


def json_to_df(obj, dataset_name="dataset"):
    """
    Mengambil tabel paling representatif dari JSON.
    Prioritas utama = list-of-dict dengan jumlah row terbesar.
    """

    candidates = find_table_candidates(obj)

    if candidates:

        candidates = sorted(
            candidates,
            key=lambda x: x["rows"],
            reverse=True
        )

        best = candidates[0]

        print(
            f"✅ {dataset_name}: "
            f"path={best['path']} | rows={best['rows']}"
        )

        return pd.json_normalize(best["data"])

    # Fallback bila response hanya dictionary
    if isinstance(obj, dict) and obj:

        try:
            df = pd.json_normalize(obj)
            print(f"⚠️ {dataset_name}: menggunakan dictionary fallback.")
            return df

        except Exception:
            pass

    print(f"⚠️ {dataset_name}: tidak ditemukan data tabular.")

    return pd.DataFrame()

Explanation

Cell 2 contains the main data retrieval and JSON-processing functions.

The api_get() function sends a GET request to the selected API endpoint and applies a 30-second timeout. It checks the HTTP status and converts the response into JSON format. Error handling is included to prevent the notebook from stopping when a request fails or when the returned response is not valid JSON.

The find_table_candidates() function recursively searches nested JSON objects for lists containing dictionaries. This is useful because API responses may store the relevant information under different keys such as data, list, results, stocks, or alerts.

The json_to_df() function selects the list containing the largest number of records and converts it into a pandas DataFrame using pd.json_normalize(). A dictionary fallback is also provided when no list-based table is detected.


Cell 3 - Fetch Data

# ============================================================
# CELL 3 — FETCH DATA
# ============================================================

sector_raw = api_get(
    URL_SECTOR_ROTATION,
    "Sector Rotation"
)

multibagger_raw = api_get(
    URL_MULTIBAGGER,
    "Multibagger Scan"
)

breakout_raw = api_get(
    URL_BREAKOUT,
    "Breakout Alerts"
)

print("\n" + "=" * 65)
print("DATA DOWNLOAD SUMMARY")
print("=" * 65)

print(
    "Sector Rotation :",
    "OK" if sector_raw else "FAILED"
)

print(
    "Multibagger     :",
    "OK" if multibagger_raw else "FAILED"
)

print(
    "Breakout Alert  :",
    "OK" if breakout_raw else "FAILED"
)

Explanation

Cell 3 retrieves the three analytical datasets from the IDX API.

The returned JSON objects are stored in:

sector_raw
multibagger_raw
breakout_raw

The script then displays a simple download summary to indicate whether each API request was successful or failed.

This stage separates API retrieval from data processing, making it easier to identify whether an error originates from the API connection or from later data transformation.


Cell 4 - Normalize and Inspect the Data

# ============================================================
# CELL 4 — NORMALIZE & DATA INSPECTION
# ============================================================

df_sector = json_to_df(
    sector_raw,
    "Sector Rotation"
)

df_multi = json_to_df(
    multibagger_raw,
    "Multibagger"
)

df_breakout = json_to_df(
    breakout_raw,
    "Breakout Alerts"
)


# ------------------------------------------------------------
# Bersihkan nama kolom
# ------------------------------------------------------------

def clean_columns(df):

    if df.empty:
        return df

    df = df.copy()

    df.columns = (
        df.columns
        .astype(str)
        .str.replace(".", "_", regex=False)
        .str.replace(" ", "_", regex=False)
        .str.lower()
    )

    return df


df_sector = clean_columns(df_sector)
df_multi = clean_columns(df_multi)
df_breakout = clean_columns(df_breakout)


# ------------------------------------------------------------
# Dataset Summary
# ------------------------------------------------------------

summary = pd.DataFrame({

    "Dataset": [
        "Sector Rotation",
        "Multibagger Energy",
        "Breakout Alerts"
    ],

    "Rows": [
        len(df_sector),
        len(df_multi),
        len(df_breakout)
    ],

    "Columns": [
        len(df_sector.columns),
        len(df_multi.columns),
        len(df_breakout.columns)
    ]
})

display(summary)


print("\n" + "=" * 70)
print("1. SECTOR ROTATION")
print("=" * 70)

display(df_sector.head(10))


print("\n" + "=" * 70)
print("2. MULTIBAGGER SCAN — ENERGY")
print("=" * 70)

display(df_multi.head(20))


print("\n" + "=" * 70)
print("3. BREAKOUT ALERTS")
print("=" * 70)

display(df_breakout.head(20))

Explanation

Cell 4 converts the raw JSON responses into three structured DataFrames:

df_sector
df_multi
df_breakout

The program then standardizes the column names by converting them to lowercase and replacing periods and spaces with underscores.

A summary table is created to show the number of rows and columns in each dataset.

The first records from the three datasets are also displayed. This inspection stage is important because it allows the user to verify the structure and available variables before running the final analysis.


Cell 5 - IDX Retail Analysis Dashboard

# ============================================================
# CELL 5 — IDX RETAIL ANALYSIS DASHBOARD
# ============================================================

def find_col(df, keywords):
    """Cari column berdasarkan beberapa kemungkinan keyword."""

    if df.empty:
        return None

    for keyword in keywords:

        # Exact
        for col in df.columns:
            if col.lower() == keyword.lower():
                return col

        # Partial
        for col in df.columns:
            if keyword.lower() in col.lower():
                return col

    return None


def numeric_series(df, col):

    if col is None:
        return None

    return pd.to_numeric(
        df[col],
        errors="coerce"
    )


# ============================================================
# IDENTIFIKASI COLUMN
# ============================================================

ticker_multi = find_col(
    df_multi,
    ["symbol", "ticker", "code", "stock_code"]
)

score_multi = find_col(
    df_multi,
    ["score", "total_score", "multibagger_score"]
)

ticker_breakout = find_col(
    df_breakout,
    ["symbol", "ticker", "code", "stock_code"]
)

score_breakout = find_col(
    df_breakout,
    ["score", "strength", "breakout_score", "confidence"]
)

sector_col = find_col(
    df_sector,
    ["sector", "sector_name", "name"]
)

rotation_col = find_col(
    df_sector,
    [
        "score",
        "rotation_score",
        "momentum",
        "performance",
        "change"
    ]
)


# ============================================================
# MARKET INTELLIGENCE SUMMARY
# ============================================================

print("=" * 70)
print("IDX RETAIL MARKET INTELLIGENCE")
print("=" * 70)

print(f"""
Sector Rotation Records : {len(df_sector):,}
Multibagger Candidates  : {len(df_multi):,}
Breakout Alerts         : {len(df_breakout):,}
""")


# ============================================================
# MULTIBAGGER RANKING
# ============================================================

if not df_multi.empty:

    print("=" * 70)
    print("TOP MULTIBAGGER CANDIDATES — ENERGY")
    print("=" * 70)

    if score_multi:

        temp_multi = df_multi.copy()

        temp_multi["_score"] = numeric_series(
            temp_multi,
            score_multi
        )

        temp_multi = temp_multi.sort_values(
            "_score",
            ascending=False
        )

        cols = [
            c for c in [
                ticker_multi,
                score_multi
            ]
            if c
        ]

        if cols:
            display(
                temp_multi[cols]
                .head(10)
                .reset_index(drop=True)
            )

        # Visualisasi
        if ticker_multi:

            chart = (
                temp_multi[
                    [ticker_multi, "_score"]
                ]
                .dropna()
                .head(10)
                .sort_values("_score")
            )

            if not chart.empty:

                plt.figure(figsize=(10, 5))

                plt.barh(
                    chart[ticker_multi].astype(str),
                    chart["_score"]
                )

                plt.title(
                    "Top Multibagger Candidates — Energy"
                )

                plt.xlabel("Multibagger Score")
                plt.ylabel("Stock")

                plt.tight_layout()
                plt.show()

    else:
        print(
            "Score column tidak teridentifikasi.\n"
            "Available columns:",
            list(df_multi.columns)
        )


# ============================================================
# BREAKOUT RANKING
# ============================================================

if not df_breakout.empty:

    print("\n" + "=" * 70)
    print("BREAKOUT ALERTS")
    print("=" * 70)

    if score_breakout:

        temp_break = df_breakout.copy()

        temp_break["_score"] = numeric_series(
            temp_break,
            score_breakout
        )

        temp_break = temp_break.sort_values(
            "_score",
            ascending=False
        )

        cols = [
            c for c in [
                ticker_breakout,
                score_breakout
            ]
            if c
        ]

        if cols:
            display(
                temp_break[cols]
                .head(10)
                .reset_index(drop=True)
            )

    else:

        display(
            df_breakout
            .head(10)
            .reset_index(drop=True)
        )


# ============================================================
# SECTOR ROTATION
# ============================================================

if not df_sector.empty:

    print("\n" + "=" * 70)
    print("SECTOR ROTATION")
    print("=" * 70)

    if rotation_col:

        temp_sector = df_sector.copy()

        temp_sector["_rotation"] = numeric_series(
            temp_sector,
            rotation_col
        )

        temp_sector = temp_sector.sort_values(
            "_rotation",
            ascending=False
        )

        cols = [
            c for c in [
                sector_col,
                rotation_col
            ]
            if c
        ]

        if cols:
            display(
                temp_sector[cols]
                .head(10)
                .reset_index(drop=True)
            )

        if sector_col:

            chart = (
                temp_sector[
                    [sector_col, "_rotation"]
                ]
                .dropna()
                .head(10)
                .sort_values("_rotation")
            )

            if not chart.empty:

                plt.figure(figsize=(10, 5))

                plt.barh(
                    chart[sector_col].astype(str),
                    chart["_rotation"]
                )

                plt.title("IDX Sector Rotation Ranking")
                plt.xlabel(rotation_col)
                plt.ylabel("Sector")

                plt.tight_layout()
                plt.show()

    else:

        display(df_sector.head(10))


print("\n✅ ANALYSIS COMPLETED")

Explanation

Cell 5 performs the final market analysis.

The find_col() function automatically searches for relevant columns using several possible names. This makes the analysis more flexible when the API uses different field names.

For the Multibagger analysis, the program identifies the ticker and score columns, converts the score into numeric format, ranks the candidates from highest to lowest, and displays the top ten Energy-sector stocks. A horizontal bar chart is also generated to visualize the ranking.

The Breakout Alert section follows a similar process by detecting ticker and breakout-related score fields and ranking the available candidates.

Finally, the Sector Rotation section identifies the sector and rotation-related variables. The sectors are ranked according to the detected score, momentum, performance, or change field, and the top results are visualized using a horizontal bar chart.


Conclusion

This project demonstrates how Python can be integrated with the Indonesia Stock Exchange API to create an automated stock screening workflow.

The five-cell structure covers the complete process from API configuration and data retrieval to JSON normalization, ranking, and visualization. The dashboard combines Sector Rotation, Energy Multibagger Candidates, and Breakout Alerts into a single analytical workflow.

The results are suitable for initial market screening and identifying stocks or sectors that require further analysis. However, multibagger scores, breakout alerts, and sector rotation rankings should not be interpreted as direct investment recommendations. Fundamental analysis, valuation, liquidity, technical confirmation, and risk assessment should still be performed before making an investment decision.
Overall, this IDX stock analysis project provides a simple workflow for retrieving, processing, ranking, and visualizing Indonesia Stock Exchange data using Python.