OHLC.dev editorialIDX

BBCA Fundamental Analysis Dashboard with Holding Composition Emiten Info and Key Statistics

This project builds a BBCA Fundamental Analysis Dashboard using Python and RapidAPI. It combines Holding Composition, Emiten Information, and Key Statistics into a complete five-cell Google Colab workflow.

August 17, 20268 min readRafatar
BBCA Fundamental Analysis Dashboard with Holding Composition Emiten Info and Key Statistics

Introduction

A BBCA Fundamental Analysis Dashboard provides a structured way to explore company information, ownership data, and key statistics from one workflow.

This project uses three IDX API endpoints for BBCA: getHoldingComposition, getEmitenInfo, and getKeystats. The notebook covers API requests, response normalization, complete data inspection, and a final fundamental dashboard.

Cell 1 — Setup Import Library and API Configuration

# ============================================================
# CELL 1 — SETUP, IMPORT LIBRARY & KONFIGURASI API
# ============================================================

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
}

SYMBOL = "BBCA"

print("=" * 100)
print("IDX EMITEN FUNDAMENTAL ANALYSIS")
print("=" * 100)
print(f"Symbol : {SYMBOL}")
print("API    : Indonesia Stock Exchange IDX - RapidAPI")
print("Status : Konfigurasi berhasil")

Cell 1 imports the required libraries and prepares the RapidAPI configuration with BBCA as the main stock symbol.

Cell 2 — Request Three API Endpoints

# ============================================================
# CELL 2 — REQUEST API
# getHoldingComposition
# getEmitenInfo
# getKeystats
# ============================================================

def request_api(name, endpoint, params=None):

    url = BASE_URL + endpoint

    print("\n" + "=" * 100)
    print(name)
    print("-" * 100)
    print("Endpoint    :", endpoint)

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

        print("Request URL :", response.url)
        print("Status      :", response.status_code)

        try:
            data = response.json()
        except Exception:
            data = {
                "success": False,
                "error": "Invalid JSON Response",
                "message": response.text[:1000]
            }

        success = 200 <= response.status_code < 300

        print("Success     :", success)

        if success:
            print("Pesan       : Request berhasil.")
        elif response.status_code == 422:
            print("Pesan       : Parameter query ditolak oleh validasi endpoint.")
        elif response.status_code == 429:
            print("Pesan       : RapidAPI rate limit tercapai.")
        elif response.status_code == 401:
            print("Pesan       : Periksa RapidAPI Key.")
        elif response.status_code == 403:
            print("Pesan       : Endpoint mungkin tidak tersedia pada paket API Anda.")
        elif response.status_code >= 500:
            print("Pesan       : Server API mengalami gangguan.")
        else:
            print("Pesan       : Request API gagal.")

        if not success:
            print("\nResponse API:")
            try:
                print(json.dumps(data, indent=2, ensure_ascii=False))
            except Exception:
                print(data)

        return {
            "name": name,
            "endpoint": endpoint,
            "status": response.status_code,
            "success": success,
            "response": data
        }

    except requests.exceptions.Timeout:

        print("Status      : TIMEOUT")
        print("Success     : False")
        print("Pesan       : Request melebihi batas waktu.")

        return {
            "name": name,
            "endpoint": endpoint,
            "status": "TIMEOUT",
            "success": False,
            "response": {}
        }

    except Exception as e:

        print("Status      : ERROR")
        print("Success     : False")
        print("Pesan       :", str(e))

        return {
            "name": name,
            "endpoint": endpoint,
            "status": "ERROR",
            "success": False,
            "response": {}
        }


# ============================================================
# 1. HOLDING COMPOSITION
# ============================================================
# Endpoint menolak:
# value=5,12,24,36
#
# Maka gunakan satu nilai yang valid terlebih dahulu.
# ============================================================

holding_result = request_api(
    "getHoldingComposition",
    f"/api/emiten/{SYMBOL}/profile/holding-composition",
    {
        "value": 5,
        "type": "all"
    }
)


print("\n⏳ Memberikan jeda 3 detik sebelum request berikutnya...")
time.sleep(3)


# ============================================================
# 2. EMITEN INFO
# ============================================================

info_result = request_api(
    "getEmitenInfo",
    f"/api/emiten/{SYMBOL}/info"
)


print("\n⏳ Memberikan jeda 3 detik sebelum request berikutnya...")
time.sleep(3)


# ============================================================
# 3. KEYSTATS
# ============================================================

keystats_result = request_api(
    "getKeystats",
    f"/api/emiten/{SYMBOL}/keystats",
    {
        "year_limit": 10
    }
)

Cell 2 retrieves all three datasets and includes handling for validation errors, rate limits, API permissions, server errors, and request timeouts. A three-second delay separates the requests.

Cell 3 — Normalize API Responses

# ============================================================
# CELL 3 — NORMALISASI RESPONSE API
# ============================================================

def extract_data(response):

    if response is None:
        return []

    if isinstance(response, list):
        return response

    if not isinstance(response, dict):
        return []

    data = response.get("data")

    if data is None:
        return []

    return data


def normalize_data(data):

    if data is None:
        return pd.DataFrame()

    # --------------------------------------------------------
    # LIST
    # --------------------------------------------------------
    if isinstance(data, list):

        if len(data) == 0:
            return pd.DataFrame()

        if all(isinstance(x, dict) for x in data):
            return pd.json_normalize(data)

        return pd.DataFrame({
            "value": data
        })

    # --------------------------------------------------------
    # DICTIONARY
    # --------------------------------------------------------
    if isinstance(data, dict):

        try:
            df = pd.json_normalize(data)

            if not df.empty:
                return df

        except Exception:
            pass

        rows = []

        for key, value in data.items():

            if isinstance(value, (dict, list)):
                try:
                    value = json.dumps(
                        value,
                        ensure_ascii=False
                    )
                except:
                    value = str(value)

            rows.append({
                "key": key,
                "value": value
            })

        return pd.DataFrame(rows)

    # --------------------------------------------------------
    # SCALAR
    # --------------------------------------------------------
    return pd.DataFrame({
        "value": [data]
    })


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

holding_data = extract_data(
    holding_result.get("response")
)

info_data = extract_data(
    info_result.get("response")
)

keystats_data = extract_data(
    keystats_result.get("response")
)


# ============================================================
# NORMALISASI
# ============================================================

holding_df = normalize_data(holding_data)
info_df = normalize_data(info_data)
keystats_df = normalize_data(keystats_data)


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

print("Holding Composition Records :", len(holding_df))
print("Emiten Info Records         :", len(info_df))
print("Keystats Records            :", len(keystats_df))


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

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

for name, result in [
    ("Holding Composition", holding_result),
    ("Emiten Info", info_result),
    ("Keystats", keystats_result)
]:

    response_data = result.get("response", {})

    print(f"\n{name}:")
    print("Tipe :", type(response_data).__name__)

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

Cell 3 extracts the data section from each response and converts lists, dictionaries, and scalar values into pandas DataFrames. It also displays the record counts and original response structures.

Cell 4 — Display Complete Fundamental Data

# ============================================================
# CELL 4 — TAMPILKAN DATA LENGKAP
# ============================================================

pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", 100)
pd.set_option("display.max_colwidth", 150)
pd.set_option("display.width", 200)


def show_dataset(title, df, raw_data=None):

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

    if df is not None and not df.empty:

        print(f"Jumlah record : {len(df)}")
        print(f"Jumlah kolom  : {len(df.columns)}")

        print("\nKolom tersedia:")
        print(list(df.columns))

        print("\nData:")
        display(df)

    else:

        print("Tidak ada data yang berhasil dinormalisasi.")

        if raw_data not in [None, {}, []]:

            print("\nRaw Data:")

            try:
                print(
                    json.dumps(
                        raw_data,
                        indent=2,
                        ensure_ascii=False
                    )[:5000]
                )
            except:
                print(raw_data)


# ============================================================
# HOLDING COMPOSITION
# ============================================================

show_dataset(
    "HOLDING COMPOSITION",
    holding_df,
    holding_data
)


# ============================================================
# EMITEN INFO
# ============================================================

show_dataset(
    "EMITEN INFO",
    info_df,
    info_data
)


# ============================================================
# KEYSTATS
# ============================================================

show_dataset(
    "KEYSTATS",
    keystats_df,
    keystats_data
)

Cell 4 displays the complete normalized datasets with their available columns. Raw API data is used as a fallback when normalization does not produce a DataFrame.

Cell 5 — BBCA Emiten Fundamental Dashboard

# ============================================================
# CELL 5 — DASHBOARD & RINGKASAN AKHIR
# ============================================================

def status_text(result):

    if result.get("success"):
        return "Berhasil"

    status = result.get("status")

    if status == 422:
        return "Gagal - Validation Error"
    elif status == 429:
        return "Gagal - Rate Limit"
    elif status == 403:
        return "Gagal - Subscription / Permission"
    elif status == 401:
        return "Gagal - API Key"
    elif status == "TIMEOUT":
        return "Gagal - Timeout"

    return "Gagal"


# ============================================================
# HELPER UNTUK VALUE LIST / DICT / ARRAY
# ============================================================

def safe_value(value):

    if value is None:
        return "-"

    # Dictionary
    if isinstance(value, dict):
        if len(value) == 0:
            return "-"
        return json.dumps(value, ensure_ascii=False)

    # List / tuple / set
    if isinstance(value, (list, tuple, set)):
        if len(value) == 0:
            return "-"
        return ", ".join(str(x) for x in value)

    # Numpy array / pandas object
    if hasattr(value, "tolist"):
        try:
            converted = value.tolist()

            if isinstance(converted, list):
                return ", ".join(str(x) for x in converted)

            return str(converted)

        except Exception:
            pass

    # NaN
    try:
        if pd.isna(value):
            return "-"
    except Exception:
        pass

    return str(value)


def print_dataframe_preview(df, max_rows=10, max_cols=8):

    if df is None or df.empty:
        print("Data tidak tersedia.")
        return

    preview = df.head(max_rows)

    for number, (_, row) in enumerate(
        preview.iterrows(),
        start=1
    ):

        values = []

        for col in preview.columns[:max_cols]:

            value = safe_value(row.get(col))

            if value != "-":
                values.append(
                    f"{col}: {value}"
                )

        if values:
            print(
                f"{number:02}. " +
                " | ".join(values)
            )
        else:
            print(f"{number:02}. -")


# ============================================================
# DASHBOARD
# ============================================================

print("=" * 100)
print("IDX BBCA EMITEN FUNDAMENTAL DASHBOARD")
print("=" * 100)


# ============================================================
# HOLDING COMPOSITION
# ============================================================

print("\n🏦 Holding Composition")
print("-" * 100)

print(
    "Jumlah Data :",
    len(holding_df)
)

print(
    "Status      :",
    status_text(holding_result)
)

if not holding_df.empty:

    print("\nData Holding:")
    print_dataframe_preview(
        holding_df,
        max_rows=10,
        max_cols=8
    )

else:
    print("\nData Holding Composition tidak tersedia.")


# ============================================================
# EMITEN INFO
# ============================================================

print("\n\n🏢 Emiten Information")
print("-" * 100)

print(
    "Jumlah Data :",
    len(info_df)
)

print(
    "Status      :",
    status_text(info_result)
)

if not info_df.empty:

    row = info_df.iloc[0]

    important_fields = [
        "symbol",
        "code",
        "ticker",
        "name",
        "companyName",
        "company_name",
        "sector",
        "subsector",
        "industry",
        "subIndustry",
        "listingDate",
        "website"
    ]

    found = False

    for field in important_fields:

        if field in info_df.columns:

            value = safe_value(
                row.get(field)
            )

            if value != "-":

                print(
                    f"{field:<20}: {value}"
                )

                found = True

    # Jika nama field API berbeda
    if not found:

        print("\nInformasi tersedia:")

        for col in info_df.columns[:15]:

            value = safe_value(
                row.get(col)
            )

            if value != "-":
                print(
                    f"{col:<30}: {value}"
                )


# ============================================================
# KEYSTATS
# ============================================================

print("\n\n📊 Key Statistics")
print("-" * 100)

print(
    "Jumlah Data :",
    len(keystats_df)
)

print(
    "Status      :",
    status_text(keystats_result)
)

if not keystats_df.empty:

    row = keystats_df.iloc[0]

    print("\nStatistik tersedia:")

    shown = 0

    for col in keystats_df.columns:

        value = safe_value(
            row.get(col)
        )

        if value != "-":

            print(
                f"{col:<35}: {value}"
            )

            shown += 1

        # Batasi agar dashboard tidak terlalu panjang
        if shown >= 20:
            break

else:
    print("\nData Key Statistics tidak tersedia.")


# ============================================================
# RINGKASAN AKHIR
# ============================================================

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

print(
    f"🏦 Holding Composition : {len(holding_df)} record"
)

print(
    f"🏢 Emiten Info         : {len(info_df)} record"
)

print(
    f"📊 Keystats            : {len(keystats_df)} record"
)

print("\nStatus API")
print("-" * 100)

print(
    f"Holding Composition : {status_text(holding_result)}"
)

print(
    f"Emiten Info         : {status_text(info_result)}"
)

print(
    f"Keystats            : {status_text(keystats_result)}"
)


# ============================================================
# OVERALL STATUS
# ============================================================

successful_api = sum([
    bool(holding_result.get("success")),
    bool(info_result.get("success")),
    bool(keystats_result.get("success"))
])

print("\n" + "-" * 100)

print(
    f"API Berhasil : {successful_api}/3"
)

if successful_api == 3:
    print("Status Sistem : Semua endpoint berhasil diproses.")
elif successful_api > 0:
    print("Status Sistem : Sebagian endpoint berhasil diproses.")
else:
    print("Status Sistem : Seluruh endpoint gagal diproses.")

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

Cell 5 creates the final BBCA fundamental dashboard. The safe_value() helper safely handles dictionaries, lists, arrays, pandas values, and NaN values before they are displayed, preventing ambiguous array errors during dashboard generation.

The dashboard then presents Holding Composition, important Emiten Information, up to 20 available Key Statistics, individual API statuses, the number of successful endpoints, and the final processing timestamp.

Final Result

result

The completed project combines three fundamental datasets for BBCA:

Holding Composition provides ownership-related information, Emiten Info provides company profile information, and Key Statistics provides fundamental statistics from the API.

The notebook also includes API error handling, flexible response normalization, complete dataset inspection, safe handling of nested or array values, and an overall endpoint status.

Conclusion

The BBCA Fundamental Analysis Dashboard demonstrates how Python and RapidAPI can combine Holding Composition, Emiten Information, and Key Statistics into one five-cell Google Colab project.

The workflow covers the complete process from API configuration and data retrieval to normalization and dashboard presentation, providing a practical foundation for developing more advanced IDX fundamental analysis projects.