idx-cli/docs/STOCKBIT.md

16 KiB

Stockbit Session Features: Research and Design

What the user's Stockbit session (idx stockbit login, see the Stockbit sections of docs/ARCHITECTURE.md) can serve beyond the order book, live quotes and daily history, and how idx should add it. Endpoints were found in Stockbit's web bundle (stockbit.com/_next/static/chunks/*.js, the API client module) and probed with a real session on Tue 2026-11-03 during trading hours, one GET at a time at least 1 s apart. Nothing here ships yet unless marked.

Rules for every session feature

  1. Read-only by construction. The session client (api/stockbit/exodus.rs) issues GET only on an allowlist of path prefixes (READ_PATHS), plus POST /login/refresh. The bundle also holds trading (orders, day trade, PIN, cash withdrawal, portfolio), chat, posting and account endpoints; idx never calls them, and the allowlist makes a stray call fail before it leaves the process.
  2. One client. Every endpoint goes through Exodus::get: rotating refresh under the file lock, one retry after a 401 / JSON 403, the shared 1 s pacing, AUTHERROR / UPSTREAMBLOCKED / RATELIMITED mapping, and no token in logs, errors or cache keys.
  3. Stockbit-only commands use Stockbit whatever --provider says, like stocks intraday; without a session they answer AUTHERROR (exit code as today). All endpoints below answer 401 without a session.
  4. Pure parsers, typed output. Stockbit sends numbers as display strings ("193,206", "+0.0001", "1.03B"), dates as "25 Mar 26" or YYYY-MM-DD, and placeholders ("", 0, "0001-01-01 00:00:00"). Parsers turn them into integers, decimals, NaiveDate and None, and are tested against recorded fixtures (tests/fixtures/stockbit_exodus_*).
  5. Units checked live per endpoint before release (chart volume is lots, the order book is shares).
  6. Cache per endpoint: book and quote 5 s, market state 30 s, movements 1 h, corporate actions 6 h.
  7. Terms: Stockbit's data is for personal, non-commercial use; the user docs (web/src/content/docs/cli/stockbit.md) say so for every feature.

Endpoint inventory

All on https://exodus.stockbit.com, bearer access token, HTTP 200 on 2026-11-03 unless noted.

Endpoint Serves Plan
/company-price-feed/v2/orderbook/companies/{SYM} quote, OHLC, foreign flow, depth shipped (#74)
/company-price-feed/market-time/session market state shipped (#74)
/charts/{SYM}/daily?timeframe=1m|3m|1y daily candles shipped (#74)
/charts/{SYM}/daily?timeframe=1w hourly candles, 5 sessions P2
/corpaction/{type}[?symbol=SYM] corporate actions and calendar shipped: stocks actions, stocks calendar
/insider/company/majorholder insider / major-holder movements P1
/insider/shareholding/composition/companies/{SYM} holder composition by period P2
/marketdetectors/{SYM} broker summary, Stockbit's bandar detector P2
/analyst-ratings/{SYM}/consensus, /analyst-ratings/{SYM} estimates, ratings P3
/order-trade/running-trade, /order-trade/trade-book trade tape P3
/emitten-metadata/shareholders/{SYM}/chart composition chart not probed: needs a second token from POST /emitten-metadata/shareholders/token
/keystats/ratio/v1/{SYM}, /findata-view/company/financial ratios, statements skip for now (MSN covers them)

Market state (market-time/session)

data.detail has two boards, regular and fca (full call auction), each with session, state_name, state_start_time, state_end_time, time_left, is_last_session, is_end_of_day. Observed:

Time (WIB) regular fca
Sat 2026-10-18 20:15 STATE_NAME_MARKET_CLOSED, session 0, end of day same
Tue 2026-11-03 12:35 STATE_NAME_BREAK, session 1, 12:00:00-13:29:59 STATE_NAME_BREAK, session 3, 12:00:00-13:59:59
Tue 2026-11-03 13:30 STATE_NAME_SESSION_2, session 2, 13:30:00-15:49:59 still STATE_NAME_BREAK
Tue 2026-11-03 14:00 STATE_NAME_SESSION_2 STATE_NAME_SESSION_4

The order book and session quotes treat any numbered STATE_NAME_SESSION_<n> as trading (#74; the assumed STATE_NAME_MARKET_OPEN never appeared). idx reads regular only, so FCA-board stocks can show the regular board's state around the break.

In session the order book's fbuy/fsell/fnet were the previous session's (equal to IPOT's 2026-11-02 net for BBCA, BBRI, TLKM) with has_foreign_bs: false; idx keeps them only when the flag is true (#74). Other order-book fields not used yet: total_bid_offer (queued lots and orders on each side), market_data (volume and value by board: regular, negotiated, cash), iepiev (indicative equilibrium price and volume, for the auctions), corp_action.active.

Charts

  • timeframe=1d, PRICE_CHART_TYPE_CANDLE: empty prices during the break.
  • timeframe=1d, PRICE_CHART_TYPE_LINE: a rolling ~350 minutes of per-minute closes across two sessions, no OHLC or volume (xaxisopt: "intraday"). Adds nothing over the public stocks intraday.
  • timeframe=1w, candle: hourly bars for 5 sessions, interval_in_minutes: 60, volume in lots. The newest bar is stamped with its last update (11:59:00), not the hour. BBCA's three bars on 2026-11-03 summed to 101.2M shares against 105.1M in the order book (pre-open auction or the negotiated market, unconfirmed).

Hourly history needs a datetime bar (Ohlc.date is a NaiveDate) and an hourly Interval, across Yahoo, MSN, output, goldens and the MCP schema; that is its own change.

P1: corporate actions (shipped)

Shipped as idx stocks actions, idx stocks calendar (not a top-level calendar: it sits beside stocks screen) and the MCP tools get_corporate_actions, get_corporate_calendar; the notes below are the endpoint as recorded. Before, idx had none (only the high-dividend screen and Yahoo's dividend yield). They explain price gaps in history (splits, bonuses, rights) and give a calendar of cum dates.

Endpoint

GET /corpaction/{type}?symbol=SYM gives one stock's history (BBCA: 37 cash dividends, cum dates 2009-06-09 to 2026-08-28); GET /corpaction/{type} without symbol is the market-wide list, newest cum/event date first, upcoming events included. The bundle passes limit (default 30); rightissue honoured it loosely (28 rows for limit=5), warrant ignored it (1329 rows for limit=3), so idx filters by date itself.

type Response key Field prefix Fields that matter
dividend dividend dividend_ cumdate, exdate, recdate, paydate, value (per share), currency
stock_dividend stock_dividend — empty on 2026-11-03; record a row before parsing
bonus bonus sahambonus_, stocksplit_ ratio ("6 : 4"), stocksplit_{cumdate,exdate,recdate,paymentdate,old,new,factor}
rightissue rightissue rightissue_ cumdate, exdate, recdate, old, new, ratio, price, factor, trading_start, trading_end
warrant warrant wrant_ trading_from, trading_end, exc_from, exc_end, exc_price
stocksplit stocksplit stocksplit_ cumdate, exdate, recdate, old, new, ratio, factor
reversesplit stock_reverse — empty on 2026-11-03; record a row before parsing
rups rups rups_ date, time, venue, eligible_date
pubex pubex puexp_ date, time, venue
ipo ipo ipo_ listing_date, company_name, ipo_price.{minimum,maximum,final}, ipo_data_detail (placeholders common)
tenderoffer tender tender_ start, end, paydate, price, shares, percentage
economic — — macro calendar, not a corporate action; out of scope

Examples from 2026-11-03: PPGL bonus 6:4 ex 2026-11-03; PRDA split 1:5 cum 2026-12-17; MGLV rights 100:15 at 8,880 cum 2026-11-04; BMAS rights 655:104 at 350 cum 2026-11-23, rights trading 2026-11-27 to 12-03; AUTO tender offer at 3,600 until 2026-11-11.

Types

As shipped (corporate/types.rs), one flat record with a calendar date and optional per-kind fields; JSON leaves absent ones out:

pub struct CorporateAction {
    pub symbol: String,               // BBCA.JK
    pub kind: ActionKind,             // JSON: dividend, stock-dividend, bonus, rights, warrant,
                                      // split, reverse-split, meeting, expose, ipo, tender
    pub date: NaiveDate,              // cum date for entitlements (ex date without one),
                                      // else the event's first day
    pub cum_date: Option<NaiveDate>,
    pub ex_date: Option<NaiveDate>,
    pub record_date: Option<NaiveDate>,
    pub pay_date: Option<NaiveDate>,
    pub cash_per_share: Option<f64>,  // dividends; IDR to the sen, else up to 6 decimals
    pub currency: Option<String>,     // dividends not paid in IDR
    pub ratio: Option<Ratio>,         // old : new
    pub factor: Option<f64>,          // Stockbit's price adjustment
    pub price: Option<i64>,           // rights, warrant, tender or IPO price (IDR)
    pub period: Option<DateRange>,    // rights or warrant trading, tender, IPO offer
    pub exercise: Option<DateRange>,  // warrant exercise
    pub shares: Option<u64>,          // offered (IPO) or sought (tender)
    pub percentage_bps: Option<i64>,  // tender
    pub time: Option<String>,         // meeting or expose
    pub venue: Option<String>,
    pub name: Option<String>,         // IPO or tender issuer
}

One parser per type maps its prefix into this record; unknown types and empty lists are not errors. A row whose dates are all placeholders is dropped.

Surface

  • idx stocks actions SYM [--kind dividend,rights,...]: one stock's history, newest first; every kind but ipo by default.
  • idx stocks calendar [--kind ...] [--from DATE] [--to DATE]: market-wide, default today to +30 days, oldest first by date; rights trading, tender offers and IPO offers still open in the range are included too.
  • MCP: get_corporate_actions {symbol, kinds?}, get_corporate_calendar {kinds?, from?, to?}.
  • Later: stocks history notes ex-dates of splits, bonuses and rights in the window, so a gap is not read as a crash.

P1: shareholder movements

Endpoint

Rows shipped as idx ownership movements and get_shareholder_movements; the summary score is next.

GET /insider/company/majorholder?symbol=&insider=&date_start=&date_end=&action_type=&source_type=&page=; all parameters optional, all filtered by Stockbit (checked 2026-11-03). date_start / date_end are YYYY-MM-DD; insider is the holder id, which is the row's id (not unique per row); action_type takes ACTION_TYPE_BUY, _SELL, _TRANSFER, _CROSS or _CORPACTION (the app's filter list); source_type takes SOURCE_TYPE_IDX or _KSEI. page counts from 1. 50 rows a page, data.is_more for the next page; a busy day has more than 50 rows. Without symbol it is market-wide, newest first: on 2026-11-03 the newest rows were from 2026-11-02. BBCA's 50 rows span 2024-03-22 to 2026-03-25.

Each row (data.movement[]): name, symbol, date ("25 Mar 26"), previous / current / changes {value: shares as "193,206", percentage}, action_type (ACTION_TYPE_BUY, _SELL, _TRANSFER, _CROSS, _RIGHT_ISSUE, _PRIVATE_PLACEMENT, _TENDER_OFFER; the bundle also names _BONUS_STOCK, _DIVIDEND_STOCK, _DIVIDEND_CASH), nationality (NATIONALITY_TYPE_LOCAL|FOREIGN), badges (SHAREHOLDER_BADGE_DIREKTUR, ..._KOMISARIS), price.raw.value, value.raw.value (IDR), is_estimated_price, purpose (free text such as "Pembelian", "Investasi"), data_source.type (SOURCE_TYPE_IDX for insider disclosures, SOURCE_TYPE_KSEI for 5% holder changes), broker_detail (empty so far). KSEI rows carry an estimated price (is_estimated_price: true); IDX rows mostly the trade price, though crossings can be estimated too. CROSS rows name securities firms as the holder. A rights issue can change the stake with no change in shares (dilution).

Relation to ownership

The KSEI register idx imports is a monthly snapshot of holders above 1%. This feed is the individual changes, days after the trade, including insiders far below 1%. It joins the ownership entities by name (the same resolution ownership resolve maintains), but needs no SQLite import: it is live.

Signal quality

Discretionary insider buying, above all by several insiders together, is the form of this data with predictive power in the literature; selling much less. The raw feed is noisy:

  • Program-like rows: BBCA's 2026-03-25 rows are every director and commissioner "buying" at the same 6,982, which reads as a share plan, not open-market conviction. Flag many holders on one date at one price.
  • TRANSFER rows and estimated prices (is_estimated_price).
  • The reporting lag: part of the move can be priced before the row appears.

So the command shows the rows faithfully, and a separate summary scores them: net value by symbol, the number of distinct buyers (cluster), value against average daily value, holders crossing 5% / 10%, and the program flag. Calling it alpha waits for an event study (feed history back to 2024 per symbol, Yahoo prices, forward 5/20/60-session returns against IHSG).

Surface

  • idx ownership movements [SYM] [--from DATE] [--to DATE] [--holder ID] [--action buy|sell|transfer|cross|corporate-action] [--source idx|ksei] [--pages N]: rows, newest first (shipped). --holder takes the holder id rather than a name: Stockbit filters by id, and a name search would only see the pages already read.
  • MCP: get_shareholder_movements {symbol?, from?, to?, holder_id?, action?, source?, pages?} (shipped).
  • Next: idx ownership movements --summary [--from DATE], the per-symbol score.

P2 and P3

  • Composition by period (/insider/shareholding/composition/companies/{SYM}, optional period_start, period_end, view): per report_date (latest 2026-10-30 on 2026-11-03), total shares and each holder's shares, percentage and previous shares. Fresher than waiting on the IDX register; could feed ownership changes as a source.
  • Market detector (/marketdetectors/{SYM}): bandar_detector with accdist labels per top-N, broker counts, average price. A second source for stocks broker / bandar beside IPOT (--source ipot|stockbit), as flow has --source ipot|idx; IPOT answered 502 on 2026-10-31.
  • Hourly candles (timeframe=1w), see "Charts".
  • Analyst consensus: revenue and operating profit actuals and estimates by year; ratings per broker.
  • Trade tape: running trade and trade book; large and only meaningful in session; wait for --watch.

Order of work

  1. Close #74: the open-state name in trading hours, fixtures for the observed states (done 2026-11-03).
  2. Corporate actions (parsers, stocks actions, stocks calendar, MCP, goldens) (done).
  3. Shareholder movements, rows first (done), then --summary.
  4. Event study for the movement score; P2 by demand.

Open questions

  • The regular state in session 1 (assumed STATE_NAME_SESSION_1), the pre-open (08:45-09:00) and the closing auction (15:50-16:00).
  • When has_foreign_bs turns true, and whether the figures are then the day's.
  • stock_dividend and reversesplit row shapes (both empty on 2026-11-03).
  • Whether /corpaction/{type} takes a date range (the bundle sends only limit and symbol).
  • The reporting lag between a movement's date and its appearance in the feed (sample the market-wide first page daily).