OHLC.dev editorialIDX

IDX Insider Retail Bandar IPO Momentum Dashboard for Smarter Stock Analysis

The article explains a five-cell Python Google Colab dashboard that uses IDX RapidAPI data to combine insider screening, retail sentiment, bandar or smart money sentiment, broker activity, and IPO momentum into one market analysis workflow.

August 23, 202614 min readRafatar
IDX Insider Retail Bandar IPO Momentum Dashboard for Smarter Stock Analysis

The IDX Insider Retail Bandar IPO Momentum Dashboard is a Python based market analysis project that brings several different market signals into one practical workflow.

Instead of examining insider activity retail sentiment bandar activity broker flows and IPO momentum separately the project combines them into a single Google Colab dashboard.

The project uses the IDX API through RapidAPI and is organized into five cells. Each cell has a specific role from API configuration and data retrieval to JSON processing and the final dashboard.

The analysis focuses on BBCA BUMI and ADRO for insider screening while the retail and bandar sentiment analysis focuses on BBCA over a seven day period.

The goal is simple: transform raw API responses into information that is easier for investors and market observers to read.

Cell 1 — Import and Configuration

# -*- coding: utf-8 -*-
"""getInsiderScreening getRetailBandarSentiment getIpoMomentum.ipynb

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/1FQ35MGYf5zRf2UBENmyX5IVuQqJ_PCne

CELL 1 — IMPORT & KONFIGURASI
"""

import requests
import pandas as pd
import json
from datetime import datetime

RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"
HOST = "indonesia-stock-exchange-idx.p.rapidapi.com"

HEADERS = {
    "Content-Type": "application/json",
    "x-rapidapi-host": HOST,
    "x-rapidapi-key": RAPIDAPI_KEY
}

SYMBOLS = "BBCA,BUMI,ADRO"
PERIOD = "2025-12"
SENTIMENT_SYMBOL = "BBCA"
SENTIMENT_DAYS = 7

print("=" * 100)
print("IDX INSIDER, RETAIL BANDAR SENTIMENT & IPO MOMENTUM ANALYSIS")
print("=" * 100)

Explanation

Cell 1 prepares the Python environment and establishes the main configuration for the project.

The code imports requests for API communication, pandas for data processing, json for handling API responses, and datetime for displaying the dashboard update time.

The configuration then defines the RapidAPI host and authentication headers.

Three stocks are selected for insider screening:

  • BBCA

  • BUMI

  • ADRO

The insider analysis period is December 2025, while the sentiment analysis focuses on BBCA using a seven day window.

This centralized configuration makes the project easier to modify because the main analysis parameters can be changed without rebuilding the rest of the code.

Cell 2 — Request Three API Endpoints

"""CELL 2 — REQUEST 3 API"""

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

        try:
            data = response.json()
        except:
            data = {"raw_response": response.text}

        return {
            "status_code": response.status_code,
            "success": response.ok,
            "data": data
        }

    except Exception as e:
        return {
            "status_code": None,
            "success": False,
            "data": {"error": str(e)}
        }


insider_result = api_get(
    "https://indonesia-stock-exchange-idx.p.rapidapi.com/api/analysis/insider-screening",
    {
        "symbols": SYMBOLS,
        "action_type": "ACTION_TYPE_UNSPECIFIED",
        "page": 1,
        "limit": 100,
        "source_type": "SOURCE_TYPE_UNSPECIFIED",
        "period": PERIOD
    }
)


sentiment_result = api_get(
    f"https://indonesia-stock-exchange-idx.p.rapidapi.com/api/analysis/sentiment/{SENTIMENT_SYMBOL}",
    {
        "days": SENTIMENT_DAYS
    }
)


ipo_result = api_get(
    "https://indonesia-stock-exchange-idx.p.rapidapi.com/api/analysis/sentiment/ipo/momentum"
)


print("=" * 100)
print("STATUS REQUEST")
print("-" * 100)
print(f"Insider Screening       : HTTP {insider_result['status_code']}")
print(f"Retail Bandar Sentiment : HTTP {sentiment_result['status_code']}")
print(f"IPO Momentum            : HTTP {ipo_result['status_code']}")
print("=" * 100)

Explanation

Cell 2 is responsible for communicating with the three IDX API endpoints.

The first endpoint retrieves insider screening data for the selected stocks.

The second endpoint retrieves sentiment information for BBCA using the selected seven day period.

The third endpoint retrieves IPO momentum information.

The api_get() function is designed to make the request reusable. It also captures the HTTP status code and attempts to decode the response as JSON.

If the response cannot be decoded as JSON the raw response is preserved instead. Connection or request errors are also captured so the notebook does not immediately stop when an API request fails.

This is particularly useful when working with RapidAPI because endpoints can return different HTTP responses depending on access permissions rate limits parameters or server conditions.

Cell 3 — Robust JSON Parser

"""CELL 3 — PROPER DATA PARSER"""

# ============================================================
# CELL 3 — ROBUST JSON PARSER
# ============================================================

def decode_json(value):
    if isinstance(value, str):
        text = value.strip()

        if text.startswith("{") or text.startswith("["):
            try:
                return decode_json(json.loads(text))
            except:
                return value

    if isinstance(value, dict):
        return {
            k: decode_json(v)
            for k, v in value.items()
        }

    if isinstance(value, list):
        return [
            decode_json(v)
            for v in value
        ]

    return value


def find_objects(obj, required_keys=None):
    """
    Mencari semua dictionary yang memiliki key tertentu
    di seluruh struktur JSON.
    """

    found = []

    if isinstance(obj, dict):

        if required_keys:

            if all(
                key in obj
                for key in required_keys
            ):
                found.append(obj)

        for value in obj.values():

            found.extend(
                find_objects(
                    value,
                    required_keys
                )
            )

    elif isinstance(obj, list):

        for item in obj:

            found.extend(
                find_objects(
                    item,
                    required_keys
                )
            )

    return found


def first_object(obj, required_keys):

    found = find_objects(
        obj,
        required_keys
    )

    if found:
        return found[0]

    return {}


def get_status(result):

    code = result.get(
        "status_code"
    )

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

    if code == 403:
        return "403 - Forbidden"

    if code == 429:
        return "429 - Rate Limit"

    if code == 422:
        return "422 - Parameter Error"

    if code is None:
        return "Connection Error"

    return f"HTTP {code}"


def format_number(value):

    if value is None or value == "":
        return "-"

    try:
        return f"{float(value):,.0f}"

    except:
        return str(value)


def format_billion(value):

    if value is None or value == "":
        return "-"

    try:
        return (
            f"Rp "
            f"{float(value) / 1_000_000_000:,.2f} B"
        )

    except:
        return str(value)


def format_percent(value):

    if value is None or value == "":
        return "-"

    try:
        return f"{float(value):,.1f}%"

    except:
        return str(value)

Explanation

Cell 3 handles the structure of the JSON returned by the API.

Financial APIs do not always return data in one simple structure. Important information can be located inside nested dictionaries, lists, or JSON strings.

The decode_json() function recursively decodes those structures.

The find_objects() function then searches through the entire response for dictionaries containing the keys required by the analysis.

The cell also provides helper functions for:

  • Detecting API status

  • Formatting numbers

  • Formatting billion values

  • Formatting percentages

This approach allows the following cells to work with structured information rather than manually navigating complicated API responses.

Cell 4 — Extract and Process the Data


"""CELL 4 — OLAH DATA"""

# ============================================================
# CELL 4 — EXTRACT DATA
# ============================================================

# ============================================================
# RAW DATA
# ============================================================

insider_raw = decode_json(
    insider_result.get("data")
)

sentiment_raw = decode_json(
    sentiment_result.get("data")
)

ipo_raw = decode_json(
    ipo_result.get("data")
)


# ============================================================
# INSIDER SCREENING
# ============================================================

insider_objects = find_objects(
    insider_raw,
    [
        "symbol",
        "totalBuyShares",
        "totalSellShares",
        "netShares"
    ]
)

insider_rows = []

for item in insider_objects:

    insider_rows.append({

        "Symbol": item.get(
            "symbol",
            "-"
        ),

        "Buy Shares": item.get(
            "totalBuyShares",
            0
        ),

        "Sell Shares": item.get(
            "totalSellShares",
            0
        ),

        "Net Shares": item.get(
            "netShares",
            0
        ),

        "Buy Value": item.get(
            "totalBuyValue",
            0
        ),

        "Sell Value": item.get(
            "totalSellValue",
            0
        ),

        "Net Value": item.get(
            "netValue",
            0
        ),

        "Buy Count": item.get(
            "buyCount",
            0
        ),

        "Sell Count": item.get(
            "sellCount",
            0
        ),

        "Insiders": item.get(
            "uniqueInsiders",
            []
        ),

        "Last Activity": item.get(
            "lastActivity",
            "-"
        ),

        "Action": item.get(
            "dominantAction",
            "-"
        )
    })


insider_df = pd.DataFrame(
    insider_rows
)


# ============================================================
# RETAIL
# ============================================================

retail_objects = find_objects(
    sentiment_raw,
    [
        "score",
        "status",
        "indicators",
        "danger_level"
    ]
)

retail = {}

for item in retail_objects:

    indicators = item.get(
        "indicators",
        {}
    )

    if (
        isinstance(indicators, dict)
        and
        (
            "frequency_score"
            in indicators
            or
            "small_lot_percentage"
            in indicators
            or
            "fomo_score"
            in indicators
        )
    ):

        retail = item

        break


# ============================================================
# BANDAR
# ============================================================

bandar_objects = find_objects(
    sentiment_raw,
    [
        "score",
        "status",
        "indicators"
    ]
)

bandar = {}

for item in bandar_objects:

    indicators = item.get(
        "indicators",
        {}
    )

    if (
        isinstance(indicators, dict)
        and
        (
            "top_broker_net_flow"
            in indicators
            or
            "large_lot_percentage"
            in indicators
            or
            "accumulation_score"
            in indicators
            or
            "foreign_flow"
            in indicators
        )
    ):

        bandar = item

        break


retail_indicators = retail.get(
    "indicators",
    {}
)

bandar_indicators = bandar.get(
    "indicators",
    {}
)


# ============================================================
# TOP BROKERS
# ============================================================

top_brokers = bandar.get(
    "top_brokers",
    {}
)

buyers = top_brokers.get(
    "buyers",
    []
)

sellers = top_brokers.get(
    "sellers",
    []
)


# ============================================================
# IPO MOMENTUM
# ============================================================

ipo_objects = find_objects(
    ipo_raw,
    ["status"]
)

ipo_sentiment = "-"

for item in ipo_objects:

    status = item.get(
        "status"
    )

    if status in [
        "BULLISH",
        "BEARISH",
        "NEUTRAL"
    ]:

        ipo_sentiment = status

        break


# fallback jika status berada di key sentiment

if ipo_sentiment == "-":

    sentiment_objects = find_objects(
        ipo_raw,
        ["sentiment"]
    )

    for item in sentiment_objects:

        value = item.get(
            "sentiment"
        )

        if isinstance(value, str):

            if value.upper() in [
                "BULLISH",
                "BEARISH",
                "NEUTRAL"
            ]:

                ipo_sentiment = (
                    value.upper()
                )

                break


# ============================================================
# FINAL OBJECTS
# ============================================================

ipo_momentum = "-"

momentum_objects = find_objects(
    ipo_raw,
    ["momentum"]
)

if momentum_objects:

    value = momentum_objects[0].get(
        "momentum"
    )

    if isinstance(value, dict):

        ipo_momentum = (
            value.get("score")
            or value.get("value")
            or value.get("status")
            or "-"
        )

    else:

        ipo_momentum = value


ipo_score = "-"

score_objects = find_objects(
    ipo_raw,
    ["score"]
)

if score_objects:

    value = score_objects[0].get(
        "score"
    )

    if not isinstance(value, dict):

        ipo_score = value

Explanation

Cell 4 is where the raw API responses become usable market data.

The insider response is searched for objects containing symbol, totalBuyShares, totalSellShares, and netShares.

The results are then converted into a pandas DataFrame containing buying activity, selling activity, net shares, transaction values, transaction counts, insider information, latest activity, and dominant action.

The next section identifies the retail sentiment object by looking for indicators such as frequency score, small lot percentage, and FOMO score.

The bandar section searches for indicators including broker net flow, large lot percentage, accumulation score, and foreign flow.

The code then extracts the top broker buyers and sellers before processing IPO sentiment, momentum, and score.

This separation is important because Cell 4 acts as the bridge between raw API responses and the investor friendly dashboard displayed in Cell 5.

Cell 5 — Final Dashboard

"""CELL 5 — DASHBOARD HASIL"""

# ============================================================
# CELL 5 — FINAL DASHBOARD
# ============================================================

print("=" * 100)
print("IDX INSIDER, RETAIL BANDAR & IPO MOMENTUM DASHBOARD")
print("=" * 100)

print(
    f"🕒 Update : "
    f"{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}"
)

print(
    f"📅 Insider : {PERIOD}   |   "
    f"📊 Sentiment : {SENTIMENT_SYMBOL} / {SENTIMENT_DAYS} hari"
)


# ============================================================
# API STATUS
# ============================================================

print("\n" + "=" * 100)
print("🔌 API STATUS")
print("=" * 100)

print(
    f"Insider Screening       : "
    f"HTTP {insider_result['status_code']} | "
    f"{get_status(insider_result)}"
)

print(
    f"Retail Bandar Sentiment : "
    f"HTTP {sentiment_result['status_code']} | "
    f"{get_status(sentiment_result)}"
)

print(
    f"IPO Momentum            : "
    f"HTTP {ipo_result['status_code']} | "
    f"{get_status(ipo_result)}"
)


# ============================================================
# INSIDER
# ============================================================

print("\n" + "=" * 100)
print("👤 INSIDER SCREENING")
print("=" * 100)

print(f"Periode      : {PERIOD}")
print(f"Symbol       : {SYMBOLS}")
print(f"Jumlah Data  : {len(insider_df)}")

if not insider_df.empty:

    for _, row in insider_df.iterrows():

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

        print(
            f"🏷️  {row['Symbol']}   |   "
            f"Aksi : {row['Action']}"
        )

        print(
            f"📈 BUY  : "
            f"{format_number(row['Buy Shares'])} saham "
            f"| {format_billion(row['Buy Value'])}"
        )

        print(
            f"📉 SELL : "
            f"{format_number(row['Sell Shares'])} saham "
            f"| {format_billion(row['Sell Value'])}"
        )

        print(
            f"⚖️  NET  : "
            f"{format_number(row['Net Shares'])} saham "
            f"| {format_billion(row['Net Value'])}"
        )

        print(
            f"🔢 Transaksi : "
            f"Buy {row['Buy Count']}x | "
            f"Sell {row['Sell Count']}x"
        )

        print(
            f"📅 Aktivitas : "
            f"{row['Last Activity']}"
        )

        if row["Insiders"]:

            print("👥 Insider:")

            for person in row["Insiders"]:

                print(
                    f"   • {person}"
                )

else:

    print("Tidak ada data insider.")


# ============================================================
# RETAIL
# ============================================================

print("\n" + "=" * 100)
print("🛒 RETAIL SENTIMENT")
print("=" * 100)

print(
    f"Symbol        : "
    f"{SENTIMENT_SYMBOL}"
)

print(
    f"Score         : "
    f"{retail.get('score', '-')}"
)

print(
    f"Status        : "
    f"{retail.get('status', '-')}"
)

print(
    f"Danger Level  : "
    f"{retail.get('danger_level', '-')}"
)


print("\n📊 Retail Indicators")
print("-" * 100)

print(
    f"Frequency Score      : "
    f"{retail_indicators.get('frequency_score', '-')}"
)

print(
    f"Small Lot Percentage  : "
    f"{format_percent(retail_indicators.get('small_lot_percentage'))}"
)

print(
    f"FOMO Score           : "
    f"{retail_indicators.get('fomo_score', '-')}"
)

print(
    f"Volume Participation : "
    f"{format_percent(retail_indicators.get('volume_participation'))}"
)


# ============================================================
# BANDAR
# ============================================================

print("\n" + "=" * 100)
print("🐋 BANDAR / SMART MONEY SENTIMENT")
print("=" * 100)

print(
    f"Score         : "
    f"{bandar.get('score', '-')}"
)

print(
    f"Status        : "
    f"{bandar.get('status', '-')}"
)

print(
    f"Large Lot     : "
    f"{format_percent(bandar_indicators.get('large_lot_percentage'))}"
)

print(
    f"Accumulation  : "
    f"{bandar_indicators.get('accumulation_score', '-')}"
)

print(
    f"Foreign Flow  : "
    f"{format_billion(bandar_indicators.get('foreign_flow'))}"
)

print(
    f"Institutional : "
    f"{format_billion(bandar_indicators.get('institutional_flow'))}"
)


# ============================================================
# BROKER BUYERS
# ============================================================

print("\n📈 TOP BROKER BUYERS")
print("-" * 100)

if buyers:

    for i, broker in enumerate(
        buyers[:5],
        1
    ):

        print(
            f"{i:02d}. "
            f"{broker.get('code', '-'):4} | "
            f"{broker.get('type', '-'):12} | "
            f"Net Buy : "
            f"{broker.get('net_value_formatted', '-')}"
        )

else:

    print(
        "Tidak ada data broker buyers."
    )


# ============================================================
# BROKER SELLERS
# ============================================================

print("\n📉 TOP BROKER SELLERS")
print("-" * 100)

if sellers:

    for i, broker in enumerate(
        sellers[:5],
        1
    ):

        print(
            f"{i:02d}. "
            f"{broker.get('code', '-'):4} | "
            f"{broker.get('type', '-'):12} | "
            f"Net Sell : "
            f"{broker.get('net_value_formatted', '-')}"
        )

else:

    print(
        "Tidak ada data broker sellers."
    )


# ============================================================
# IPO MOMENTUM
# ============================================================

print("\n" + "=" * 100)
print("🚀 IPO MOMENTUM")
print("=" * 100)

print(
    f"Momentum  : {ipo_momentum}"
)

print(
    f"Sentiment : {ipo_sentiment}"
)

if ipo_score != "-":

    print(
        f"Score     : {ipo_score}"
    )


# ============================================================
# QUICK INSIGHT
# ============================================================

print("\n" + "=" * 100)
print("🧾 QUICK INSIGHT")
print("=" * 100)

if not insider_df.empty:

    action = str(
        insider_df.iloc[0]["Action"]
    ).upper()

    if action == "DISTRIBUTION":

        print(
            "👤 Insider : DISTRIBUTION — "
            "aktivitas insider didominasi penjualan."
        )

    elif action == "ACCUMULATION":

        print(
            "👤 Insider : ACCUMULATION — "
            "aktivitas insider didominasi pembelian."
        )

    else:

        print(
            f"👤 Insider : {action}"
        )


print(
    f"🛒 Retail  : "
    f"{retail.get('status', '-')}"
    f" | Score {retail.get('score', '-')}"
)

print(
    f"🐋 Bandar  : "
    f"{bandar.get('status', '-')}"
    f" | Score {bandar.get('score', '-')}"
)

print(
    f"🚀 IPO     : "
    f"{ipo_sentiment}"
)

print("\n" + "=" * 100)
print("✅ ANALISIS SELESAI")
print("=" * 100)

Explanation

Cell 5 is the presentation layer of the project.

Instead of displaying raw JSON, the code organizes the processed information into clear sections.

The dashboard begins with the update time and analysis parameters before displaying the API status for all three endpoints.

The Insider Screening section then presents the selected period, symbols, number of returned records, buying activity, selling activity, net activity, transaction counts, insider information, and latest activity.

The Retail Sentiment section displays the BBCA score, status, danger level, frequency score, small lot percentage, FOMO score, and volume participation.

The Bandar or Smart Money section displays the overall score, status, large lot percentage, accumulation score, foreign flow, and institutional flow.

The dashboard also presents the top five broker buyers and sellers, followed by the IPO Momentum section containing momentum, sentiment, and score when available.

Finally, the Quick Insight section summarizes the dominant insider action and provides a compact overview of retail, bandar, and IPO sentiment.

This makes the final output much easier to read than working directly with the raw API response.

Reading the Insider Activity

The Insider Screening section is useful for understanding reported buying and selling activity associated with insiders.

Rather than displaying only one number, the dashboard separates:


  • Buy Shares

  • Sell Shares

  • Net Shares

  • Buy Value

  • Sell Value

  • Net Value

  • Buy Count

  • Sell Count

  • Insider information

  • Last Activity

  • Dominant Action

This provides more context when evaluating insider activity.

A positive net position may indicate stronger buying activity within the returned dataset, while a negative net position may indicate stronger selling activity.

However, insider transactions should not be interpreted as an automatic trading signal. The transaction context, company fundamentals, valuation, and broader market environment still matter.


Understanding Retail Sentiment

The Retail Sentiment section focuses on BBCA and provides several indicators related to retail participation.

The dashboard includes frequency score, small lot percentage, FOMO score, and volume participation.

These metrics can help identify how active retail participation is during the selected period.

A high retail participation level does not necessarily mean that the stock will move higher.

Instead, retail sentiment can be compared with price action and other market signals to create a broader picture.


Understanding Bandar and Smart Money Sentiment

The Bandar or Smart Money section provides another perspective on market participation.

The dashboard includes large lot percentage, accumulation score, foreign flow, and institutional flow.

These indicators can be useful when comparing larger transaction activity against retail participation.

The objective is not to assume that every large transaction represents one specific type of market participant.

Instead, these metrics provide additional information that can be evaluated alongside broker activity and price movement.


Broker Activity

Broker activity adds another layer to the analysis.

The dashboard displays the top five broker buyers and top five broker sellers.

Each entry includes the broker code, broker type, and formatted net transaction value when available.

This can help identify which broker channels are associated with stronger buying or selling activity in the returned dataset.

Broker flow should still be interpreted carefully because a broker code does not necessarily reveal the complete intention of the underlying client.


IPO Momentum

The IPO Momentum section provides a broader market perspective.

The dashboard attempts to extract three important elements:

  • Momentum

  • Sentiment

  • Score

The sentiment can be classified as:

  • Bullish

  • Bearish

  • Neutral

This information can then be compared with individual stock activity.

For example, strong insider accumulation combined with supportive broader market sentiment may create a different research context from insider distribution combined with weak market sentiment.

The dashboard does not attempt to turn these signals into an automatic trading recommendation.

Why Combine These Signals

The main advantage of this project is the combination of several market perspectives.

Insider activity provides one perspective.

Retail sentiment provides another.

Bandar and smart money indicators provide additional information about larger transaction behavior.

Broker activity helps identify the strongest broker-level buying and selling flows.

IPO momentum adds a broader market component.

When these signals are considered together, the dashboard becomes more useful as a research framework than relying on a single indicator.

Conclusion

The IDX Insider Retail Bandar IPO Momentum Dashboard demonstrates how raw IDX API data can be transformed into a structured market analysis workflow using Python.

The project follows a simple five cell architecture:

  1. Import and configuration

  2. API requests

  3. JSON parsing

  4. Data extraction

  5. Final dashboard

Each cell has a specific responsibility while the final output combines insider activity, retail sentiment, bandar indicators, broker flows, and IPO momentum.

The result is a compact dashboard that can be used as a starting point for deeper stock research.

It is important to remember that the dashboard is an analytical tool rather than a standalone trading system. Market participants should combine these signals with price action, financial statements, valuation, liquidity, macroeconomic conditions, and their own risk management before making an investment decision.