mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
`idx ownership movements [SYMBOL]` and the MCP tool
`get_shareholder_movements` list reported changes in insiders' and major
holders' stakes, newest first, from Stockbit's
/insider/company/majorholder: IDX insider disclosures (any size, with the
trade price) and KSEI changes of holders at 5% and above (Stockbit's
estimated price). Stockbit filters by date range, holder id, action and
source; each page is 50 rows and one request, up to 10 pages, cached an
hour per page under every filter.
Rows keep exact signed share counts and the stake in basis points as
Stockbit rounds it; unknown action labels are kept as `other` rather than
dropped. JSON is {movements, more}; JSONL is one movement per line. The
command lives under `ownership` but needs no import, and `ownership`
commands now take --offline / --no-cache for it.
The per-symbol summary score is the next PR; docs/STOCKBIT.md records the
endpoint's parameters and action types as checked on 2026-11-03.
Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY
271 lines
15 KiB
Markdown
271 lines
15 KiB
Markdown
# 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 on an allowlist of path prefixes, 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
|
|
|
|
```rust
|
|
pub struct CorporateAction {
|
|
pub symbol: String, // BBCA.JK
|
|
pub kind: ActionKind, // Dividend, StockDividend, Bonus, Rights, Warrant,
|
|
// Split, ReverseSplit, Meeting, PublicExpose, Ipo, TenderOffer
|
|
pub cum_date: Option<NaiveDate>,
|
|
pub ex_date: Option<NaiveDate>,
|
|
pub record_date: Option<NaiveDate>,
|
|
pub pay_date: Option<NaiveDate>,
|
|
pub event_date: Option<NaiveDate>, // meeting, expose, listing, tender end
|
|
pub detail: ActionDetail, // kind-specific: amount + currency; old:new ratio
|
|
// and factor; price; trading/exercise windows; venue
|
|
}
|
|
```
|
|
|
|
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`.
|
|
- MCP: `get_corporate_actions {symbol, kinds?, since?}`,
|
|
`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).
|