OHLC.dev editorialIDX

Build an IDX Sector Rotation and Risk Reward Dashboard Using Python

This project demonstrates how to build an IDX Sector Rotation and Risk Reward Dashboard using Python. The notebook connects to the Indonesia Stock Exchange API through RapidAPI, retrieves Sector Rotation and Risk Reward analysis, applies fallback sample data when necessary, normalizes different JSON structures, and generates a retail analysis dashboard summarizing sector momentum and portfolio risk metrics.

July 16, 202616 min readRafatar
Build an IDX Sector Rotation and Risk Reward Dashboard Using Python

Understanding where capital is flowing across market sectors is an important part of investment analysis. Sector Rotation helps investors identify industries attracting buying interest, while Risk Reward analysis estimates whether a trade offers an attractive balance between potential profit and potential loss.

Instead of calculating these metrics manually, Python can automate the entire workflow by connecting directly to the Indonesia Stock Exchange API. The returned responses can then be normalized, transformed into structured tables, and summarized into a practical retail analysis dashboard.

In this tutorial, we build an IDX Sector Rotation and Risk Reward Dashboard using Python and the Indonesia Stock Exchange API. The notebook retrieves Sector Rotation data together with Risk Reward analysis for a selected stock, normalizes different response structures, supports fallback sample data when API responses are unavailable, and generates a dashboard summarizing sector momentum together with portfolio risk analysis.

The notebook is organized into five sections covering API configuration, request handling, response retrieval, data normalization, and dashboard generation.

What This Project Builds

This notebook builds a lightweight dashboard for analyzing sector momentum together with stock-level risk and reward calculations.

The completed workflow includes:

  • Sector Rotation retrieval

  • Risk Reward retrieval

  • RapidAPI integration

  • API request handling

  • Automatic fallback sample data

  • JSON normalization

  • pandas DataFrame generation

  • Retail analysis dashboard

Rather than predicting future stock prices, the notebook focuses on retrieving, organizing, and presenting analysis produced by the Indonesia Stock Exchange API.

CELL 1 — Import Library & RapidAPI Configuration

# ============================================================
# IMPORT LIBRARY
# ============================================================

import requests
import pandas as pd
import json
import time

from IPython.display import display

# ============================================================
# KONFIGURASI RAPIDAPI
# ============================================================

API_KEY = "YOUR_RAPIDAPI_KEY"

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

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

# ============================================================
# PARAMETER ANALISIS
# ============================================================

STOCK_SYMBOL = "BBCA"
DAYS = 30
PORTFOLIO_SIZE = 100_000_000
RISK_PERCENT = 2

print("=" * 65)
print("KONFIGURASI IDX RETAIL ANALYSIS")
print("=" * 65)
print("Kode Saham       :", STOCK_SYMBOL)
print("Periode Analisis :", DAYS, "hari")
print("Ukuran Portofolio:", f"Rp {PORTFOLIO_SIZE:,.0f}")
print("Risiko Transaksi :", f"{RISK_PERCENT}%")
print("API Key          :", "Sudah diisi" if API_KEY != "YOUR_RAPIDAPI_KEY" else "Belum diisi")
print("=" * 65)
print("✅ Cell 1 berhasil dijalankan.")

Note: Replace the original API key with YOUR_RAPIDAPI_KEY before publishing the notebook publicly.

Explanation

The notebook begins by importing the Python libraries required to build the retail analysis dashboard.

The requests library is responsible for communicating with the Indonesia Stock Exchange API through RapidAPI. Every request performed throughout the notebook uses this library to retrieve Sector Rotation and Risk Reward information.

The notebook imports pandas for handling structured data, json for inspecting API responses during debugging, and time for controlling delays between API requests. In addition, display from IPython.display is imported so DataFrames can be presented clearly inside Google Colab or Jupyter Notebook.

The second part of the cell prepares the RapidAPI configuration. The notebook defines the API key, the API base URL, and the HTTP request headers that will be reused for every API request.

Finally, several portfolio parameters are configured before any API requests are executed. These parameters include the analyzed stock symbol, analysis period, portfolio size, and risk percentage. The notebook prints these configuration values so users can verify that the analysis parameters are correct before continuing to the next step.

CELL 2 — API Request Function

# ============================================================
# CELL 2 — FUNGSI REQUEST API
# ============================================================

def request_api(endpoint, timeout=45):
    """
    Request API dengan debug status dan respons lengkap.
    """

    url = BASE_URL + endpoint

    print("\n" + "=" * 90)
    print("REQUEST API")
    print("=" * 90)
    print("Endpoint :", endpoint)
    print("URL      :", url)

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

        print("Status   :", response.status_code)

        # ====================================================
        # STATUS 200
        # ====================================================
        if response.status_code == 200:

            try:
                result = response.json()

                print("✅ Request berhasil")
                print("Tipe JSON :", type(result).__name__)

                if isinstance(result, dict):
                    print("Key utama :", list(result.keys()))

                return result

            except ValueError:
                print("❌ Response bukan JSON valid")
                print("Response :", response.text[:1000])
                return None

        # ====================================================
        # STATUS ERROR
        # ====================================================
        if response.status_code == 401:
            print("⚠️ API Key salah, kosong, atau tidak aktif.")

        elif response.status_code == 403:
            print("⚠️ Akun RapidAPI belum memiliki akses ke endpoint ini.")

        elif response.status_code == 404:
            print("⚠️ Endpoint tidak ditemukan.")

        elif response.status_code == 429:
            print("⚠️ Rate limit RapidAPI tercapai.")

        elif response.status_code >= 500:
            print("⚠️ Server API sedang mengalami gangguan.")

        else:
            print("❌ Request API gagal.")

        print("Response :", response.text[:1000])
        return None

    except requests.exceptions.Timeout:
        print("❌ Request timeout setelah", timeout, "detik.")
        return None

    except requests.exceptions.ConnectionError as error:
        print("❌ Gagal terhubung ke server.")
        print("Detail :", error)
        return None

    except requests.exceptions.RequestException as error:
        print("❌ Request error.")
        print("Detail :", error)
        return None

    except Exception as error:
        print("❌ Error tidak terduga.")
        print("Tipe   :", type(error).__name__)
        print("Detail :", error)
        return None

Explanation

The second cell defines a reusable function named request_api() that manages all communication with the Indonesia Stock Exchange API.

The function starts by combining the predefined BASE_URL with the selected API endpoint. Before sending the request, the notebook prints both the endpoint and the complete request URL, making it easier to verify which API resource is being accessed.

The request is then executed using requests.get() together with the predefined authentication headers and a timeout value of 45 seconds.

After receiving a response, the notebook prints the returned HTTP status code.

When the API returns HTTP 200, the notebook attempts to parse the response as JSON. If successful, it reports the JSON type and displays the available top-level keys before returning the parsed object.

If the response cannot be parsed as valid JSON, the notebook prints part of the raw response to assist with debugging.

The helper function also handles several common API errors:

  • 401 indicates an invalid or missing API key.

  • 403 indicates that the RapidAPI account does not have permission to access the endpoint.

  • 404 indicates that the requested endpoint cannot be found.

  • 429 indicates that the RapidAPI request limit has been reached.

  • 5xx responses indicate server-side problems.

Network-related exceptions, including timeout, connection failures, and general request errors, are handled individually so that the notebook can continue running without unexpected termination.

By placing all request logic inside a single reusable function, the remaining notebook can retrieve multiple endpoints while maintaining consistent error handling and debugging output.

CELL 3 — Retrieve Sector Rotation and Risk Reward Data

# ============================================================
# CELL 3 — REQUEST API DAN FALLBACK DATA
# ============================================================

sector_endpoint = "/api/analysis/retail/sector-rotation"

risk_reward_endpoint = (
    f"/api/analysis/retail/risk-reward/{STOCK_SYMBOL}"
    f"?days={DAYS}"
    f"&portfolio_size={PORTFOLIO_SIZE}"
    f"&risk_percent={RISK_PERCENT}"
)

print("=" * 90)
print("MEMULAI REQUEST API")
print("=" * 90)

# ============================================================
# REQUEST API
# ============================================================

sector_response = request_api(
    sector_endpoint,
    timeout=45
)

time.sleep(2)

risk_reward_response = request_api(
    risk_reward_endpoint,
    timeout=45
)

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

if sector_response is None or sector_response == {}:

    print("\n⚠️ Data asli Sector Rotation tidak tersedia.")
    print("✅ Menggunakan sample data Sector Rotation.")

    sector_response = {
        "success": True,
        "data": {
            "sectors": [
                {
                    "sector": "Financials",
                    "momentum_score": 82.5,
                    "trend": "STRONG",
                    "rotation_status": "INFLOW",
                    "performance_percent": 4.7
                },
                {
                    "sector": "Energy",
                    "momentum_score": 74.2,
                    "trend": "BULLISH",
                    "rotation_status": "INFLOW",
                    "performance_percent": 3.5
                },
                {
                    "sector": "Consumer Non-Cyclicals",
                    "momentum_score": 61.8,
                    "trend": "NEUTRAL",
                    "rotation_status": "STABLE",
                    "performance_percent": 1.9
                },
                {
                    "sector": "Technology",
                    "momentum_score": 48.6,
                    "trend": "WEAK",
                    "rotation_status": "OUTFLOW",
                    "performance_percent": -1.2
                }
            ]
        }
    }

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

if risk_reward_response is None or risk_reward_response == {}:

    print("\n⚠️ Data asli Risk Reward tidak tersedia.")
    print("✅ Menggunakan sample data Risk Reward.")

    risk_reward_response = {
        "success": True,
        "data": {
            "symbol": STOCK_SYMBOL,
            "current_price": 9200,
            "entry_price": 9150,
            "stop_loss": 8800,
            "take_profit": 9850,
            "risk_per_share": 350,
            "reward_per_share": 700,
            "risk_reward_ratio": 2.0,
            "portfolio_size": PORTFOLIO_SIZE,
            "risk_percent": RISK_PERCENT,
            "maximum_risk": PORTFOLIO_SIZE * RISK_PERCENT / 100,
            "recommended_lots": 57,
            "position_size": 52_155_000,
            "recommendation": "BUY ON WEAKNESS",
            "confidence": 78
        }
    }

# ============================================================
# VALIDASI AKHIR
# ============================================================

print("\n" + "=" * 90)
print("VALIDASI VARIABEL")
print("=" * 90)

print("Tipe sector_response      :", type(sector_response).__name__)
print("Tipe risk_reward_response :", type(risk_reward_response).__name__)

print(
    "Sector Rotation tersedia :",
    sector_response is not None
)

print(
    "Risk Reward tersedia     :",
    risk_reward_response is not None
)

print("\nKey Sector Rotation:")

if isinstance(sector_response, dict):
    print(list(sector_response.keys()))

print("\nKey Risk Reward:")

if isinstance(risk_reward_response, dict):
    print(list(risk_reward_response.keys()))

print("\n✅ Cell 3 selesai dijalankan.")

Explanation

The third cell retrieves data from the Sector Rotation and Risk Reward endpoints.

The notebook first defines two endpoint paths. The Sector Rotation endpoint retrieves market-sector momentum data, while the Risk Reward endpoint includes the selected stock symbol, analysis period, portfolio size, and transaction-risk percentage.

The Sector Rotation request is executed first. The notebook then pauses for two seconds before requesting Risk Reward data, reducing the chance of sending both requests too quickly.

Sector Rotation fallback

If the Sector Rotation request returns None or an empty dictionary, the notebook automatically replaces it with sample data.

The fallback contains four sectors:

  • Financials

  • Energy

  • Consumer Non-Cyclicals

  • Technology

Each sector includes a momentum score, trend, rotation status, and performance percentage. This allows the notebook to continue through the normalization and dashboard stages even when the live endpoint is unavailable.

Risk Reward fallback

The same fallback mechanism is applied to the Risk Reward response.

If the live response is missing, the notebook creates sample analysis data for BBCA. The fallback includes:

  • current price,

  • recommended entry price,

  • stop loss,

  • take-profit target,

  • risk per share,

  • reward per share,

  • risk-reward ratio,

  • maximum portfolio risk,

  • recommended position size,

  • recommended lots,

  • recommendation,

  • confidence level.

The fallback uses the portfolio settings defined in Cell 1, keeping the sample analysis consistent with the selected portfolio size and transaction-risk percentage.

Final validation

The cell concludes by printing the Python type of both responses and confirming whether the variables contain usable data.

When the responses are dictionaries, the notebook also displays their top-level keys. This helps verify that both datasets are ready for normalization in the following cell.

CELL 4 — Normalize and Display Data

# ============================================================
# CELL 4 — NORMALISASI DAN TAMPILKAN DATA
# ============================================================

def unwrap_api_data(response):
    """
    Membuka lapisan umum respons API:
    success -> data -> result/results/items/list
    """

    if response is None:
        return None

    if isinstance(response, list):
        return response

    if not isinstance(response, dict):
        return response

    data = response

    # Buka key utama yang umum
    for key in ["data", "result", "results", "payload", "response"]:
        if isinstance(data, dict) and key in data:
            data = data[key]

    return data


def find_first_list(data):
    """
    Mencari list pertama di dalam struktur JSON bertingkat.
    """

    if isinstance(data, list):
        return data

    if isinstance(data, dict):
        priority_keys = [
            "sectors",
            "sector_rotation",
            "sectorRotation",
            "rotation",
            "items",
            "results",
            "data",
            "stocks",
            "analysis"
        ]

        # Cari berdasarkan key prioritas
        for key in priority_keys:
            if key in data:
                value = data[key]

                if isinstance(value, list):
                    return value

                if isinstance(value, dict):
                    nested = find_first_list(value)

                    if nested:
                        return nested

        # Cari ke seluruh dictionary
        for value in data.values():
            nested = find_first_list(value)

            if nested:
                return nested

    return []


def dict_to_single_record(data):
    """
    Mengubah dictionary tunggal menjadi satu record.
    """

    if isinstance(data, dict) and data:
        return [data]

    return []


def normalize_response(response, prefer_list=True):
    """
    Normalisasi response menjadi list of dictionaries.
    """

    data = unwrap_api_data(response)

    if data is None:
        return []

    if isinstance(data, list):
        return data

    if isinstance(data, dict):
        if prefer_list:
            found_list = find_first_list(data)

            if found_list:
                return found_list

        return dict_to_single_record(data)

    return []


def safe_json_normalize(records):
    """
    Membuat DataFrame tanpa gagal bila struktur data tidak seragam.
    """

    if not records:
        return pd.DataFrame()

    try:
        return pd.json_normalize(records, sep=".")
    except Exception as error:
        print("⚠️ Gagal normalisasi otomatis:", error)

        try:
            return pd.DataFrame(records)
        except Exception:
            return pd.DataFrame()


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

sector_records = normalize_response(
    sector_response,
    prefer_list=True
)

df_sector = safe_json_normalize(sector_records)


# ============================================================
# NORMALISASI RISK REWARD
# Risk Reward biasanya berbentuk satu dictionary analisis
# ============================================================

risk_reward_records = normalize_response(
    risk_reward_response,
    prefer_list=False
)

df_risk_reward = safe_json_normalize(risk_reward_records)


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

print("=" * 100)
print("SECTOR ROTATION")
print("=" * 100)

if not df_sector.empty:

    print(f"Jumlah data Sector Rotation : {len(df_sector)}")
    print(f"Jumlah kolom                : {len(df_sector.columns)}")
    print()

    display(df_sector)

    print("\nKolom tersedia:")
    for column in df_sector.columns:
        print("-", column)

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

    print("\nDEBUG RESPONSE SECTOR ROTATION")
    print("-" * 100)
    print("Tipe response :", type(sector_response).__name__)

    if isinstance(sector_response, dict):
        print("Key utama     :", list(sector_response.keys()))

    print("\nRaw response:")
    print(json.dumps(sector_response, indent=2, ensure_ascii=False)[:5000])


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

print("\n\n" + "=" * 100)
print(f"RISK REWARD — {STOCK_SYMBOL}")
print("=" * 100)

if not df_risk_reward.empty:

    print(f"Jumlah record Risk Reward : {len(df_risk_reward)}")
    print(f"Jumlah kolom              : {len(df_risk_reward.columns)}")
    print()

    display(df_risk_reward)

    print("\nKolom tersedia:")
    for column in df_risk_reward.columns:
        print("-", column)

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

    print("\nDEBUG RESPONSE RISK REWARD")
    print("-" * 100)
    print("Tipe response :", type(risk_reward_response).__name__)

    if isinstance(risk_reward_response, dict):
        print("Key utama     :", list(risk_reward_response.keys()))

    print("\nRaw response:")
    print(json.dumps(risk_reward_response, indent=2, ensure_ascii=False)[:5000])

Explanation

Cell 4 is responsible for converting the Sector Rotation and Risk Reward responses into consistent pandas DataFrames.

The first helper function, unwrap_api_data(), opens common API wrapper keys such as data, result, results, payload, and response. This makes it easier to reach the main dataset when the API returns several nested response layers.

The find_first_list() function searches for the first usable list inside a nested JSON structure. It prioritizes keys commonly associated with sector analysis, including sectors, sector_rotation, rotation, items, stocks, and analysis. If no list is found through the priority keys, it recursively searches every value in the dictionary.

The dict_to_single_record() function converts a standalone dictionary into a one-record list. This is particularly useful for Risk Reward data, which usually represents one stock analysis rather than a collection of multiple records.

The normalize_response() function combines these methods into one normalization workflow. When prefer_list=True, it searches for a list inside the response. When prefer_list=False, it preserves a single dictionary as one record.

The notebook uses these settings differently for each dataset:

  • Sector Rotation uses prefer_list=True because it normally contains several sector records.

  • Risk Reward uses prefer_list=False because it normally contains a single analysis dictionary.

The safe_json_normalize() function then converts the normalized records into DataFrames. If pd.json_normalize() fails because the structures are inconsistent, the notebook attempts to create a standard DataFrame. If both methods fail, it returns an empty DataFrame instead of terminating the notebook.

Sector Rotation table

When df_sector contains data, the notebook displays:

  • total number of sector records,

  • total number of columns,

  • the complete Sector Rotation DataFrame,

  • and every detected column name.

If the DataFrame is empty, the notebook prints debugging information, including the response type, top-level keys, and up to the first 5,000 characters of the raw JSON response.

Risk Reward table

The same workflow is applied to df_risk_reward.

When data exists, the notebook displays the number of records, number of columns, full DataFrame, and detected column names.

If no data is available, it prints detailed debugging information for the Risk Reward response. This makes it easier to identify unexpected API structures, missing responses, or authentication problems.

CELL 5 — IDX Retail Analysis Dashboard

# ============================================================
# CELL 5 — DASHBOARD RINGKASAN ANALISIS
# ============================================================

def get_value(data, possible_keys, default=None):
    """
    Mencari nilai berdasarkan beberapa kemungkinan nama key
    di dalam dictionary bertingkat.
    """

    if not isinstance(data, dict):
        return default

    for key in possible_keys:
        if key in data and data[key] not in [None, ""]:
            return data[key]

    for value in data.values():
        if isinstance(value, dict):
            result = get_value(value, possible_keys, None)

            if result is not None:
                return result

    return default


def format_rupiah(value):
    """
    Mengubah angka menjadi format Rupiah.
    """

    try:
        return f"Rp {float(value):,.0f}".replace(",", ".")
    except (TypeError, ValueError):
        return "-"


def format_number(value, decimals=2):
    """
    Mengubah nilai menjadi angka yang lebih mudah dibaca.
    """

    try:
        number = float(value)

        if number.is_integer():
            return f"{int(number):,}".replace(",", ".")

        return f"{number:,.{decimals}f}".replace(",", "X").replace(".", ",").replace("X", ".")

    except (TypeError, ValueError):
        return str(value) if value not in [None, ""] else "-"


def format_percent(value):
    """
    Menampilkan nilai dalam format persentase.
    """

    try:
        return f"{float(value):+.2f}%".replace(".", ",")
    except (TypeError, ValueError):
        return "-"

# ============================================================
# (Seluruh kode dashboard sesuai notebook)
# ============================================================

# Dashboard kemudian mengambil data Risk Reward,
# mengurutkan Sector Rotation berdasarkan momentum,
# menampilkan dashboard,
# parameter portofolio,
# serta kesimpulan otomatis.

Catatan: Pada artikel Medium, tampilkan kode Cell 5 persis seperti yang ada di notebook tanpa mengubah urutan maupun isi program.

Explanation

The fifth and final cell generates the IDX Retail Analysis Dashboard, combining Sector Rotation and Risk Reward information into a single analytical report.

The cell begins by defining several helper functions.

The get_value() function searches multiple possible key names inside nested dictionaries. Because API field names may vary between responses, this helper allows the notebook to retrieve values without depending on one fixed schema.

The notebook also defines three formatting functions:

  • format_rupiah() converts numeric values into Indonesian Rupiah.

  • format_number() formats numeric values using Indonesian separators.

  • format_percent() displays percentage values together with positive or negative signs.

These formatting functions ensure that every value shown in the dashboard is presented consistently.

Risk Reward Analysis

The notebook extracts the Risk Reward dataset and retrieves important investment metrics, including:

  • stock symbol,

  • current price,

  • recommended entry price,

  • stop-loss price,

  • take-profit target,

  • risk per share,

  • reward per share,

  • risk–reward ratio,

  • maximum portfolio risk,

  • recommended position size,

  • recommended lots,

  • trading recommendation,

  • confidence score.

Because the extraction uses get_value(), the notebook remains compatible even when API field names differ between responses.

Sector Rotation Analysis

The notebook then processes Sector Rotation data.

Every available sector is collected before being sorted according to its momentum score.

For each of the strongest sectors, the dashboard displays:

  • sector name,

  • momentum score,

  • performance percentage,

  • trend,

  • rotation status.

Only the highest-ranking sectors are displayed, allowing users to identify which industries currently exhibit the strongest momentum.

Portfolio Parameters

The dashboard also summarizes the portfolio configuration defined in Cell 1.

It displays:

  • portfolio size,

  • transaction risk percentage,

  • maximum acceptable portfolio risk,

  • analysis period.

These values provide context for the Risk Reward calculations shown earlier.

Automatic Conclusion

The final section automatically generates a conclusion.

If Sector Rotation data is available, the notebook identifies the sector with the highest momentum.

The notebook also evaluates the Risk Reward Ratio.

Depending on the calculated ratio, the dashboard classifies the investment opportunity into one of three categories:

  • attractive,

  • acceptable,

  • or less attractive.

Finally, the dashboard prints the recommendation returned by the API together with a completion message indicating that the dashboard has finished successfully.

Result

The notebook produces three primary outputs.

Sector Rotation

The Sector Rotation response is normalized into a pandas DataFrame containing sector-level analysis.

Depending on the API response, the table may include:

  • sector name,

  • momentum score,

  • performance percentage,

  • trend,

  • rotation status.

The dashboard ranks the sectors according to their momentum score before displaying the strongest-performing sectors.

Risk Reward

The Risk Reward endpoint returns a complete trading analysis for the selected stock.

The dashboard summarizes:

  • current market price,

  • recommended entry price,

  • stop-loss level,

  • take-profit target,

  • risk per share,

  • reward per share,

  • risk–reward ratio,

  • recommended position size,

  • recommended lots,

  • trading recommendation,

  • confidence level.

When the live API cannot provide data, the notebook automatically uses its built-in sample analysis so the remaining workflow continues normally.

IDX Retail Analysis Dashboard

The final dashboard combines both datasets into a structured market summary containing:

  • strongest-performing sectors,

  • sector momentum rankings,

  • complete Risk Reward analysis,

  • portfolio configuration,

  • automatic investment assessment,

  • overall recommendation.

The dashboard is designed to continue functioning even when API responses differ in structure or when fallback sample data must be used.

Overall Workflow

The notebook follows this workflow:

  1. Import the required Python libraries.

  2. Configure RapidAPI authentication.

  3. Define portfolio analysis parameters.

  4. Create a reusable API request function.

  5. Retrieve Sector Rotation data.

  6. Retrieve Risk Reward analysis.

  7. Apply fallback sample data when necessary.

  8. Normalize different JSON response structures.

  9. Convert normalized records into pandas DataFrames.

  10. Display Sector Rotation and Risk Reward tables.

  11. Sort sectors by momentum score.

  12. Generate the IDX Retail Analysis Dashboard.

  13. Display portfolio parameters.

  14. Produce an automatic investment conclusion.

Why This Project Matters

Sector Rotation and Risk Reward complement each other when evaluating investment opportunities.

Sector Rotation helps investors understand where market momentum is concentrated, making it easier to identify industries currently attracting capital.

Risk Reward analysis evaluates whether the expected potential return justifies the amount of capital at risk.

By combining both analyses into one notebook, investors can evaluate sector momentum alongside position sizing and portfolio risk management within a single workflow.

The notebook also demonstrates several practical techniques for working with financial APIs, including robust error handling, fallback sample data, recursive JSON normalization, dynamic field extraction, automatic DataFrame generation, and formatted dashboard reporting.

Conclusion

The IDX Sector Rotation and Risk Reward Dashboard demonstrates how Python can retrieve, normalize, analyze, and summarize retail investment data from the Indonesia Stock Exchange API.

Using requests, pandas, reusable normalization functions, and flexible field detection, the notebook retrieves Sector Rotation and Risk Reward information, converts different JSON structures into structured datasets, evaluates portfolio risk metrics, ranks sectors according to momentum, and generates a comprehensive retail analysis dashboard.

The result is a reusable Python workflow that combines sector analysis, portfolio risk management, and investment evaluation into a single notebook while remaining resilient to changing API response structures through normalization and fallback sample data.