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

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.
