OHLC.dev editorialIDX

IDX Retail Analysis Dashboard with Sector Rotation Risk Reward and Multibagger Scan

This project builds an IDX Retail Analysis Dashboard using Python and RapidAPI. It combines Sector Rotation, Risk Reward analysis for BBCA, and a Multibagger Scan for the Energy sector in a complete five-cell Google Colab workflow.

August 16, 20269 min readRafatar
IDX Retail Analysis Dashboard with Sector Rotation Risk Reward and Multibagger Scan

An IDX Retail Analysis Dashboard provides a practical way to combine several stock market analysis tools in one Python project.

This project uses three API endpoints: getSectorRotation, calculateRiskReward, and scanMultibagger. Risk Reward analysis uses BBCA with a 30-day period, Rp100,000,000 portfolio size, and 2% risk, while the Multibagger Scan focuses on the Energy sector with a minimum score of 50 and a maximum of 20 results.

The notebook consists of five cells covering setup, API requests, normalization, data tables, and the final dashboard.

Cell 1 — Setup Library API Key and Configuration

# ============================================================
# CELL 1 - SETUP LIBRARY, API KEY, DAN KONFIGURASI
# ============================================================

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-host": "indonesia-stock-exchange-idx.p.rapidapi.com",
    "x-rapidapi-key": RAPIDAPI_KEY
}

# ============================================================
# ENDPOINT
# ============================================================

SECTOR_ROTATION_URL = (
    f"{BASE_URL}/api/analysis/retail/sector-rotation"
)

RISK_REWARD_URL = (
    f"{BASE_URL}/api/analysis/retail/risk-reward/BBCA"
    "?days=30"
    "&portfolio_size=100000000"
    "&risk_percent=2"
)

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

print("=" * 100)
print("IDX RETAIL ANALYSIS PROJECT")
print("=" * 100)

print("\nEndpoint yang digunakan:")
print("1. getSectorRotation")
print("2. calculateRiskReward")
print("3. scanMultibagger")

print("\nKonfigurasi:")
print("Risk Reward Symbol   : BBCA")
print("Risk Reward Days     : 30")
print("Portfolio Size       : Rp100.000.000")
print("Risk Percent         : 2%")
print("Multibagger Sector   : Energy")
print("Minimum Score        : 50")
print("Maximum Results      : 20")

print("\n✅ Setup selesai.")

Cell 1 prepares the libraries, RapidAPI configuration, and all three endpoint URLs used by the project.

Cell 2 — Request Data from Three API Endpoints

# ============================================================
# CELL 2 - REQUEST DATA DARI 3 ENDPOINT API
# ============================================================

def request_api(name, url):
    print("\n" + "=" * 100)
    print(name)
    print("-" * 100)
    print("Request URL :", url)

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

        print("Status      :", response.status_code)

        # ----------------------------------------------------
        # SUCCESS
        # ----------------------------------------------------
        if response.status_code == 200:

            try:
                result = response.json()

                success = (
                    result.get("success")
                    if isinstance(result, dict)
                    else True
                )

                print("Success     :", success)
                print("Response    : Berhasil diterima.")

                return result

            except Exception as e:
                print("JSON Error  :", e)
                return None

        # ----------------------------------------------------
        # RATE LIMIT
        # ----------------------------------------------------
        elif response.status_code == 429:
            print("Success     : False")
            print("Pesan       : Rate limit API tercapai.")

        # ----------------------------------------------------
        # FORBIDDEN
        # ----------------------------------------------------
        elif response.status_code == 403:
            print("Success     : False")
            print("Pesan       : Akses API ditolak / subscription diperlukan.")

        # ----------------------------------------------------
        # VALIDATION ERROR
        # ----------------------------------------------------
        elif response.status_code == 422:
            print("Success     : False")
            print("Pesan       : Parameter request ditolak oleh API.")

        else:
            print("Success     : False")
            print("Pesan       : Request gagal.")

        try:
            print("\nResponse API:")
            print(
                json.dumps(
                    response.json(),
                    indent=2,
                    ensure_ascii=False
                )[:3000]
            )
        except:
            print(response.text[:3000])

        return None

    except requests.exceptions.Timeout:
        print("Status      : Timeout")
        print("Success     : False")
        print("Pesan       : Request melebihi batas waktu 30 detik.")
        return None

    except requests.exceptions.RequestException as e:
        print("Status      : Request Error")
        print("Success     : False")
        print("Pesan       :", str(e))
        return None


# ============================================================
# REQUEST 1 - SECTOR ROTATION
# ============================================================

sector_response = request_api(
    "getSectorRotation",
    SECTOR_ROTATION_URL
)

print("\n⏳ Jeda 5 detik sebelum request berikutnya...")
time.sleep(5)


# ============================================================
# REQUEST 2 - RISK REWARD BBCA
# ============================================================

risk_response = request_api(
    "calculateRiskReward",
    RISK_REWARD_URL
)

print("\n⏳ Jeda 5 detik sebelum request berikutnya...")
time.sleep(5)


# ============================================================
# REQUEST 3 - MULTIBAGGER SCAN
# ============================================================

multibagger_response = request_api(
    "scanMultibagger",
    MULTIBAGGER_URL
)

print("\n" + "=" * 100)
print("SELURUH REQUEST SELESAI")
print("=" * 100)

Cell 2 requests all three datasets and includes handling for successful requests, rate limits, forbidden access, validation errors, timeouts, and request failures.

Cell 3 — Normalize and Debug API Responses

# ============================================================
# CELL 3 - NORMALISASI DAN DEBUG STRUKTUR RESPONSE
# ============================================================

def extract_data(response):
    """
    Mengambil bagian data secara aman dari response API.
    """

    if response is None:
        return None

    if isinstance(response, dict):

        if "data" in response:
            return response["data"]

        if "result" in response:
            return response["result"]

        return response

    return response


def normalize_to_records(data):
    """
    Mengubah response menjadi list of dictionaries
    tanpa memaksakan struktur tertentu.
    """

    if data is None:
        return []

    # Sudah berupa list
    if isinstance(data, list):
        return data

    # Dictionary
    if isinstance(data, dict):

        # Cari list utama di dalam dictionary
        preferred_keys = [
            "results",
            "stocks",
            "sectors",
            "data",
            "items",
            "companies",
            "recommendations",
            "candidates"
        ]

        for key in preferred_keys:
            value = data.get(key)

            if isinstance(value, list):
                return value

        # Kalau tidak ada list, simpan dictionary sebagai 1 record
        return [data]

    return [{"value": data}]


# ============================================================
# EXTRACT DATA
# ============================================================

sector_data = extract_data(sector_response)
risk_data = extract_data(risk_response)
multibagger_data = extract_data(multibagger_response)


# ============================================================
# NORMALIZE
# ============================================================

sector_records = normalize_to_records(sector_data)
risk_records = normalize_to_records(risk_data)
multibagger_records = normalize_to_records(multibagger_data)


# ============================================================
# DATAFRAME
# ============================================================

try:
    sector_df = pd.json_normalize(sector_records)
except:
    sector_df = pd.DataFrame()

try:
    risk_df = pd.json_normalize(risk_records)
except:
    risk_df = pd.DataFrame()

try:
    multibagger_df = pd.json_normalize(multibagger_records)
except:
    multibagger_df = pd.DataFrame()


# ============================================================
# HASIL NORMALISASI
# ============================================================

print("=" * 100)
print("HASIL NORMALISASI")
print("=" * 100)

print("Sector Rotation Records :", len(sector_records))
print("Risk Reward Records     :", len(risk_records))
print("Multibagger Records     :", len(multibagger_records))


# ============================================================
# DEBUG STRUKTUR RESPONSE
# ============================================================

print("\n" + "=" * 100)
print("STRUKTUR RESPONSE")
print("=" * 100)


def debug_response(name, response, data):

    print(f"\n{name}")
    print("-" * 100)

    print(
        "Tipe Response :",
        type(response).__name__
        if response is not None
        else "NoneType"
    )

    if isinstance(response, dict):
        print("Response Keys :", list(response.keys()))

    print(
        "Tipe Data     :",
        type(data).__name__
        if data is not None
        else "NoneType"
    )

    if isinstance(data, dict):
        print("Data Keys     :", list(data.keys()))

    elif isinstance(data, list):
        print("Jumlah Item   :", len(data))

        if data and isinstance(data[0], dict):
            print("First Keys    :", list(data[0].keys()))


debug_response(
    "Sector Rotation",
    sector_response,
    sector_data
)

debug_response(
    "Risk Reward",
    risk_response,
    risk_data
)

debug_response(
    "Multibagger Scan",
    multibagger_response,
    multibagger_data
)


# ============================================================
# PREVIEW
# ============================================================

print("\n" + "=" * 100)
print("PREVIEW HASIL NORMALISASI")
print("=" * 100)

print("\nSector Rotation Preview:")
if sector_records:
    print(json.dumps(
        sector_records[:2],
        indent=2,
        ensure_ascii=False,
        default=str
    )[:3000])
else:
    print("Tidak ada data Sector Rotation.")

print("\nRisk Reward Preview:")
if risk_records:
    print(json.dumps(
        risk_records[:1],
        indent=2,
        ensure_ascii=False,
        default=str
    )[:3000])
else:
    print("Tidak ada data Risk Reward.")

print("\nMultibagger Preview:")
if multibagger_records:
    print(json.dumps(
        multibagger_records[:3],
        indent=2,
        ensure_ascii=False,
        default=str
    )[:3000])
else:
    print("Tidak ada data Multibagger.")

Cell 3 extracts and normalizes each API response into records and pandas DataFrames. It also provides response structure debugging and short data previews.

Cell 4 — Display Analysis Data Tables

# ============================================================
# CELL 4 - TABEL ANALISIS DATA
# ============================================================

print("=" * 100)
print("IDX RETAIL ANALYSIS - DATA TABLES")
print("=" * 100)


def show_dataframe(title, df, max_rows=20):

    print("\n" + "=" * 100)
    print(title)
    print("=" * 100)

    if df is None or df.empty:
        print("Tidak ada data.")
        return

    print("Jumlah Record :", len(df))
    print("Jumlah Kolom  :", len(df.columns))

    print("\nKolom tersedia:")
    print(", ".join(map(str, df.columns)))

    print("\nData:")
    display(
        df.head(max_rows).reset_index(drop=True)
    )


# ============================================================
# 1. SECTOR ROTATION
# ============================================================

show_dataframe(
    "1. SECTOR ROTATION",
    sector_df,
    20
)


# ============================================================
# 2. RISK REWARD BBCA
# ============================================================

show_dataframe(
    "2. RISK REWARD - BBCA",
    risk_df,
    20
)


# ============================================================
# 3. MULTIBAGGER ENERGY
# ============================================================

show_dataframe(
    "3. MULTIBAGGER SCAN - ENERGY",
    multibagger_df,
    20
)


# ============================================================
# BASIC SUMMARY
# ============================================================

print("\n" + "=" * 100)
print("DATA AVAILABILITY")
print("=" * 100)

print(
    "Sector Rotation :",
    "Tersedia" if not sector_df.empty else "Tidak tersedia"
)

print(
    "Risk Reward     :",
    "Tersedia" if not risk_df.empty else "Tidak tersedia"
)

print(
    "Multibagger     :",
    "Tersedia" if not multibagger_df.empty else "Tidak tersedia"
)

Cell 4 displays all three normalized datasets and provides a simple availability summary before the final dashboard.

Cell 5 — Dashboard and Final Summary

# ============================================================
# CELL 5 - DASHBOARD DAN RINGKASAN AKHIR
# ============================================================

print("=" * 110)
print("IDX RETAIL ANALYSIS DASHBOARD")
print("=" * 110)


# ============================================================
# HELPER
# ============================================================

def get_first_value(record, keys, default="-"):

    if not isinstance(record, dict):
        return default

    # Exact key
    for key in keys:
        value = record.get(key)

        if value not in [None, "", [], {}]:
            return value

    # Recursive search
    def recursive_search(obj):

        if isinstance(obj, dict):

            for k, v in obj.items():

                if str(k).lower() in [
                    str(x).lower()
                    for x in keys
                ]:
                    if v not in [None, "", [], {}]:
                        return v

            for v in obj.values():
                result = recursive_search(v)

                if result is not None:
                    return result

        elif isinstance(obj, list):

            for item in obj:
                result = recursive_search(item)

                if result is not None:
                    return result

        return None

    found = recursive_search(record)

    return found if found is not None else default


def format_number(value):

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

    try:
        value = float(value)

        if value >= 1_000_000_000:
            return f"{value / 1_000_000_000:,.2f} B"

        if value >= 1_000_000:
            return f"{value / 1_000_000:,.2f} M"

        return f"{value:,.2f}"

    except:
        return str(value)


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

print("\n📈 SECTOR ROTATION")
print("-" * 110)

if sector_records:

    for i, item in enumerate(sector_records[:10], start=1):

        sector = get_first_value(
            item,
            ["sector", "sectorName", "name"]
        )

        status = get_first_value(
            item,
            [
                "status",
                "signal",
                "rotation",
                "trend",
                "recommendation",
                "phase"
            ]
        )

        score = get_first_value(
            item,
            [
                "score",
                "rotationScore",
                "strength",
                "momentumScore"
            ]
        )

        print(
            f"{i:02d}. "
            f"{str(sector):20} | "
            f"Status : {str(status):15} | "
            f"Score : {score}"
        )

else:
    print("Tidak ada data Sector Rotation.")


# ============================================================
# RISK REWARD
# ============================================================

print("\n\n⚖️ RISK REWARD ANALYSIS - BBCA")
print("-" * 110)

if risk_records:

    risk_item = risk_records[0]

    symbol = get_first_value(
        risk_item,
        ["symbol", "ticker"],
        "BBCA"
    )

    current_price = get_first_value(
        risk_item,
        [
            "currentPrice",
            "current_price",
            "price",
            "entryPrice",
            "entry_price"
        ]
    )

    stop_loss = get_first_value(
        risk_item,
        [
            "stopLoss",
            "stop_loss",
            "stopLossPrice"
        ]
    )

    target = get_first_value(
        risk_item,
        [
            "targetPrice",
            "target_price",
            "takeProfit",
            "take_profit"
        ]
    )

    ratio = get_first_value(
        risk_item,
        [
            "riskRewardRatio",
            "risk_reward_ratio",
            "ratio",
            "rrRatio"
        ]
    )

    position_size = get_first_value(
        risk_item,
        [
            "positionSize",
            "position_size",
            "recommendedPositionSize",
            "maxPosition"
        ]
    )

    recommendation = get_first_value(
        risk_item,
        [
            "recommendation",
            "signal",
            "action"
        ]
    )

    print("Symbol              :", symbol)
    print("Current / Entry     :", format_number(current_price))
    print("Stop Loss           :", format_number(stop_loss))
    print("Target Price        :", format_number(target))
    print("Risk Reward Ratio   :", ratio)
    print("Position Size       :", format_number(position_size))
    print("Recommendation      :", recommendation)

else:
    print("Tidak ada data Risk Reward BBCA.")


# ============================================================
# MULTIBAGGER
# ============================================================

print("\n\n🚀 MULTIBAGGER SCAN - ENERGY")
print("-" * 110)

if multibagger_records:

    for i, item in enumerate(
        multibagger_records[:20],
        start=1
    ):

        symbol = get_first_value(
            item,
            ["symbol", "ticker", "code"]
        )

        company = get_first_value(
            item,
            [
                "companyName",
                "company_name",
                "name"
            ]
        )

        score = get_first_value(
            item,
            [
                "score",
                "multibaggerScore",
                "totalScore"
            ]
        )

        price = get_first_value(
            item,
            [
                "price",
                "currentPrice",
                "current_price"
            ]
        )

        recommendation = get_first_value(
            item,
            [
                "recommendation",
                "signal",
                "rating"
            ]
        )

        print(
            f"{i:02d}. "
            f"{str(symbol):8} | "
            f"{str(company)[:25]:25} | "
            f"Score : {str(score):8} | "
            f"Price : {format_number(price):12} | "
            f"{recommendation}"
        )

else:
    print("Tidak ada kandidat Multibagger.")


# ============================================================
# RINGKASAN
# ============================================================

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

print(
    f"📈 Sector Rotation : "
    f"{len(sector_records)} record"
)

print(
    f"⚖️ Risk Reward     : "
    f"{len(risk_records)} record"
)

print(
    f"🚀 Multibagger     : "
    f"{len(multibagger_records)} record"
)

print()

print(
    "Status Sector Rotation :",
    "Berhasil"
    if sector_response is not None
    else "Gagal / Tidak ada response"
)

print(
    "Status Risk Reward     :",
    "Berhasil"
    if risk_response is not None
    else "Gagal / Tidak ada response"
)

print(
    "Status Multibagger     :",
    "Berhasil"
    if multibagger_response is not None
    else "Gagal / Tidak ada response"
)

print("\nParameter Analisis:")
print("• Risk Reward Symbol : BBCA")
print("• Periode             : 30 hari")
print("• Portfolio           : Rp100.000.000")
print("• Risk per Trade      : 2%")
print("• Multibagger Sector  : Energy")
print("• Minimum Score       : 50")
print("• Maximum Candidates  : 20")

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

print("=" * 110)

Cell 5 builds the final IDX Retail Analysis Dashboard by displaying Sector Rotation signals, BBCA Risk Reward information, Energy Multibagger candidates, API status, analysis parameters, and the final processing timestamp.

Final Result

result

The completed project combines three different retail market analysis tools into one workflow:

  • Sector Rotation for identifying sector movement.

  • Risk Reward analysis for BBCA.

  • Multibagger screening for the Energy sector.

The notebook also includes API error handling, flexible response normalization, DataFrame inspection, recursive value extraction, and a final dashboard.

Conclusion

The IDX Retail Analysis Dashboard demonstrates how Python and RapidAPI can combine Sector Rotation, Risk Reward, and Multibagger screening in a single five-cell Google Colab project.

The workflow keeps the process simple while still covering API requests, data normalization, analysis tables, and dashboard output. It can also serve as a foundation for developing more advanced IDX stock screening and market analysis projects.