feat: corporate actions from the Stockbit session (stocks actions, stocks calendar)

`idx stocks actions SYM [--kind ...]`: one stock's cash and stock
dividends, bonus shares, rights, warrants, splits and reverse splits,
meetings, public exposes and tender offers (`--kind ipo` adds its IPO),
newest first. `idx stocks calendar [--from] [--to] [--kind ...]`: the
market's actions dated in the range (default today and 30 days ahead),
oldest first. MCP: get_corporate_actions, get_corporate_calendar.

Stockbit's `/corpaction/{type}` serves one kind per request; each type has
its own field prefix and list key, so entitlements are read by field
suffix. An action's `date` is the cum date for entitlements, else the
event's first day. Dividends keep their decimals. One request per kind,
cached 6 hours. AUTHERROR without a session.

Fixtures are the 2026-11-03 recordings (market-wide ones trimmed to
October-December). Goldens added; help-stocks and completions-bash gain
the two subcommands, MCP tools.json the two tools (regenerated with
UPDATE_GOLDEN=1). The MCP docs counted 17 tools without stockbit_login;
now 20.

Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY
This commit is contained in:
Rasyidan Akbar F. 2026-11-03 14:35:20 +07:00
commit 43ff4aa433
55 changed files with 2026 additions and 34 deletions

View file

@ -97,7 +97,8 @@ idx ownership changes --from 2026-07-31 --to 2026-08-31
minute) and `--provider stockbit` quotes with the best bid/offer while the minute) and `--provider stockbit` quotes with the best bid/offer while the
market is open, delayed at least 10 minutes. With your own account market is open, delayed at least 10 minutes. With your own account
(`idx stockbit login`): `stocks orderbook` depth, live (`idx stockbit login`): `stocks orderbook` depth, live
quotes and Stockbit daily history. Personal use under Stockbit's terms; see quotes, Stockbit daily history, and corporate actions (`stocks actions`,
`stocks calendar`: dividends, rights, splits, meetings, IPOs and more). Personal use under Stockbit's terms; see
the [Stockbit page](https://idx.rasyidanaf.com/cli/stockbit/) the [Stockbit page](https://idx.rasyidanaf.com/cli/stockbit/)
- Local file cache with TTL, and `--offline` mode with stale-cache fallback - Local file cache with TTL, and `--offline` mode with stale-cache fallback
@ -165,8 +166,9 @@ change it to `auto` with `idx config set general.output auto`.
## MCP server ## MCP server
`idx-mcp` serves the same data to MCP clients over stdio, as 17 tools: `idx-mcp` serves the same data to MCP clients over stdio, as 20 tools:
`get_quotes`, `get_intraday`, `get_orderbook`, `stockbit_status`, `get_price_history`, `screen_stocks`, `get_company`, `get_quotes`, `get_intraday`, `get_orderbook`, `get_corporate_actions`,
`get_corporate_calendar`, `stockbit_status`, `stockbit_login`, `get_price_history`, `screen_stocks`, `get_company`,
`analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`, `analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`,
`get_foreign_flow`, `get_shareholders`, `get_investor_holdings`, `get_foreign_flow`, `get_shareholders`, `get_investor_holdings`,
`search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as `search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as

View file

@ -14,9 +14,10 @@ history lives in git.
- Multi-period financial statements (today `financials` is single-period) - Multi-period financial statements (today `financials` is single-period)
- Order book depth without a login: no free source exists (IDX sells delayed - Order book depth without a login: no free source exists (IDX sells delayed
depth to vendors); `stocks orderbook` needs the user's Stockbit login. depth to vendors); `stocks orderbook` needs the user's Stockbit login.
- Corporate actions (`stocks actions`, `calendar`) and shareholder movements - Shareholder movements (`ownership movements`) from the Stockbit session;
(`ownership movements`) from the Stockbit session; design and endpoints in design and endpoints in `docs/STOCKBIT.md`. Corporate-action follow-ups:
`docs/STOCKBIT.md`. ex-dates of splits, bonuses and rights noted in `stocks history`, and the
`stock_dividend` / `reversesplit` row shapes once Stockbit lists one.
- Stockbit login follow-ups: hourly OHLCV from the session's `1w` chart (the - Stockbit login follow-ups: hourly OHLCV from the session's `1w` chart (the
`1d` chart is per-minute closes only); confirming session 1 is `1d` chart is per-minute closes only); confirming session 1 is
`STATE_NAME_SESSION_1` (session 2, `BREAK` and `MARKET_CLOSED` were `STATE_NAME_SESSION_1` (session 2, `BREAK` and `MARKET_CLOSED` were

View file

@ -4,15 +4,17 @@ use clap::{Args, Subcommand};
use crate::config::{HistoryProviderKind, IdxConfig}; use crate::config::{HistoryProviderKind, IdxConfig};
use crate::output::{ use crate::output::{
render_bandar, render_broker_summary, render_compare, render_earnings, render_financials, render_bandar, render_broker_summary, render_compare, render_corporate_actions,
render_foreign_flow, render_fundamental, render_growth, render_history, render_insights, render_corporate_calendar, render_earnings, render_financials, render_foreign_flow,
render_intraday, render_news, render_orderbook, render_profile, render_quotes, render_risk, render_fundamental, render_growth, render_history, render_insights, render_intraday,
render_screener, render_sentiment, render_technical, render_valuation, render_news, render_orderbook, render_profile, render_quotes, render_risk, render_screener,
render_sentiment, render_technical, render_valuation,
}; };
use idx_core::analysis::BandarQuery; use idx_core::analysis::BandarQuery;
use idx_core::analysis::bandar::DEFAULT_TOP; use idx_core::analysis::bandar::DEFAULT_TOP;
use idx_core::api::types::{Interval, Period}; use idx_core::api::types::{Interval, Period};
use idx_core::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange}; use idx_core::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange};
use idx_core::corporate::types::{ActionKind, CalendarRange};
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::financial::{EarningsFilter, StatementFilter, StatementKind}; use idx_core::financial::{EarningsFilter, StatementFilter, StatementKind};
use idx_core::flow::types::{FlowRange, FlowSource}; use idx_core::flow::types::{FlowRange, FlowSource};
@ -163,6 +165,42 @@ the output says so.",
#[arg(long, default_value_t = 10)] #[arg(long, default_value_t = 10)]
depth: usize, depth: usize,
}, },
#[command(
about = "Dividends, rights, splits and other corporate actions of a stock (Stockbit login)",
long_about = "One IDX stock's corporate actions from your own Stockbit session (`idx \
stockbit login`), newest first: cash and stock dividends, bonus shares, rights issues, \
warrants, splits and reverse splits, shareholders' meetings, public exposes and tender offers \
(`--kind ipo` adds its IPO). Stockbit serves one kind per request, 1 s apart, so the first \
call takes about a second per kind; answers are cached for 6 hours.",
after_help = "Examples:\n idx stocks actions BBCA\n idx stocks actions TLKM --kind dividend\n idx -o json stocks actions BBRI --kind dividend,split | jq '.[0]'"
)]
Actions {
/// Single ticker symbol (e.g. BBCA).
symbol: String,
/// Kinds to list (default: all but ipo).
#[arg(long = "kind", value_enum, value_delimiter = ',', num_args = 1..)]
kinds: Vec<ActionKind>,
},
#[command(
about = "Upcoming and recent corporate actions across the market (Stockbit login)",
long_about = "Corporate actions across IDX from your own Stockbit session (`idx stockbit \
login`), oldest first, dated within --from and --to (default: today and the next 30 days). \
An action's date is its cum date for dividends, bonuses, rights and splits, else the event's \
first day. Stockbit lists about a year back and what is announced ahead. One request per \
kind, 1 s apart; answers are cached for 6 hours.",
after_help = "Examples:\n idx stocks calendar\n idx stocks calendar --kind dividend,rights\n idx stocks calendar --from 2026-12-01 --to 2026-12-31 --kind meeting"
)]
Calendar {
/// First day, YYYY-MM-DD. Default: today.
#[arg(long)]
from: Option<String>,
/// Last day, YYYY-MM-DD. Default: 30 days after --from (at most 366).
#[arg(long)]
to: Option<String>,
/// Kinds to list (default: all).
#[arg(long = "kind", value_enum, value_delimiter = ',', num_args = 1..)]
kinds: Vec<ActionKind>,
},
#[command( #[command(
about = "Get historical OHLC data", about = "Get historical OHLC data",
after_help = "Examples:\n idx stocks history BBCA --period 3mo\n idx stocks history BBCA --period 1y --interval 1wk" after_help = "Examples:\n idx stocks history BBCA --period 3mo\n idx stocks history BBCA --period 1y --interval 1wk"
@ -360,6 +398,27 @@ pub fn handle(
book.offers.truncate(*depth); book.offers.truncate(*depth);
render_orderbook(&book, format, no_color) render_orderbook(&book, format, no_color)
} }
StocksSubcommand::Actions { symbol, kinds } => {
let kinds = if kinds.is_empty() {
ActionKind::STOCK_DEFAULT.to_vec()
} else {
kinds.clone()
};
let actions = idx.corporate().actions(symbol, &kinds, policy)?;
let resolved = idx_core::api::resolve_symbol(symbol, &config.core.exchange)?;
render_corporate_actions(&resolved, &actions.data, format)
}
StocksSubcommand::Calendar { from, to, kinds } => {
let range =
CalendarRange::from_args(from.as_deref(), to.as_deref(), idx.corporate().today())?;
let kinds = if kinds.is_empty() {
ActionKind::ALL.to_vec()
} else {
kinds.clone()
};
let actions = idx.corporate().calendar(range, &kinds, policy)?;
render_corporate_calendar(&range, &actions.data, format)
}
StocksSubcommand::History { StocksSubcommand::History {
symbol, symbol,
period, period,

View file

@ -19,6 +19,7 @@ use idx_core::api::types::{
}; };
use idx_core::broker::types::BrokerSummary; use idx_core::broker::types::BrokerSummary;
use idx_core::config::ProviderKind; use idx_core::config::ProviderKind;
use idx_core::corporate::types::{CalendarRange, CorporateAction};
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::flow::types::ForeignFlow; use idx_core::flow::types::ForeignFlow;
@ -103,6 +104,28 @@ pub fn render_orderbook(
} }
} }
pub fn render_corporate_actions(
symbol: &str,
actions: &[CorporateAction],
format: &OutputFormat,
) -> Result<(), IdxError> {
match format {
OutputFormat::Table => table::print_corporate_actions(symbol, actions),
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(actions),
}
}
pub fn render_corporate_calendar(
range: &CalendarRange,
actions: &[CorporateAction],
format: &OutputFormat,
) -> Result<(), IdxError> {
match format {
OutputFormat::Table => table::print_corporate_calendar(range, actions),
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(actions),
}
}
pub fn render_broker_summary( pub fn render_broker_summary(
summary: &BrokerSummary, summary: &BrokerSummary,
format: &OutputFormat, format: &OutputFormat,

View file

@ -15,6 +15,7 @@ use idx_core::api::types::{
Intraday, NewsItem, Ohlc, OrderBook, Quote, SentimentData, Intraday, NewsItem, Ohlc, OrderBook, Quote, SentimentData,
}; };
use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary}; use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary};
use idx_core::corporate::types::{ActionKind, CalendarRange, CorporateAction, DateRange};
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::flow::types::{FlowSource, ForeignFlow}; use idx_core::flow::types::{FlowSource, ForeignFlow};
@ -1670,6 +1671,190 @@ outside the regular board (negotiated and cash)."
); );
} }
fn action_kind_label(kind: ActionKind) -> &'static str {
match kind {
ActionKind::Dividend => "dividend",
ActionKind::StockDividend => "stock dividend",
ActionKind::Bonus => "bonus",
ActionKind::Rights => "rights",
ActionKind::Warrant => "warrant",
ActionKind::Split => "split",
ActionKind::ReverseSplit => "reverse split",
ActionKind::Meeting => "meeting",
ActionKind::PublicExpose => "public expose",
ActionKind::Ipo => "IPO",
ActionKind::TenderOffer => "tender offer",
}
}
/// IDR with thousands separators and up to two decimals: `1,191.09`, `3.8`.
fn format_rupiah(value: f64) -> String {
let cents = (value * 100.0).round() as i64;
let (whole, frac) = (cents / 100, (cents % 100).abs());
match frac {
0 => format_idr(whole),
f if f % 10 == 0 => format!("{}.{}", format_idr(whole), f / 10),
f => format!("{}.{f:02}", format_idr(whole)),
}
}
/// What an action gives or asks, in one line.
fn action_detail(action: &CorporateAction) -> String {
let span = |range: Option<DateRange>| range.map(|r| format!("{} to {}", r.start, r.end));
let price = action.price.map(|p| format!("Rp {}", format_idr(p)));
let mut parts: Vec<String> = Vec::new();
match action.kind {
ActionKind::Dividend => {
parts.extend(
action
.cash_per_share
.map(|c| format!("Rp {} per share", format_rupiah(c))),
);
}
ActionKind::StockDividend | ActionKind::Bonus => {
parts.extend(
action
.ratio
.map(|r| format!("{} new per {} held", format_u64(r.new), format_u64(r.old))),
);
}
ActionKind::Split | ActionKind::ReverseSplit => {
parts.extend(
action
.ratio
.map(|r| format!("{} becomes {}", format_u64(r.old), format_u64(r.new))),
);
}
ActionKind::Rights => {
parts.extend(action.ratio.map(|r| {
format!(
"{} rights per {} held",
format_u64(r.new),
format_u64(r.old)
)
}));
parts.extend(price.map(|p| format!("at {p}")));
parts.extend(span(action.period).map(|s| format!("trading {s}")));
}
ActionKind::Warrant => {
parts.extend(price.map(|p| format!("exercise at {p}")));
parts.extend(span(action.exercise).map(|s| format!("exercisable {s}")));
parts.extend(action.period.map(|r| format!("trades until {}", r.end)));
}
ActionKind::Meeting | ActionKind::PublicExpose => {
parts.extend(
action
.time
.as_deref()
.map(|t| format!("{} WIB", terminal_safe(t))),
);
parts.extend(
action
.venue
.as_deref()
.map(|v| truncate_chars(&terminal_safe(v), 60)),
);
}
ActionKind::Ipo => {
parts.extend(price);
parts.extend(action.shares.map(|s| format!("{} shares", format_u64(s))));
parts.extend(span(action.period).map(|s| format!("offered {s}")));
}
ActionKind::TenderOffer => {
parts.extend(price);
parts.extend(action.shares.map(|s| {
let share = action
.percentage_bps
.map(|bps| format!(" ({}.{:02}%)", bps / 100, bps % 100))
.unwrap_or_default();
format!("for {} shares{share}", format_u64(s))
}));
parts.extend(span(action.period).map(|s| format!("open {s}")));
}
}
if parts.is_empty() {
"-".to_string()
} else {
parts.join(", ")
}
}
const CORPORATE_DATE_NOTE: &str = "DATE is the cum date for dividends, bonuses, rights and \
splits (buy by that day to be entitled); for other actions, the event's first day.";
fn opt_date(date: Option<chrono::NaiveDate>) -> String {
date.map(|d| d.to_string())
.unwrap_or_else(|| "-".to_string())
}
pub fn print_corporate_actions(symbol: &str, actions: &[CorporateAction]) -> Result<(), IdxError> {
println!(
"{}",
format!("Corporate actions for {}", terminal_safe(symbol)).bold()
);
if actions.is_empty() {
println!("None of these kinds on record.");
} else {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec!["DATE", "KIND", "EX", "RECORD", "PAY", "DETAIL"]);
for action in actions {
table.add_row(vec![
cell(action.date),
cell(action_kind_label(action.kind)),
cell(opt_date(action.ex_date)),
cell(opt_date(action.record_date)),
cell(opt_date(action.pay_date)),
cell(action_detail(action)),
]);
}
println!("{table}");
println!("{CORPORATE_DATE_NOTE}");
}
println!("{STOCKBIT_SESSION_NOTE}");
Ok(())
}
pub fn print_corporate_calendar(
range: &CalendarRange,
actions: &[CorporateAction],
) -> Result<(), IdxError> {
println!(
"{}",
format!(
"Corporate actions from {} to {} ({})",
range.from,
range.to,
actions.len()
)
.bold()
);
if actions.is_empty() {
println!("None of these kinds in that range.");
} else {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec!["DATE", "SYMBOL", "KIND", "EX", "DETAIL"]);
for action in actions {
table.add_row(vec![
cell(action.date),
cell(action.symbol.trim_end_matches(".JK")),
cell(action_kind_label(action.kind)),
cell(opt_date(action.ex_date)),
cell(action_detail(action)),
]);
}
println!("{table}");
println!("{CORPORATE_DATE_NOTE}");
}
println!("{STOCKBIT_SESSION_NOTE}");
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{

View file

@ -285,6 +285,30 @@ env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0OD
name = "stockbit-status-logged-out" name = "stockbit-status-logged-out"
args = ["stockbit", "status"] args = ["stockbit", "status"]
# Corporate actions: BBCA's recorded dividends, splits, meetings and exposes
# (no fixture for a kind means none, as Stockbit answers); the calendar reads
# the market-wide recordings of 2026-11-03.
[[case]]
name = "stockbit-actions"
args = ["stocks", "actions", "BBCA", "--kind", "split,meeting,expose"]
env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0ODAwLCAibm90ZSI6ICJ0ZXN0IHRva2VuLCBzaWduZWQgYnkgbm9ib2R5In0.c2ln" }
[[case]]
name = "stockbit-actions-dividends"
args = ["stocks", "actions", "BBCA", "--kind", "dividend"]
env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0ODAwLCAibm90ZSI6ICJ0ZXN0IHRva2VuLCBzaWduZWQgYnkgbm9ib2R5In0.c2ln" }
[[case]]
name = "stockbit-calendar"
args = ["stocks", "calendar", "--to", "2026-11-12", "--kind", "dividend,bonus,rights,warrant,split,ipo,tender"]
env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0ODAwLCAibm90ZSI6ICJ0ZXN0IHRva2VuLCBzaWduZWQgYnkgbm9ib2R5In0.c2ln", IDX_MOCK_TODAY = "2026-11-03" }
[[case]]
name = "stockbit-calendar-meetings"
args = ["stocks", "calendar", "--from", "2026-11-05", "--to", "2026-11-06", "--kind", "meeting,expose"]
env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0ODAwLCAibm90ZSI6ICJ0ZXN0IHRva2VuLCBzaWduZWQgYnkgbm9ib2R5In0.c2ln", IDX_MOCK_TODAY = "2026-11-03" }
# --- stocks: broker summaries (IPOT) and foreign flow (IDX) -------------------- # --- stocks: broker summaries (IPOT) and foreign flow (IDX) --------------------
[[case]] [[case]]
@ -392,6 +416,15 @@ args = ["--provider", "stockbit", "stocks", "fundamental", "BBCA"]
name = "error-orderbook-no-session" name = "error-orderbook-no-session"
args = ["stocks", "orderbook", "BBCA"] args = ["stocks", "orderbook", "BBCA"]
[[case]]
name = "error-actions-no-session"
args = ["stocks", "actions", "BBCA"]
[[case]]
name = "error-calendar-range"
args = ["stocks", "calendar", "--from", "2026-11-10", "--to", "2026-11-01"]
env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0ODAwLCAibm90ZSI6ICJ0ZXN0IHRva2VuLCBzaWduZWQgYnkgbm9ib2R5In0.c2ln", IDX_MOCK_TODAY = "2026-11-03" }
[[case]] [[case]]
name = "error-stockbit-history-window" name = "error-stockbit-history-window"
args = ["stocks", "history", "BBCA", "--period", "5y", "--history-provider", "stockbit"] args = ["stocks", "history", "BBCA", "--period", "5y", "--history-provider", "stockbit"]

View file

@ -189,12 +189,18 @@ _idx() {
idx__help__stockbit,status) idx__help__stockbit,status)
cmd="idx__help__stockbit__status" cmd="idx__help__stockbit__status"
;; ;;
idx__help__stocks,actions)
cmd="idx__help__stocks__actions"
;;
idx__help__stocks,bandar) idx__help__stocks,bandar)
cmd="idx__help__stocks__bandar" cmd="idx__help__stocks__bandar"
;; ;;
idx__help__stocks,broker) idx__help__stocks,broker)
cmd="idx__help__stocks__broker" cmd="idx__help__stocks__broker"
;; ;;
idx__help__stocks,calendar)
cmd="idx__help__stocks__calendar"
;;
idx__help__stocks,compare) idx__help__stocks,compare)
cmd="idx__help__stocks__compare" cmd="idx__help__stocks__compare"
;; ;;
@ -390,12 +396,18 @@ _idx() {
idx__stockbit__help,status) idx__stockbit__help,status)
cmd="idx__stockbit__help__status" cmd="idx__stockbit__help__status"
;; ;;
idx__stocks,actions)
cmd="idx__stocks__actions"
;;
idx__stocks,bandar) idx__stocks,bandar)
cmd="idx__stocks__bandar" cmd="idx__stocks__bandar"
;; ;;
idx__stocks,broker) idx__stocks,broker)
cmd="idx__stocks__broker" cmd="idx__stocks__broker"
;; ;;
idx__stocks,calendar)
cmd="idx__stocks__calendar"
;;
idx__stocks,compare) idx__stocks,compare)
cmd="idx__stocks__compare" cmd="idx__stocks__compare"
;; ;;
@ -453,12 +465,18 @@ _idx() {
idx__stocks,valuation) idx__stocks,valuation)
cmd="idx__stocks__valuation" cmd="idx__stocks__valuation"
;; ;;
idx__stocks__help,actions)
cmd="idx__stocks__help__actions"
;;
idx__stocks__help,bandar) idx__stocks__help,bandar)
cmd="idx__stocks__help__bandar" cmd="idx__stocks__help__bandar"
;; ;;
idx__stocks__help,broker) idx__stocks__help,broker)
cmd="idx__stocks__help__broker" cmd="idx__stocks__help__broker"
;; ;;
idx__stocks__help,calendar)
cmd="idx__stocks__help__calendar"
;;
idx__stocks__help,compare) idx__stocks__help,compare)
cmd="idx__stocks__help__compare" cmd="idx__stocks__help__compare"
;; ;;
@ -1371,7 +1389,7 @@ _idx() {
return 0 return 0
;; ;;
idx__help__stocks) idx__help__stocks)
opts="quote intraday orderbook history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare" opts="quote intraday orderbook actions calendar history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1384,6 +1402,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__help__stocks__actions)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__help__stocks__bandar) idx__help__stocks__bandar)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -1412,6 +1444,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__help__stocks__calendar)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__help__stocks__compare) idx__help__stocks__compare)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -2689,7 +2735,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks) idx__stocks)
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help quote intraday orderbook history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help quote intraday orderbook actions calendar history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2714,6 +2760,36 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__actions)
opts="-o -q -v -h --kind --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--kind)
COMPREPLY=($(compgen -W "dividend stock-dividend bonus rights warrant split reverse-split meeting expose ipo tender" -- "${cur}"))
return 0
;;
--output)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__bandar) idx__stocks__bandar)
opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..." opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@ -2810,6 +2886,44 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__calendar)
opts="-o -q -v -h --from --to --kind --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--from)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--to)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--kind)
COMPREPLY=($(compgen -W "dividend stock-dividend bonus rights warrant split reverse-split meeting expose ipo tender" -- "${cur}"))
return 0
;;
--output)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__compare) idx__stocks__compare)
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..." opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@ -2987,7 +3101,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__help) idx__stocks__help)
opts="quote intraday orderbook history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help" opts="quote intraday orderbook actions calendar history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -3000,6 +3114,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__help__actions)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__help__bandar) idx__stocks__help__bandar)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -3028,6 +3156,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__help__calendar)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__help__compare) idx__stocks__help__compare)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then

View file

@ -0,0 +1,4 @@
exit: 4
--- stdout
--- stderr
{"code":"AUTHERROR","error":true,"message":"auth error: no Stockbit session; run `idx stockbit login` (or set IDX_STOCKBIT_REFRESH_TOKEN)"}

View file

@ -0,0 +1,4 @@
exit: 4
--- stdout
--- stderr
Error: auth error: no Stockbit session; run `idx stockbit login` (or set IDX_STOCKBIT_REFRESH_TOKEN)

View file

@ -0,0 +1,4 @@
exit: 2
--- stdout
--- stderr
{"code":"INVALIDINPUT","error":true,"message":"invalid input: to (2026-11-01) is before from (2026-11-10)"}

View file

@ -0,0 +1,4 @@
exit: 2
--- stdout
--- stderr
Error: invalid input: to (2026-11-01) is before from (2026-11-10)

View file

@ -8,6 +8,8 @@ Commands:
quote Get real-time stock quotes quote Get real-time stock quotes
intraday Today's session price by the minute (Stockbit) intraday Today's session price by the minute (Stockbit)
orderbook Order book depth and foreign flow (Stockbit login) orderbook Order book depth and foreign flow (Stockbit login)
actions Dividends, rights, splits and other corporate actions of a stock (Stockbit login)
calendar Upcoming and recent corporate actions across the market (Stockbit login)
history Get historical OHLC data history Get historical OHLC data
technical Run technical analysis on a stock technical Run technical analysis on a stock
growth Run growth analysis on a stock growth Run growth analysis on a stock

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,83 @@
exit: 0
--- stdout
Corporate actions for BBCA.JK
┌────────────┬──────────┬────────────┬────────────┬────────────┬────────────────────┐
│ DATE ┆ KIND ┆ EX ┆ RECORD ┆ PAY ┆ DETAIL │
╞════════════╪══════════╪════════════╪════════════╪════════════╪════════════════════╡
│ 2026-08-28 ┆ dividend ┆ 2026-08-31 ┆ 2026-09-01 ┆ 2026-09-16 ┆ Rp 25 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-06-15 ┆ dividend ┆ 2026-06-17 ┆ 2026-06-18 ┆ 2026-06-26 ┆ Rp 20 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-03-27 ┆ dividend ┆ 2026-03-30 ┆ 2026-03-31 ┆ 2026-04-08 ┆ Rp 281 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2025-12-02 ┆ dividend ┆ 2025-12-03 ┆ 2025-12-04 ┆ 2025-12-22 ┆ Rp 55 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2025-03-20 ┆ dividend ┆ 2025-03-21 ┆ 2025-03-24 ┆ 2025-04-11 ┆ Rp 250 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2024-11-20 ┆ dividend ┆ 2024-11-21 ┆ 2024-11-22 ┆ 2024-12-11 ┆ Rp 50 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2024-03-22 ┆ dividend ┆ 2024-03-25 ┆ 2024-03-26 ┆ 2024-04-04 ┆ Rp 227.5 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2023-12-01 ┆ dividend ┆ 2023-12-04 ┆ 2023-12-05 ┆ 2023-12-20 ┆ Rp 42.5 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2023-03-28 ┆ dividend ┆ 2023-03-29 ┆ 2023-03-30 ┆ 2023-04-14 ┆ Rp 170 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2022-12-01 ┆ dividend ┆ 2022-12-02 ┆ 2022-12-05 ┆ 2022-12-20 ┆ Rp 35 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2022-03-25 ┆ dividend ┆ 2022-03-28 ┆ 2022-03-29 ┆ 2022-04-19 ┆ Rp 120 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2021-11-16 ┆ dividend ┆ 2021-11-17 ┆ 2021-11-18 ┆ 2021-12-07 ┆ Rp 25 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2021-04-07 ┆ dividend ┆ 2021-04-08 ┆ 2021-04-09 ┆ 2021-04-28 ┆ Rp 432 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2020-12-07 ┆ dividend ┆ 2020-12-08 ┆ 2020-12-10 ┆ 2020-12-22 ┆ Rp 98 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2020-04-20 ┆ dividend ┆ 2020-04-21 ┆ 2020-04-22 ┆ 2020-05-11 ┆ Rp 455 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2019-12-05 ┆ dividend ┆ 2019-12-06 ┆ 2019-12-09 ┆ 2019-12-20 ┆ Rp 100 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2019-04-23 ┆ dividend ┆ 2019-04-24 ┆ 2019-04-25 ┆ 2019-04-30 ┆ Rp 255 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2018-11-30 ┆ dividend ┆ 2018-12-03 ┆ 2018-12-04 ┆ 2018-12-21 ┆ Rp 85 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2018-04-12 ┆ dividend ┆ 2018-04-13 ┆ 2018-04-17 ┆ 2018-04-30 ┆ Rp 175 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2017-11-28 ┆ dividend ┆ 2017-11-29 ┆ 2017-12-04 ┆ 2017-12-20 ┆ Rp 80 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2017-04-13 ┆ dividend ┆ 2017-04-17 ┆ 2017-04-20 ┆ 2017-04-28 ┆ Rp 130 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2016-11-30 ┆ dividend ┆ 2016-12-01 ┆ 2016-12-05 ┆ 2016-12-22 ┆ Rp 70 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2016-04-14 ┆ dividend ┆ 2016-04-15 ┆ 2016-04-19 ┆ 2016-04-29 ┆ Rp 105 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2015-11-12 ┆ dividend ┆ 2015-11-13 ┆ 2015-11-17 ┆ 2015-12-09 ┆ Rp 55 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2015-04-16 ┆ dividend ┆ 2015-04-17 ┆ 2015-04-21 ┆ 2015-05-13 ┆ Rp 98 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2014-12-04 ┆ dividend ┆ 2014-12-05 ┆ 2014-12-09 ┆ 2014-12-23 ┆ Rp 50 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2014-04-29 ┆ dividend ┆ 2014-04-30 ┆ 2014-05-05 ┆ 2014-05-20 ┆ Rp 75 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2013-11-28 ┆ dividend ┆ 2013-11-29 ┆ 2013-12-03 ┆ 2013-12-17 ┆ Rp 45 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2013-05-28 ┆ dividend ┆ 2013-05-29 ┆ 2013-05-31 ┆ 2013-06-17 ┆ Rp 71 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2012-12-03 ┆ dividend ┆ 2012-12-04 ┆ 2012-12-06 ┆ 2012-12-20 ┆ Rp 43.5 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2012-06-08 ┆ dividend ┆ 2012-06-11 ┆ 2012-06-13 ┆ 2012-06-27 ┆ Rp 70 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2011-12-06 ┆ dividend ┆ 2011-12-07 ┆ 2011-12-09 ┆ 2011-12-23 ┆ Rp 43.5 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2011-06-06 ┆ dividend ┆ 2011-06-07 ┆ 2011-06-09 ┆ 2011-06-23 ┆ Rp 70 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2010-11-19 ┆ dividend ┆ 2010-11-22 ┆ 2010-11-24 ┆ 2010-12-09 ┆ Rp 42.5 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2010-05-31 ┆ dividend ┆ 2010-06-01 ┆ 2010-06-03 ┆ 2010-06-17 ┆ Rp 70 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2009-11-12 ┆ dividend ┆ 2009-11-13 ┆ 2009-11-17 ┆ 2009-12-02 ┆ Rp 40 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2009-06-09 ┆ dividend ┆ 2009-06-10 ┆ 2009-06-12 ┆ 2009-06-26 ┆ Rp 65 per share │
└────────────┴──────────┴────────────┴────────────┴────────────┴────────────────────┘
DATE is the cum date for dividends, bonuses, rights and splits (buy by that day to be entitled); for other actions, the event's first day.
Source: your Stockbit session; for personal, non-commercial use (Stockbit's terms).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
[{"symbol":"BBCA.JK","kind":"public_expose","date":"2026-09-09","time":"16:00","venue":"Public Expose Live 2026 yang tercantum pada website idx.co.id"},{"symbol":"BBCA.JK","kind":"meeting","date":"2026-03-12","record_date":"2026-02-09","time":"14:00","venue":"Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta 10310"},{"symbol":"BBCA.JK","kind":"public_expose","date":"2025-09-11","time":"16:00","venue":"Public Expose Live 2025 yang tercantum pada website idx.co.id"},{"symbol":"BBCA.JK","kind":"meeting","date":"2025-03-12","record_date":"2025-02-07","time":"09:30","venue":"Menara BCA, Grand Indonesia, Jl. M.H. Thamrin No. 1, Jakarta, 10310"},{"symbol":"BBCA.JK","kind":"public_expose","date":"2024-08-28","time":"15:00","venue":"Public Expose Live 2024 Informasi Jadwal Pubex Live 2024 dan pendaftaran peserta di website www.idx.co.id"},{"symbol":"BBCA.JK","kind":"meeting","date":"2024-03-14","record_date":"2024-02-07","time":"09:30","venue":"Menara BCA Grand Indonesia Lt 18.Jl. MH Thamrin No 1 Jakarta Selatan"},{"symbol":"BBCA.JK","kind":"public_expose","date":"2023-11-29","time":"10:00","venue":"Public Expose Live 2023"},{"symbol":"BBCA.JK","kind":"meeting","date":"2023-03-16","record_date":"2023-02-13","time":"09:30","venue":"Menara BCA"},{"symbol":"BBCA.JK","kind":"public_expose","date":"2022-09-14","time":"10:00","venue":"Public Expose Live 2022"},{"symbol":"BBCA.JK","kind":"meeting","date":"2022-03-17","record_date":"2022-02-14","time":"09:00","venue":"Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta"},{"symbol":"BBCA.JK","kind":"split","date":"2021-10-12","cum_date":"2021-10-12","ex_date":"2021-10-13","record_date":"2021-10-14","ratio":{"old":1,"new":5},"factor":5.0},{"symbol":"BBCA.JK","kind":"meeting","date":"2021-09-23","record_date":"2021-08-27","time":"10:00","venue":"Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta"},{"symbol":"BBCA.JK","kind":"public_expose","date":"2021-09-08","time":"15:00","venue":"Public Expose Live 2021"},{"symbol":"BBCA.JK","kind":"meeting","date":"2021-03-29","record_date":"2021-02-24","time":"09:00","venue":"Menara BCA, Grand Indonesia, Lt 19. Jl. M.H Thamrin No 1. Jakarta"},{"symbol":"BBCA.JK","kind":"meeting","date":"2020-07-30","record_date":"2020-07-03","time":"10:00","venue":"Menara BCA Grand Indonesia, Lt. 19. Jl. M.H. Thamrin No. 1. Jakarta"},{"symbol":"BBCA.JK","kind":"split","date":"2008-01-25","cum_date":"2008-01-25","ex_date":"2008-01-28","record_date":"2008-01-30","ratio":{"old":1,"new":2},"factor":2.0},{"symbol":"BBCA.JK","kind":"split","date":"2004-06-07","cum_date":"2004-06-07","ex_date":"2004-06-08","record_date":"2004-06-10","ratio":{"old":1,"new":2},"factor":2.0}]
--- stderr

View file

@ -0,0 +1,43 @@
exit: 0
--- stdout
Corporate actions for BBCA.JK
┌────────────┬───────────────┬────────────┬────────────┬─────┬────────────────────────────────────────────────────────────────────────────┐
│ DATE ┆ KIND ┆ EX ┆ RECORD ┆ PAY ┆ DETAIL │
╞════════════╪═══════════════╪════════════╪════════════╪═════╪════════════════════════════════════════════════════════════════════════════╡
│ 2026-09-09 ┆ public expose ┆ - ┆ - ┆ - ┆ 16:00 WIB, Public Expose Live 2026 yang tercantum pada website idx.co.i... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-03-12 ┆ meeting ┆ - ┆ 2026-02-09 ┆ - ┆ 14:00 WIB, Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta 1... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2025-09-11 ┆ public expose ┆ - ┆ - ┆ - ┆ 16:00 WIB, Public Expose Live 2025 yang tercantum pada website idx.co.i... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2025-03-12 ┆ meeting ┆ - ┆ 2025-02-07 ┆ - ┆ 09:30 WIB, Menara BCA, Grand Indonesia, Jl. M.H. Thamrin No. 1, Jakarta... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2024-08-28 ┆ public expose ┆ - ┆ - ┆ - ┆ 15:00 WIB, Public Expose Live 2024 Informasi Jadwal Pubex Live 2024 dan... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2024-03-14 ┆ meeting ┆ - ┆ 2024-02-07 ┆ - ┆ 09:30 WIB, Menara BCA Grand Indonesia Lt 18.Jl. MH Thamrin No 1 Jakarta... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2023-11-29 ┆ public expose ┆ - ┆ - ┆ - ┆ 10:00 WIB, Public Expose Live 2023 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2023-03-16 ┆ meeting ┆ - ┆ 2023-02-13 ┆ - ┆ 09:30 WIB, Menara BCA │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2022-09-14 ┆ public expose ┆ - ┆ - ┆ - ┆ 10:00 WIB, Public Expose Live 2022 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2022-03-17 ┆ meeting ┆ - ┆ 2022-02-14 ┆ - ┆ 09:00 WIB, Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2021-10-12 ┆ split ┆ 2021-10-13 ┆ 2021-10-14 ┆ - ┆ 1 becomes 5 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2021-09-23 ┆ meeting ┆ - ┆ 2021-08-27 ┆ - ┆ 10:00 WIB, Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2021-09-08 ┆ public expose ┆ - ┆ - ┆ - ┆ 15:00 WIB, Public Expose Live 2021 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2021-03-29 ┆ meeting ┆ - ┆ 2021-02-24 ┆ - ┆ 09:00 WIB, Menara BCA, Grand Indonesia, Lt 19. Jl. M.H Thamrin No 1. Ja... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2020-07-30 ┆ meeting ┆ - ┆ 2020-07-03 ┆ - ┆ 10:00 WIB, Menara BCA Grand Indonesia, Lt. 19. Jl. M.H. Thamrin No. 1. ... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2008-01-25 ┆ split ┆ 2008-01-28 ┆ 2008-01-30 ┆ - ┆ 1 becomes 2 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2004-06-07 ┆ split ┆ 2004-06-08 ┆ 2004-06-10 ┆ - ┆ 1 becomes 2 │
└────────────┴───────────────┴────────────┴────────────┴─────┴────────────────────────────────────────────────────────────────────────────┘
DATE is the cum date for dividends, bonuses, rights and splits (buy by that day to be entitled); for other actions, the event's first day.
Source: your Stockbit session; for personal, non-commercial use (Stockbit's terms).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
[{"symbol":"AUTO.JK","kind":"public_expose","date":"2026-11-05","time":"10:00","venue":"Zoom Webinar"},{"symbol":"BATA.JK","kind":"meeting","date":"2026-11-05","record_date":"2026-10-09","time":"14:00","venue":"Gedung Ventura, lantai 7. Jl. Raden Ajeng Kartini No.26, Cilandak Barat, Jakarta Selatan"},{"symbol":"AMAG.JK","kind":"public_expose","date":"2026-11-06","time":"15:00","venue":"Virtual Zoom Webinar, di PT Asuransi Multi Artha Guna Tbk, Gedung TCC Batavia Tower One, Jalan KH Mas Mansyur Kav 126, Jakarta Pusat"},{"symbol":"BRIS.JK","kind":"meeting","date":"2026-11-06","record_date":"2026-10-12","time":"10:00","venue":"Jakarta Pusat, melalui fasilitas Electronic General Meeting System KSEI (eASY.KSEI) dalam tautan https//akses.ksei.co.id"},{"symbol":"GIAA.JK","kind":"meeting","date":"2026-11-06","record_date":"2026-10-12","time":"14:00","venue":"Secara elektronik melalui sistem eASY.KSEI (https//akses.ksei.co.id/)"},{"symbol":"INPS.JK","kind":"meeting","date":"2026-11-06","record_date":"2026-10-07","time":"14:00","venue":"Gedung Inprase, Jl. Sunter Garden Raya Blok D8 No. 3G-3H, Sunter Agung, Tanjung Priok, Jakarta Utara, DKI Jakarta 14350"},{"symbol":"KDTN.JK","kind":"meeting","date":"2026-11-06","record_date":"2026-10-12","time":"14:00","venue":"PT Puri Sentul Permai Tbk diumumkan dan sesuai iklan)"},{"symbol":"MTFN.JK","kind":"meeting","date":"2026-11-06","record_date":"2026-10-12","time":"10:00","venue":"Kantor Taman E3.3, Unit D3A, Lantai 5, Jl. DR. Ide Anak Agung Gde Agung, Kuningan Jakarta Selatan"},{"symbol":"MTFN.JK","kind":"public_expose","date":"2026-11-06","time":"11:00","venue":"Kantor Taman E3.3, Unit D3A, Lantai 5 Jl. DR. Ide Anak Agung Gde Agung, Kuningan Jakarta Selatan"},{"symbol":"ULTJ.JK","kind":"meeting","date":"2026-11-06","record_date":"2026-08-31","time":"10:00","venue":"Kantor Perseroan, Jalan Raya Cimareme No 131 Bandung Barat, Jawa Barat Indonesia"}]
--- stderr

View file

@ -0,0 +1,29 @@
exit: 0
--- stdout
Corporate actions from 2026-11-05 to 2026-11-06 (10)
┌────────────┬────────┬───────────────┬────┬────────────────────────────────────────────────────────────────────────────┐
│ DATE ┆ SYMBOL ┆ KIND ┆ EX ┆ DETAIL │
╞════════════╪════════╪═══════════════╪════╪════════════════════════════════════════════════════════════════════════════╡
│ 2026-11-05 ┆ AUTO ┆ public expose ┆ - ┆ 10:00 WIB, Zoom Webinar │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-05 ┆ BATA ┆ meeting ┆ - ┆ 14:00 WIB, Gedung Ventura, lantai 7. Jl. Raden Ajeng Kartini No.26, Cil... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ AMAG ┆ public expose ┆ - ┆ 15:00 WIB, Virtual Zoom Webinar, di PT Asuransi Multi Artha Guna Tbk, G... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ BRIS ┆ meeting ┆ - ┆ 10:00 WIB, Jakarta Pusat, melalui fasilitas Electronic General Meeting ... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ GIAA ┆ meeting ┆ - ┆ 14:00 WIB, Secara elektronik melalui sistem eASY.KSEI (https//akses.kse... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ INPS ┆ meeting ┆ - ┆ 14:00 WIB, Gedung Inprase, Jl. Sunter Garden Raya Blok D8 No. 3G-3H, Su... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ KDTN ┆ meeting ┆ - ┆ 14:00 WIB, PT Puri Sentul Permai Tbk diumumkan dan sesuai iklan) │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ MTFN ┆ meeting ┆ - ┆ 10:00 WIB, Kantor Taman E3.3, Unit D3A, Lantai 5, Jl. DR. Ide Anak Agun... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ MTFN ┆ public expose ┆ - ┆ 11:00 WIB, Kantor Taman E3.3, Unit D3A, Lantai 5 Jl. DR. Ide Anak Agung... │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ ULTJ ┆ meeting ┆ - ┆ 10:00 WIB, Kantor Perseroan, Jalan Raya Cimareme No 131 Bandung Barat, ... │
└────────────┴────────┴───────────────┴────┴────────────────────────────────────────────────────────────────────────────┘
DATE is the cum date for dividends, bonuses, rights and splits (buy by that day to be entitled); for other actions, the event's first day.
Source: your Stockbit session; for personal, non-commercial use (Stockbit's terms).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
[{"symbol":"MGLV.JK","kind":"rights","date":"2026-11-04","cum_date":"2026-11-04","ex_date":"2026-11-05","record_date":"2026-11-06","ratio":{"old":100,"new":15},"factor":1.15,"price":8880,"period":{"start":"2026-11-10","end":"2026-11-23"}},{"symbol":"JPFA.JK","kind":"dividend","date":"2026-11-05","cum_date":"2026-11-05","ex_date":"2026-11-06","record_date":"2026-11-09","pay_date":"2026-11-12","cash_per_share":90.0},{"symbol":"PTPS.JK","kind":"dividend","date":"2026-11-05","cum_date":"2026-11-05","ex_date":"2026-11-06","record_date":"2026-11-09","pay_date":"2026-11-18","cash_per_share":2.0},{"symbol":"UNTR.JK","kind":"dividend","date":"2026-11-05","cum_date":"2026-11-05","ex_date":"2026-11-06","record_date":"2026-11-09","pay_date":"2026-11-23","cash_per_share":430.0},{"symbol":"CNMA.JK","kind":"dividend","date":"2026-11-06","cum_date":"2026-11-06","ex_date":"2026-11-09","record_date":"2026-11-10","pay_date":"2026-11-19","cash_per_share":4.0},{"symbol":"SMSM.JK","kind":"dividend","date":"2026-11-09","cum_date":"2026-11-09","ex_date":"2026-11-10","record_date":"2026-11-11","pay_date":"2026-11-26","cash_per_share":45.0},{"symbol":"AVIA.JK","kind":"dividend","date":"2026-11-10","cum_date":"2026-11-10","ex_date":"2026-11-11","record_date":"2026-11-12","pay_date":"2026-11-19","cash_per_share":12.0},{"symbol":"BWPT.JK","kind":"dividend","date":"2026-11-10","cum_date":"2026-11-10","ex_date":"2026-11-11","record_date":"2026-11-12","pay_date":"2026-11-25","cash_per_share":1.0},{"symbol":"ITMG.JK","kind":"dividend","date":"2026-11-10","cum_date":"2026-11-10","ex_date":"2026-11-11","record_date":"2026-11-12","pay_date":"2026-11-18","cash_per_share":1191.09},{"symbol":"MEDC.JK","kind":"dividend","date":"2026-11-10","cum_date":"2026-11-10","ex_date":"2026-11-11","record_date":"2026-11-12","pay_date":"2026-12-02","cash_per_share":21.65},{"symbol":"SSMS.JK","kind":"dividend","date":"2026-11-10","cum_date":"2026-11-10","ex_date":"2026-11-11","record_date":"2026-11-12","pay_date":"2026-12-02","cash_per_share":52.49},{"symbol":"TAPG.JK","kind":"dividend","date":"2026-11-10","cum_date":"2026-11-10","ex_date":"2026-11-11","record_date":"2026-11-12","pay_date":"2026-11-27","cash_per_share":70.0},{"symbol":"IFII.JK","kind":"dividend","date":"2026-11-11","cum_date":"2026-11-11","ex_date":"2026-11-12","record_date":"2026-11-13","pay_date":"2026-11-26","cash_per_share":5.0},{"symbol":"MIDI.JK","kind":"dividend","date":"2026-11-11","cum_date":"2026-11-11","ex_date":"2026-11-12","record_date":"2026-11-13","pay_date":"2026-12-01","cash_per_share":3.8}]
--- stderr

View file

@ -0,0 +1,37 @@
exit: 0
--- stdout
Corporate actions from 2026-11-03 to 2026-11-12 (14)
┌────────────┬────────┬──────────┬────────────┬───────────────────────────────────────────────────────────────────────┐
│ DATE ┆ SYMBOL ┆ KIND ┆ EX ┆ DETAIL │
╞════════════╪════════╪══════════╪════════════╪═══════════════════════════════════════════════════════════════════════╡
│ 2026-11-04 ┆ MGLV ┆ rights ┆ 2026-11-05 ┆ 15 rights per 100 held, at Rp 8,880, trading 2026-11-10 to 2026-11-23 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-05 ┆ JPFA ┆ dividend ┆ 2026-11-06 ┆ Rp 90 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-05 ┆ PTPS ┆ dividend ┆ 2026-11-06 ┆ Rp 2 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-05 ┆ UNTR ┆ dividend ┆ 2026-11-06 ┆ Rp 430 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-06 ┆ CNMA ┆ dividend ┆ 2026-11-09 ┆ Rp 4 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-09 ┆ SMSM ┆ dividend ┆ 2026-11-10 ┆ Rp 45 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-10 ┆ AVIA ┆ dividend ┆ 2026-11-11 ┆ Rp 12 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-10 ┆ BWPT ┆ dividend ┆ 2026-11-11 ┆ Rp 1 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-10 ┆ ITMG ┆ dividend ┆ 2026-11-11 ┆ Rp 1,191.09 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-10 ┆ MEDC ┆ dividend ┆ 2026-11-11 ┆ Rp 21.65 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-10 ┆ SSMS ┆ dividend ┆ 2026-11-11 ┆ Rp 52.49 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-10 ┆ TAPG ┆ dividend ┆ 2026-11-11 ┆ Rp 70 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-11 ┆ IFII ┆ dividend ┆ 2026-11-12 ┆ Rp 5 per share │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-11-11 ┆ MIDI ┆ dividend ┆ 2026-11-12 ┆ Rp 3.8 per share │
└────────────┴────────┴──────────┴────────────┴───────────────────────────────────────────────────────────────────────┘
DATE is the cum date for dividends, bonuses, rights and splits (buy by that day to be entitled); for other actions, the event's first day.
Source: your Stockbit session; for personal, non-commercial use (Stockbit's terms).
--- stderr

View file

@ -1,14 +1,16 @@
//! Fixture-backed provider for tests, enabled by the `testing` feature and //! Fixture-backed provider for tests, enabled by the `testing` feature and
//! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it. //! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it.
use super::stockbit::exodus; use super::stockbit::{corpaction, exodus};
use super::types::{Bar, Fundamentals, Interval, Intraday, OrderBook, Period, Quote}; use super::types::{Bar, Fundamentals, Interval, Intraday, OrderBook, Period, Quote};
use super::{ use super::{
BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, IntradayProvider, BrokerSummaryProvider, CorporateActionProvider, FundamentalsProvider, HistoryProvider,
OrderBookProvider, QuoteProvider, TradingSummaryProvider, idx, ipot, msn, stockbit, yahoo, IntradayProvider, OrderBookProvider, QuoteProvider, TradingSummaryProvider, idx, ipot, msn,
stockbit, yahoo,
}; };
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery}; use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
use crate::config::ProviderKind; use crate::config::ProviderKind;
use crate::corporate::types::{ActionKind, CorporateAction};
use crate::error::IdxError; use crate::error::IdxError;
pub(crate) fn enabled() -> bool { pub(crate) fn enabled() -> bool {
@ -224,6 +226,34 @@ impl OrderBookProvider for MockStockbitProvider {
} }
} }
/// `stockbit_exodus_corpaction_<type>.json` for the market,
/// `..._<type>_<code>.json` for one stock; a stock without a fixture has none
/// of that kind, as Stockbit answers.
impl CorporateActionProvider for MockStockbitProvider {
fn corporate_actions(
&self,
kind: ActionKind,
symbol: Option<&str>,
) -> Result<Vec<CorporateAction>, IdxError> {
let subject = symbol.unwrap_or("corporate actions");
let name = match symbol {
Some(symbol) => format!(
"corpaction_{}_{}.json",
corpaction::path_type(kind),
stockbit::stockbit_code(symbol)?.to_ascii_lowercase()
),
None => format!("corpaction_{}.json", corpaction::path_type(kind)),
};
let body = match self.exodus(subject, &name) {
Err(IdxError::SymbolNotFound(_)) if symbol.is_some() => {
r#"{"data":{"none":[]}}"#.to_string()
}
other => other?,
};
corpaction::parse_corporate_actions(kind, &body)
}
}
impl HistoryProvider for MockStockbitProvider { impl HistoryProvider for MockStockbitProvider {
fn history( fn history(
&self, &self,

View file

@ -78,6 +78,17 @@ pub trait OrderBookProvider {
fn orderbook(&self, symbol: &str) -> Result<OrderBook, IdxError>; fn orderbook(&self, symbol: &str) -> Result<OrderBook, IdxError>;
} }
/// Corporate actions of one kind: needs the user's Stockbit session.
pub trait CorporateActionProvider {
/// One stock's history (`symbol` resolved, e.g. `BBCA.JK`), or the
/// market's announced and recent actions with `None`.
fn corporate_actions(
&self,
kind: crate::corporate::types::ActionKind,
symbol: Option<&str>,
) -> Result<Vec<crate::corporate::types::CorporateAction>, IdxError>;
}
pub trait FundamentalsProvider { pub trait FundamentalsProvider {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>; fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
} }
@ -345,6 +356,14 @@ pub fn orderbook_provider() -> Box<dyn OrderBookProvider> {
Box::new(stockbit::StockbitProvider::new()) Box::new(stockbit::StockbitProvider::new())
} }
pub fn corporate_action_provider() -> Box<dyn CorporateActionProvider> {
#[cfg(feature = "testing")]
if mock::enabled() {
return Box::new(mock::MockStockbitProvider);
}
Box::new(stockbit::StockbitProvider::new())
}
/// Today in WIB, the date IDX trades on. /// Today in WIB, the date IDX trades on.
pub(crate) fn today_wib() -> chrono::NaiveDate { pub(crate) fn today_wib() -> chrono::NaiveDate {
#[cfg(feature = "testing")] #[cfg(feature = "testing")]

View file

@ -0,0 +1,405 @@
//! Corporate actions from Stockbit's session API:
//! `GET /corpaction/{type}?symbol={CODE}` (one stock's history) or without
//! `symbol` (the market, about a year back and the announced ones ahead; the
//! `limit` the web app sends is not honoured by every type).
//!
//! Each type names its fields with its own prefix (`dividend_`,
//! `rightissue_`, `wrant_`, `rups_`, `puexp_`, `tender_`, ...), bonus rows
//! reuse `stocksplit_*`, and the list sits under a key that is not always the
//! path's (`tenderoffer` → `tender`, `reversesplit` → `stock_reverse`). So
//! the entitlement kinds are read by field suffix (`_cumdate`, `_exdate`,
//! ...), which also covers the two kinds no row was recorded for yet
//! (`stock_dividend`, `reversesplit`, both empty on 2026-11-03). Placeholders
//! (`""`, `0`, `0001-01-01 00:00:00`) read as absent; a row without a symbol
//! or a date is dropped.
use chrono::{Datelike, NaiveDate};
use serde_json::{Map, Value};
use crate::corporate::types::{ActionKind, CorporateAction, DateRange, Ratio};
use crate::error::IdxError;
/// Stockbit's path segment for `kind`.
pub fn path_type(kind: ActionKind) -> &'static str {
match kind {
ActionKind::Dividend => "dividend",
ActionKind::StockDividend => "stock_dividend",
ActionKind::Bonus => "bonus",
ActionKind::Rights => "rightissue",
ActionKind::Warrant => "warrant",
ActionKind::Split => "stocksplit",
ActionKind::ReverseSplit => "reversesplit",
ActionKind::Meeting => "rups",
ActionKind::PublicExpose => "pubex",
ActionKind::Ipo => "ipo",
ActionKind::TenderOffer => "tenderoffer",
}
}
/// The actions of one `kind` in a `/corpaction/{type}` response, in the
/// response's order.
pub fn parse_corporate_actions(
kind: ActionKind,
body: &str,
) -> Result<Vec<CorporateAction>, IdxError> {
let bad = |what: String| IdxError::ParseError(format!("stockbit corporate actions: {what}"));
let root: Value = serde_json::from_str(body).map_err(|e| bad(e.to_string()))?;
let rows = match root.get("data") {
Some(Value::Object(data)) => match data.values().find_map(Value::as_array) {
Some(rows) => rows.as_slice(),
None if data.values().all(Value::is_null) => &[],
None => return Err(bad("no list in data".to_string())),
},
Some(Value::Array(rows)) => rows.as_slice(),
Some(Value::Null) => &[],
_ => return Err(bad("no data".to_string())),
};
Ok(rows
.iter()
.filter_map(Value::as_object)
.filter_map(|row| parse_row(kind, row))
.collect())
}
fn parse_row(kind: ActionKind, row: &Map<String, Value>) -> Option<CorporateAction> {
let code = text(row.get("company_symbol"))?.to_ascii_uppercase();
if !code.bytes().all(|b| b.is_ascii_alphanumeric()) {
return None;
}
let symbol = format!("{code}.JK");
let mut action = if kind.is_entitlement() {
entitlement(kind, symbol, row)?
} else {
event(kind, symbol, row)?
};
if matches!(kind, ActionKind::Ipo | ActionKind::TenderOffer) {
action.name = text(row.get("company_name")).map(str::to_string);
}
Some(action)
}
/// Dividends, bonuses, rights and splits: cum/ex/record/pay dates, a ratio
/// and an adjustment factor.
fn entitlement(
kind: ActionKind,
symbol: String,
row: &Map<String, Value>,
) -> Option<CorporateAction> {
let cum = date(suffixed(row, "cumdate"));
let ex = date(suffixed(row, "exdate"));
let mut action = CorporateAction::new(symbol, kind, cum.or(ex)?);
action.cum_date = cum;
action.ex_date = ex;
action.record_date = date(suffixed(row, "recdate"));
action.pay_date = date(suffixed(row, "paydate")).or_else(|| date(suffixed(row, "paymentdate")));
action.ratio = ratio(row);
action.factor = number(suffixed(row, "factor"));
match kind {
ActionKind::Dividend => {
action.cash_per_share = number(suffixed(row, "value")).map(|v| round_to(v, 2));
}
ActionKind::Rights => {
action.price = whole(suffixed(row, "price"));
action.period = range(suffixed(row, "trading_start"), suffixed(row, "trading_end"));
}
_ => {}
}
Some(action)
}
/// Warrants, meetings, exposes, IPOs and tender offers.
fn event(kind: ActionKind, symbol: String, row: &Map<String, Value>) -> Option<CorporateAction> {
let get = |key: &str| row.get(key);
let mut action;
match kind {
ActionKind::Warrant => {
let trading = range(get("wrant_trading_from"), get("wrant_trading_end"));
let exercise = range(get("wrant_exc_from"), get("wrant_exc_end"));
let first = date(get("wrant_trading_from")).or(exercise.map(|r| r.start))?;
action = CorporateAction::new(symbol, kind, first);
action.period = trading;
action.exercise = exercise;
action.price = whole(get("wrant_exc_price"));
}
ActionKind::Meeting | ActionKind::PublicExpose => {
let prefix = if kind == ActionKind::Meeting {
"rups"
} else {
"puexp"
};
let field = |name: &str| row.get(&format!("{prefix}_{name}"));
action = CorporateAction::new(symbol, kind, date(field("date"))?);
action.time = text(field("time")).map(clock);
action.venue = text(field("venue")).map(squeeze);
if kind == ActionKind::Meeting {
action.record_date = date(field("eligible_date"));
}
}
ActionKind::Ipo => {
action = CorporateAction::new(symbol, kind, date(get("ipo_listing_date"))?);
action.price = whole(row.get("ipo_price").and_then(|p| p.get("final")));
let detail = row.get("ipo_data_detail");
let detail = |key: &str| detail.and_then(|d| d.get(key));
action.shares = count(detail("shares"));
action.period = range(detail("offering_start"), detail("offering_end"));
}
ActionKind::TenderOffer => {
let period = range(get("tender_start"), get("tender_end"));
let first = date(get("tender_start")).or(date(get("tender_end")))?;
action = CorporateAction::new(symbol, kind, first);
action.period = period;
action.pay_date = date(get("tender_paydate"));
action.price = whole(get("tender_price"));
action.shares = count(get("tender_shares"));
action.percentage_bps =
number(get("tender_percentage")).map(|p| (p * 100.0).round() as i64);
}
_ => return None,
}
Some(action)
}
/// The value of the one field named `<prefix>_<suffix>`; the shortest name
/// wins should several end so.
fn suffixed<'a>(row: &'a Map<String, Value>, suffix: &str) -> Option<&'a Value> {
let tail = format!("_{suffix}");
row.iter()
.filter(|(key, _)| key.ends_with(&tail))
.min_by_key(|(key, _)| key.len())
.map(|(_, value)| value)
}
/// `old`/`new` fields, else the `"6 : 4"` text.
fn ratio(row: &Map<String, Value>) -> Option<Ratio> {
let pair = |old: Option<u64>, new: Option<u64>| {
Some(Ratio {
old: old?,
new: new?,
})
};
pair(count(suffixed(row, "old")), count(suffixed(row, "new"))).or_else(|| {
let text = text(suffixed(row, "ratio"))?;
let (old, new) = text.split_once(':')?;
pair(
old.trim().replace(',', "").parse().ok().filter(|n| *n > 0),
new.trim().replace(',', "").parse().ok().filter(|n| *n > 0),
)
})
}
fn text(value: Option<&Value>) -> Option<&str> {
value
.and_then(Value::as_str)
.map(str::trim)
.filter(|s| !s.is_empty())
}
/// `YYYY-MM-DD`, optionally followed by a time; year 1 is a placeholder.
fn date(value: Option<&Value>) -> Option<NaiveDate> {
let text = text(value)?;
let day = text.get(..10)?;
NaiveDate::parse_from_str(day, "%Y-%m-%d")
.ok()
.filter(|d| d.year() >= 1900)
}
fn range(start: Option<&Value>, end: Option<&Value>) -> Option<DateRange> {
let (start, end) = (date(start)?, date(end)?);
(start <= end).then_some(DateRange { start, end })
}
/// A positive number from a JSON number or a display string (`"1,191.09"`,
/// `"Rp 25"`); zero is a placeholder.
fn number(value: Option<&Value>) -> Option<f64> {
let n = match value? {
Value::Number(n) => n.as_f64()?,
Value::String(s) => {
let cleaned: String = s
.trim()
.trim_start_matches("Rp")
.chars()
.filter(|c| *c != ',' && !c.is_whitespace())
.collect();
cleaned.parse().ok()?
}
_ => return None,
};
(n.is_finite() && n > 0.0).then_some(n)
}
fn whole(value: Option<&Value>) -> Option<i64> {
number(value).map(|n| n.round() as i64)
}
fn count(value: Option<&Value>) -> Option<u64> {
number(value).map(|n| n.round() as u64)
}
fn round_to(value: f64, places: i32) -> f64 {
let scale = 10f64.powi(places);
(value * scale).round() / scale
}
/// `13:00:00` → `13:00`; anything else as given.
fn clock(raw: &str) -> String {
match raw.split(':').collect::<Vec<_>>().as_slice() {
[h, m, s] if s.len() == 2 && h.len() <= 2 && m.len() == 2 => format!("{h}:{m}"),
_ => raw.to_string(),
}
}
fn squeeze(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> String {
std::fs::read_to_string(format!(
"{}/tests/fixtures/stockbit_exodus_corpaction_{name}.json",
env!("CARGO_MANIFEST_DIR")
))
.unwrap()
}
fn day(s: &str) -> NaiveDate {
NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
}
fn find<'a>(actions: &'a [CorporateAction], symbol: &str) -> &'a CorporateAction {
actions.iter().find(|a| a.symbol == symbol).unwrap()
}
#[test]
fn test_parse_dividends_keep_cents_and_dates() {
let bbca =
parse_corporate_actions(ActionKind::Dividend, &fixture("dividend_bbca")).unwrap();
assert_eq!(bbca.len(), 37);
let latest = &bbca[0];
assert_eq!(latest.symbol, "BBCA.JK");
assert_eq!(latest.date, day("2026-08-28"));
assert_eq!(
(latest.ex_date, latest.record_date, latest.pay_date),
(
Some(day("2026-08-31")),
Some(day("2026-09-01")),
Some(day("2026-09-16"))
)
);
assert_eq!(latest.cash_per_share, Some(25.0));
assert_eq!(latest.ratio, None);
let market = parse_corporate_actions(ActionKind::Dividend, &fixture("dividend")).unwrap();
assert_eq!(find(&market, "ITMG.JK").cash_per_share, Some(1191.09));
}
#[test]
fn test_parse_bonus_reads_split_named_fields() {
let bonus = parse_corporate_actions(ActionKind::Bonus, &fixture("bonus")).unwrap();
let ppgl = find(&bonus, "PPGL.JK");
assert_eq!(ppgl.date, day("2026-11-02"));
assert_eq!(ppgl.ex_date, Some(day("2026-11-03")));
assert_eq!(ppgl.pay_date, Some(day("2026-11-25")));
assert_eq!(ppgl.ratio, Some(Ratio { old: 6, new: 4 }));
assert_eq!(ppgl.factor, Some(1.6667));
}
#[test]
fn test_parse_rights_and_split() {
let rights = parse_corporate_actions(ActionKind::Rights, &fixture("rightissue")).unwrap();
let bmas = find(&rights, "BMAS.JK");
assert_eq!(bmas.date, day("2026-11-23"));
assert_eq!(bmas.ratio, Some(Ratio { old: 655, new: 104 }));
assert_eq!(bmas.price, Some(350));
assert_eq!(
bmas.period,
Some(DateRange {
start: day("2026-11-27"),
end: day("2026-12-03")
})
);
let split =
parse_corporate_actions(ActionKind::Split, &fixture("stocksplit_bbca")).unwrap();
assert_eq!(split[0].date, day("2021-10-12"));
assert_eq!(split[0].ratio, Some(Ratio { old: 1, new: 5 }));
assert_eq!(split[0].factor, Some(5.0));
}
#[test]
fn test_parse_events() {
let warrants = parse_corporate_actions(ActionKind::Warrant, &fixture("warrant")).unwrap();
let brna = find(&warrants, "BRNA.JK");
assert_eq!(brna.date, day("2026-10-16"));
assert_eq!(brna.price, Some(800));
assert_eq!(brna.exercise.map(|r| r.end), Some(day("2029-10-15")));
let meetings = parse_corporate_actions(ActionKind::Meeting, &fixture("rups")).unwrap();
let arti = find(&meetings, "ARTI.JK");
assert_eq!(arti.date, day("2026-12-09"));
assert_eq!(arti.time.as_deref(), Some("10:30"));
assert_eq!(arti.record_date, Some(day("2026-11-12")));
let exposes = parse_corporate_actions(ActionKind::PublicExpose, &fixture("pubex")).unwrap();
assert_eq!(find(&exposes, "MEJA.JK").time.as_deref(), Some("13:00"));
let tenders =
parse_corporate_actions(ActionKind::TenderOffer, &fixture("tenderoffer")).unwrap();
let auto = find(&tenders, "AUTO.JK");
assert_eq!(auto.date, day("2026-10-13"));
assert_eq!(auto.price, Some(3600));
assert_eq!(auto.shares, Some(238_442_293));
assert_eq!(auto.percentage_bps, Some(495));
assert_eq!(auto.pay_date, Some(day("2026-11-18")));
assert_eq!(auto.name.as_deref(), Some("Astra Otoparts Tbk."));
}
#[test]
fn test_parse_ipo_drops_placeholders() {
let ipos = parse_corporate_actions(ActionKind::Ipo, &fixture("ipo")).unwrap();
let rans = find(&ipos, "RANS.JK");
assert_eq!(rans.price, Some(170));
assert_eq!(rans.shares, Some(2_525_000_000));
assert_eq!(rans.period.map(|r| r.start), Some(day("2026-07-02")));
// An ETF listed without offer terms: no price, no period.
let xkgs = find(&ipos, "XKGS.JK");
assert_eq!((xkgs.price, xkgs.period, xkgs.shares), (None, None, None));
}
#[test]
fn test_parse_empty_lists_and_odd_keys() {
for (kind, body) in [
(
ActionKind::StockDividend,
r#"{"data":{"stock_dividend":[]}}"#,
),
(ActionKind::ReverseSplit, r#"{"data":{"stock_reverse":[]}}"#),
(ActionKind::Dividend, r#"{"data":null}"#),
] {
assert!(parse_corporate_actions(kind, body).unwrap().is_empty());
}
assert!(parse_corporate_actions(ActionKind::Dividend, r#"{"message":"x"}"#).is_err());
// A reverse split named like a split, read by suffix.
let body = r#"{"data":{"stock_reverse":[{"company_symbol":"ABCD",
"reverse_cumdate":"2026-12-01","reverse_exdate":"2026-12-02",
"reverse_old":"10","reverse_new":"1","reverse_factor":"0.1"}]}}"#;
let rows = parse_corporate_actions(ActionKind::ReverseSplit, body).unwrap();
assert_eq!(rows[0].ratio, Some(Ratio { old: 10, new: 1 }));
assert_eq!(rows[0].factor, Some(0.1));
}
#[test]
fn test_parse_numbers_and_placeholders() {
assert_eq!(number(Some(&Value::from("1,191.09"))), Some(1191.09));
assert_eq!(number(Some(&Value::from("Rp 25"))), Some(25.0));
assert_eq!(number(Some(&Value::from(0))), None);
assert_eq!(date(Some(&Value::from("0001-01-01 00:00:00"))), None);
assert_eq!(
date(Some(&Value::from("2026-07-02 00:00:00"))),
Some(day("2026-07-02"))
);
assert_eq!(clock("09:00:00"), "09:00");
assert_eq!(clock("10:30"), "10:30");
}
}

View file

@ -25,9 +25,11 @@ use chrono::{Months, NaiveDate};
use serde_json::Value; use serde_json::Value;
use super::auth::SessionStore; use super::auth::SessionStore;
use super::corpaction;
use super::parse::number; use super::parse::number;
use super::{USER_AGENT, pace, stockbit_code}; use super::{USER_AGENT, pace, stockbit_code};
use crate::api::types::{BookLevel, Interval, Ohlc, OrderBook, Period, Quote}; use crate::api::types::{BookLevel, Interval, Ohlc, OrderBook, Period, Quote};
use crate::corporate::types::{ActionKind, CorporateAction};
use crate::error::IdxError; use crate::error::IdxError;
const BASE_URL: &str = "https://exodus.stockbit.com"; const BASE_URL: &str = "https://exodus.stockbit.com";
@ -120,6 +122,24 @@ impl Exodus {
daily_for_period(&body, period) daily_for_period(&body, period)
} }
/// Corporate actions of `kind`: one stock's (`resolved`) or the market's.
pub(crate) fn corporate_actions(
&self,
kind: ActionKind,
resolved: Option<&str>,
) -> Result<Vec<CorporateAction>, IdxError> {
let path = format!("/corpaction/{}", corpaction::path_type(kind));
let (path, subject) = match resolved {
Some(resolved) => (
format!("{path}?symbol={}", stockbit_code(resolved)?),
resolved.to_string(),
),
None => (path, "corporate actions".to_string()),
};
let body = self.get(&path, &subject)?;
corpaction::parse_corporate_actions(kind, &body)
}
/// The market's state, read at most every `MARKET_STATE_TTL` per process. /// The market's state, read at most every `MARKET_STATE_TTL` per process.
/// Unknown on any failure, which hides bid/offer. /// Unknown on any failure, which hides bid/offer.
fn market_status(&self) -> Option<String> { fn market_status(&self) -> Option<String> {

View file

@ -17,6 +17,7 @@
pub mod auth; pub mod auth;
pub mod browser; pub mod browser;
pub mod corpaction;
pub mod exodus; pub mod exodus;
pub mod loopback; pub mod loopback;
mod parse; mod parse;
@ -31,8 +32,10 @@ pub use parse::{parse_symbol_page, status_error, stockbit_code};
use super::types::{Bar, Fundamentals, Interval, Intraday, OrderBook, Period, Quote}; use super::types::{Bar, Fundamentals, Interval, Intraday, OrderBook, Period, Quote};
use super::{ use super::{
FundamentalsProvider, HistoryProvider, IntradayProvider, OrderBookProvider, QuoteProvider, CorporateActionProvider, FundamentalsProvider, HistoryProvider, IntradayProvider,
OrderBookProvider, QuoteProvider,
}; };
use crate::corporate::types::{ActionKind, CorporateAction};
use crate::error::IdxError; use crate::error::IdxError;
use auth::SessionStore; use auth::SessionStore;
use exodus::Exodus; use exodus::Exodus;
@ -203,6 +206,16 @@ impl OrderBookProvider for StockbitProvider {
} }
} }
impl CorporateActionProvider for StockbitProvider {
fn corporate_actions(
&self,
kind: ActionKind,
symbol: Option<&str>,
) -> Result<Vec<CorporateAction>, IdxError> {
self.require_session()?.corporate_actions(kind, symbol)
}
}
impl HistoryProvider for StockbitProvider { impl HistoryProvider for StockbitProvider {
fn history( fn history(
&self, &self,

View file

@ -94,6 +94,10 @@ impl Idx {
crate::flow::Flow { idx: self } crate::flow::Flow { idx: self }
} }
pub fn corporate(&self) -> crate::corporate::Corporate<'_> {
crate::corporate::Corporate { idx: self }
}
pub fn financial(&self) -> crate::financial::Financial<'_> { pub fn financial(&self) -> crate::financial::Financial<'_> {
crate::financial::Financial { idx: self } crate::financial::Financial { idx: self }
} }

View file

@ -0,0 +1,108 @@
//! Corporate actions (cash and stock dividends, bonus shares, rights issues,
//! warrants, splits, shareholders' meetings, public exposes, IPOs, tender
//! offers) from the user's Stockbit session: one stock's history, or the
//! market's calendar. Stockbit serves one kind per request, so a query costs
//! one request per kind (paced 1 s apart), each cached for 6 hours.
pub mod types;
use chrono::{NaiveDate, Utc};
use crate::api::{corporate_action_provider, stockbit, today_wib};
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
use crate::error::IdxError;
use types::{ActionKind, CalendarRange, CorporateAction};
/// Announcements change a few times a day at most.
const TTL_SECS: u64 = 6 * 3600;
const BUCKET: &str = "stockbit-corpaction";
pub struct Corporate<'a> {
pub(crate) idx: &'a Idx,
}
impl Corporate<'_> {
/// Today in WIB.
pub fn today(&self) -> NaiveDate {
today_wib()
}
/// One IDX stock's corporate actions of `kinds`, newest first.
pub fn actions(
&self,
symbol: &str,
kinds: &[ActionKind],
policy: CachePolicy,
) -> Result<Fetched<Vec<CorporateAction>>, IdxError> {
let (resolved, _) = self.idx.idx_code(symbol, "corporate actions")?;
let mut fetched = self.fetch(kinds, Some(&resolved), policy)?;
fetched
.data
.sort_by(|a, b| b.date.cmp(&a.date).then(a.kind.cmp(&b.kind)));
Ok(fetched)
}
/// The market's corporate actions of `kinds` dated within `range` (see
/// `CorporateAction::date`), oldest first.
pub fn calendar(
&self,
range: CalendarRange,
kinds: &[ActionKind],
policy: CachePolicy,
) -> Result<Fetched<Vec<CorporateAction>>, IdxError> {
let fetched = self.fetch(kinds, None, policy)?;
Ok(fetched.map(|mut actions| {
actions.retain(|a| range.contains(a.date));
actions.sort_by(|a, b| (a.date, &a.symbol, a.kind).cmp(&(b.date, &b.symbol, b.kind)));
actions
}))
}
/// Every kind in turn, through the cache; the result is as old as its
/// oldest part and stale if any part is.
fn fetch(
&self,
kinds: &[ActionKind],
resolved: Option<&str>,
policy: CachePolicy,
) -> Result<Fetched<Vec<CorporateAction>>, IdxError> {
if kinds.is_empty() {
return Err(IdxError::InvalidInput(
"name at least one kind of corporate action".to_string(),
));
}
let provider = corporate_action_provider();
let subject = resolved.unwrap_or("corporate actions");
let mut merged = Fetched {
data: Vec::new(),
source: stockbit::SOURCE,
fetched_at: Utc::now(),
stale: false,
};
let mut seen = Vec::with_capacity(kinds.len());
for &kind in kinds {
if seen.contains(&kind) {
continue;
}
seen.push(kind);
let key = format!("{}:{}", kind.as_str(), resolved.unwrap_or("market"));
let part = cached_fetch(
&self.idx.cache,
CacheSpec {
bucket: BUCKET,
key: &key,
ttl_secs: TTL_SECS,
subject,
source: stockbit::SOURCE,
},
policy,
|| provider.corporate_actions(kind, resolved),
Ok,
)?;
merged.fetched_at = merged.fetched_at.min(part.fetched_at);
merged.stale |= part.stale;
merged.data.extend(part.data);
}
Ok(merged)
}
}

View file

@ -0,0 +1,292 @@
use chrono::{Duration, NaiveDate};
use serde::{Deserialize, Serialize};
use crate::error::IdxError;
/// The kinds of corporate action Stockbit lists, one request each.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "snake_case")]
pub enum ActionKind {
/// Cash dividend.
Dividend,
#[cfg_attr(feature = "clap", value(name = "stock-dividend"))]
StockDividend,
/// Bonus shares.
Bonus,
/// Rights issue (HMETD).
Rights,
Warrant,
/// Stock split.
Split,
#[cfg_attr(feature = "clap", value(name = "reverse-split"))]
ReverseSplit,
/// Shareholders' meeting (RUPS).
Meeting,
/// Public expose.
#[cfg_attr(feature = "clap", value(name = "expose"))]
PublicExpose,
Ipo,
#[cfg_attr(feature = "clap", value(name = "tender"))]
TenderOffer,
}
impl ActionKind {
pub const ALL: [Self; 11] = [
Self::Dividend,
Self::StockDividend,
Self::Bonus,
Self::Rights,
Self::Warrant,
Self::Split,
Self::ReverseSplit,
Self::Meeting,
Self::PublicExpose,
Self::Ipo,
Self::TenderOffer,
];
/// What one stock's history covers by default: everything but its IPO.
pub const STOCK_DEFAULT: [Self; 10] = [
Self::Dividend,
Self::StockDividend,
Self::Bonus,
Self::Rights,
Self::Warrant,
Self::Split,
Self::ReverseSplit,
Self::Meeting,
Self::PublicExpose,
Self::TenderOffer,
];
/// The CLI's name (`--kind`), also used in MCP inputs.
pub fn as_str(self) -> &'static str {
match self {
Self::Dividend => "dividend",
Self::StockDividend => "stock-dividend",
Self::Bonus => "bonus",
Self::Rights => "rights",
Self::Warrant => "warrant",
Self::Split => "split",
Self::ReverseSplit => "reverse-split",
Self::Meeting => "meeting",
Self::PublicExpose => "expose",
Self::Ipo => "ipo",
Self::TenderOffer => "tender",
}
}
/// Whether the action entitles holders on a cum date (and so moves the
/// price on the ex date).
pub fn is_entitlement(self) -> bool {
matches!(
self,
Self::Dividend
| Self::StockDividend
| Self::Bonus
| Self::Rights
| Self::Split
| Self::ReverseSplit
)
}
}
impl std::str::FromStr for ActionKind {
type Err = IdxError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|kind| kind.as_str() == value)
.ok_or_else(|| {
let names: Vec<_> = Self::ALL.iter().map(|k| k.as_str()).collect();
IdxError::InvalidInput(format!(
"invalid kind '{value}' (expected one of: {})",
names.join(", ")
))
})
}
}
/// `old : new` as Stockbit states it: a 1:5 split turns 1 share into 5; a
/// 655:104 rights issue gives 104 rights per 655 shares held; a 6:4 bonus
/// gives 4 bonus shares per 6 held.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ratio {
pub old: u64,
pub new: u64,
}
/// Inclusive dates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DateRange {
pub start: NaiveDate,
pub end: NaiveDate,
}
/// One corporate action of one stock. Which fields are present depends on
/// `kind`; absent ones are left out of JSON.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CorporateAction {
/// `BBCA.JK`.
pub symbol: String,
pub kind: ActionKind,
/// Where the action sits on a calendar: the cum date for entitlements
/// (the last day to buy and be entitled), else the meeting or expose
/// day, the listing day, a warrant's first trading day, or a tender
/// offer's first day.
pub date: NaiveDate,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cum_date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ex_date: Option<NaiveDate>,
/// Recording date; for a meeting, the date that decides who may attend.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub record_date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pay_date: Option<NaiveDate>,
/// Cash dividend per share, IDR as Stockbit gives it (up to 2 decimals,
/// e.g. 3.8).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cash_per_share: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ratio: Option<Ratio>,
/// Stockbit's price adjustment factor: 5 for a 1:5 split, 1.6667 for a
/// 6:4 bonus.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub factor: Option<f64>,
/// Whole IDR: the rights exercise price, a warrant's exercise price, the
/// tender offer price or the final IPO price.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub price: Option<i64>,
/// When rights or warrants trade, a tender offer runs, or an IPO is
/// offered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub period: Option<DateRange>,
/// When warrants can be exercised.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exercise: Option<DateRange>,
/// Shares offered (IPO) or sought (tender offer).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shares: Option<u64>,
/// Share of the company a tender offer seeks, basis points.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub percentage_bps: Option<i64>,
/// Meeting or expose time, as Stockbit writes it (WIB).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub venue: Option<String>,
/// The issuer's name, when Stockbit gives it (IPO, tender offer).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl CorporateAction {
pub fn new(symbol: String, kind: ActionKind, date: NaiveDate) -> Self {
Self {
symbol,
kind,
date,
cum_date: None,
ex_date: None,
record_date: None,
pay_date: None,
cash_per_share: None,
ratio: None,
factor: None,
price: None,
period: None,
exercise: None,
shares: None,
percentage_bps: None,
time: None,
venue: None,
name: None,
}
}
}
/// Default calendar span, days from today.
pub const CALENDAR_DEFAULT_DAYS: i64 = 30;
/// Longest calendar range, days.
pub const CALENDAR_MAX_DAYS: i64 = 366;
/// The dates a market-wide calendar covers, inclusive.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CalendarRange {
pub from: NaiveDate,
pub to: NaiveDate,
}
impl CalendarRange {
/// `from` defaults to today, `to` to 30 days after `from`.
pub fn from_args(
from: Option<&str>,
to: Option<&str>,
today: NaiveDate,
) -> Result<Self, IdxError> {
let parse = |name: &str, value: &str| {
NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d").map_err(|_| {
IdxError::InvalidInput(format!("invalid {name} '{value}'; use YYYY-MM-DD"))
})
};
let from = from.map(|v| parse("from", v)).transpose()?.unwrap_or(today);
let to = to
.map(|v| parse("to", v))
.transpose()?
.unwrap_or(from + Duration::days(CALENDAR_DEFAULT_DAYS));
if to < from {
return Err(IdxError::InvalidInput(format!(
"to ({to}) is before from ({from})"
)));
}
if (to - from).num_days() > CALENDAR_MAX_DAYS {
return Err(IdxError::InvalidInput(format!(
"a calendar covers at most {CALENDAR_MAX_DAYS} days"
)));
}
Ok(Self { from, to })
}
pub fn contains(&self, date: NaiveDate) -> bool {
(self.from..=self.to).contains(&date)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn day(s: &str) -> NaiveDate {
NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
}
#[test]
fn test_calendar_range_defaults_and_bounds() {
let today = day("2026-11-03");
let range = CalendarRange::from_args(None, None, today).unwrap();
assert_eq!((range.from, range.to), (today, day("2026-12-03")));
let range = CalendarRange::from_args(Some("2026-11-10"), None, today).unwrap();
assert_eq!(range.to, day("2026-12-10"));
for (from, to) in [
(Some("2026-11-10"), Some("2026-11-09")),
(Some("2026-01-01"), Some("2027-06-01")),
(Some("11/10/2026"), None),
] {
assert!(matches!(
CalendarRange::from_args(from, to, today),
Err(IdxError::InvalidInput(_))
));
}
}
#[test]
fn test_action_kind_names_round_trip() {
for kind in ActionKind::ALL {
assert_eq!(kind.as_str().parse::<ActionKind>().unwrap(), kind);
}
assert!("rups".parse::<ActionKind>().is_err());
}
}

View file

@ -8,6 +8,7 @@ pub mod broker;
pub mod cache; pub mod cache;
mod client; mod client;
pub mod config; pub mod config;
pub mod corporate;
pub mod curl_impersonate; pub mod curl_impersonate;
pub mod error; pub mod error;
pub mod financial; pub mod financial;

View file

@ -0,0 +1 @@
{"data":{"bonus":[{"company_id":"1000003000","company_symbol":"PPGL","corp_action_active":true,"sahambonus_id":"7732","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"6 : 4","stocksplit_created":"","stocksplit_cumdate":"2026-11-02","stocksplit_exdate":"2026-11-03","stocksplit_factor":"1.6667","stocksplit_new":"4","stocksplit_old":"6","stocksplit_paymentdate":"2026-11-25","stocksplit_recdate":"2026-11-04","event_note":""},{"company_id":"397","company_symbol":"SPMA","corp_action_active":false,"sahambonus_id":"7730","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"100 : 30","stocksplit_created":"","stocksplit_cumdate":"2026-07-08","stocksplit_exdate":"2026-07-09","stocksplit_factor":"1.3","stocksplit_new":"30","stocksplit_old":"100","stocksplit_paymentdate":"2026-07-30","stocksplit_recdate":"2026-07-10","event_note":""},{"company_id":"457","company_symbol":"WINS","corp_action_active":false,"sahambonus_id":"7731","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"71 : 1","stocksplit_created":"","stocksplit_cumdate":"2026-05-25","stocksplit_exdate":"2026-05-26","stocksplit_factor":"1.0141","stocksplit_new":"1","stocksplit_old":"71","stocksplit_paymentdate":"2026-06-18","stocksplit_recdate":"2026-05-29","event_note":""},{"company_id":"1000004517","company_symbol":"MEJA","corp_action_active":false,"sahambonus_id":"7727","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"6 : 1","stocksplit_created":"","stocksplit_cumdate":"2026-04-16","stocksplit_exdate":"2026-04-17","stocksplit_factor":"1.1667","stocksplit_new":"1","stocksplit_old":"6","stocksplit_paymentdate":"2026-05-08","stocksplit_recdate":"2026-04-20","event_note":""},{"company_id":"1000004196","company_symbol":"CNMA","corp_action_active":false,"sahambonus_id":"7728","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"50 : 1","stocksplit_created":"","stocksplit_cumdate":"2026-04-14","stocksplit_exdate":"2026-04-15","stocksplit_factor":"1.02","stocksplit_new":"1","stocksplit_old":"50","stocksplit_paymentdate":"2026-04-28","stocksplit_recdate":"2026-04-16","event_note":""},{"company_id":"284","company_symbol":"MEGA","corp_action_active":false,"sahambonus_id":"7723","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"1 : 1","stocksplit_created":"","stocksplit_cumdate":"2026-04-09","stocksplit_exdate":"2026-04-10","stocksplit_factor":"2","stocksplit_new":"1","stocksplit_old":"1","stocksplit_paymentdate":"2026-04-30","stocksplit_recdate":"2026-04-13","event_note":""},{"company_id":"1000003506","company_symbol":"WGSH","corp_action_active":false,"sahambonus_id":"7722","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"1 : 1","stocksplit_created":"","stocksplit_cumdate":"2026-04-02","stocksplit_exdate":"2026-04-06","stocksplit_factor":"2","stocksplit_new":"1","stocksplit_old":"1","stocksplit_paymentdate":"2026-04-24","stocksplit_recdate":"2026-04-07","event_note":""},{"company_id":"1000000034","company_symbol":"RISE","corp_action_active":false,"sahambonus_id":"7721","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"25 : 12","stocksplit_created":"","stocksplit_cumdate":"2026-01-19","stocksplit_exdate":"2026-01-20","stocksplit_factor":"1.48","stocksplit_new":"12","stocksplit_old":"25","stocksplit_paymentdate":"2026-02-09","stocksplit_recdate":"2026-01-21","event_note":""},{"company_id":"43","company_symbol":"ASRM","corp_action_active":false,"sahambonus_id":"7720","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"20 : 1","stocksplit_created":"","stocksplit_cumdate":"2025-11-12","stocksplit_exdate":"2025-11-13","stocksplit_factor":"1.0500","stocksplit_new":"1","stocksplit_old":"20","stocksplit_paymentdate":"2025-12-05","stocksplit_recdate":"2025-11-14","event_note":""},{"company_id":"397","company_symbol":"SPMA","corp_action_active":false,"sahambonus_id":"7719","sahambonus_iqp_id":"","sahambonus_lastupdate":"","sahambonus_new_price":0,"sahambonus_new_share":0,"sahambonus_ratio":"100 : 30","stocksplit_created":"","stocksplit_cumdate":"2025-11-07","stocksplit_exdate":"2025-11-10","stocksplit_factor":"1.3000","stocksplit_new":"30","stocksplit_old":"100","stocksplit_paymentdate":"2025-11-25","stocksplit_recdate":"2025-11-11","event_note":""}]},"message":"Successfully retrieved stock bonus data"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"data":{"pubex":[{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"puexp_date":"2026-09-09","puexp_id":"156753","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"16:00:00","puexp_venue":"Public Expose Live 2026 yang tercantum pada website idx.co.id"},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"puexp_date":"2025-09-11","puexp_id":"87667","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"16:00:00","puexp_venue":"Public Expose Live 2025 yang tercantum pada website idx.co.id"},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"puexp_date":"2024-08-28","puexp_id":"36803","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"15:00:00","puexp_venue":"Public Expose Live 2024 Informasi Jadwal Pubex Live 2024 dan pendaftaran peserta di website www.idx.co.id"},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"puexp_date":"2023-11-29","puexp_id":"20156","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"10:00:00","puexp_venue":"Public Expose Live 2023"},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"puexp_date":"2022-09-14","puexp_id":"19470","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"10:00:00","puexp_venue":"Public Expose Live 2022"},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"puexp_date":"2021-09-08","puexp_id":"965","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"15:00:00","puexp_venue":"Public Expose Live 2021\t\t\t"}]},"message":"Successfully retrieved Public Expose data"}

View file

@ -0,0 +1 @@
{"data":{"stock_reverse":[]},"message":"Successfully retrieved reverse split data"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"data":{"rups":[{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-03-12","rups_id":"1460182","rups_time":"14:00","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA, Grand Indonesia Jl. M.H. Thamrin No. 1 Jakarta 10310","rups_eligible_date":"2026-02-09","company_name":"","company_icon_url":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2025-03-12","rups_id":"860258","rups_time":"09:30","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA, Grand Indonesia, Jl. M.H. Thamrin No. 1, Jakarta, 10310","rups_eligible_date":"2025-02-07","company_name":"","company_icon_url":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2024-03-14","rups_id":"385363","rups_time":"09:30","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA Grand Indonesia Lt 18.Jl. MH Thamrin No 1 Jakarta Selatan","rups_eligible_date":"2024-02-07","company_name":"","company_icon_url":""},{"company_id":"0","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2023-03-16","rups_id":"368621","rups_time":"09:30","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA","rups_eligible_date":"2023-02-13","company_name":"","company_icon_url":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2022-03-17","rups_id":"365637","rups_time":"09:00","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA, Grand Indonesia\nJl. M.H. Thamrin No. 1\nJakarta","rups_eligible_date":"2022-02-14","company_name":"","company_icon_url":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2021-09-23","rups_id":"356628","rups_time":"10:00","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA, Grand Indonesia\nJl. M.H. Thamrin No. 1\nJakarta","rups_eligible_date":"2021-08-27","company_name":"","company_icon_url":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2021-03-29","rups_id":"314659","rups_time":"09:00","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA, Grand Indonesia, Lt 19. Jl. M.H Thamrin No 1. Jakarta\t\t\t\t ","rups_eligible_date":"2021-02-24","company_name":"","company_icon_url":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2020-07-30","rups_id":"314133","rups_time":"10:00","rups_iqp_agenda":"","rups_iqp_id":"","rups_iqp_rec_dt":"","rups_iqp_remark":"","rups_iqp_result":"","rups_iqp_revised_date":"","rups_iqp_type":"","rups_venue":"Menara BCA Grand Indonesia, Lt. 19. Jl. M.H. Thamrin No. 1. Jakarta\t\t\t ","rups_eligible_date":"2020-07-03","company_name":"","company_icon_url":""}]},"message":"Successfully retrieved RUPS/Shareholders meeting data"}

View file

@ -0,0 +1 @@
{"data":{"stock_dividend":[]},"message":"Successfully retrieved stock dividend data"}

View file

@ -0,0 +1 @@
{"data":{"stocksplit":[{"company_id":"601","company_symbol":"PRDA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-12-17","stocksplit_exdate":"2026-12-18","stocksplit_factor":"5","stocksplit_id":"6100","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"5","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 5","stocksplit_recdate":"2026-12-21","event_note":""},{"company_id":"600","company_symbol":"PBSA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-10-13","stocksplit_exdate":"2026-10-14","stocksplit_factor":"2","stocksplit_id":"6099","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"2","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 2","stocksplit_recdate":"2026-10-15","event_note":""},{"company_id":"488","company_symbol":"MLPT","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-07-20","stocksplit_exdate":"2026-07-21","stocksplit_factor":"25","stocksplit_id":"6095","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"25","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 25","stocksplit_recdate":"2026-07-22","event_note":""},{"company_id":"1000003487","company_symbol":"RMKE","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-07-16","stocksplit_exdate":"2026-07-17","stocksplit_factor":"5","stocksplit_id":"6094","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"5","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 5","stocksplit_recdate":"2026-07-20","event_note":""},{"company_id":"354","company_symbol":"RAJA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-07-15","stocksplit_exdate":"2026-07-16","stocksplit_factor":"5","stocksplit_id":"6092","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"5","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 5","stocksplit_recdate":"2026-07-17","event_note":""},{"company_id":"1000004216","company_symbol":"CYBR","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-05-12","stocksplit_exdate":"2026-05-13","stocksplit_factor":"2","stocksplit_id":"6091","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"2","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 2","stocksplit_recdate":"2026-05-18","event_note":""},{"company_id":"1000004217","company_symbol":"CYBR-W","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-05-12","stocksplit_exdate":"2026-05-13","stocksplit_factor":"2","stocksplit_id":"6093","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"2","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 2","stocksplit_recdate":"2026-05-18","event_note":""},{"company_id":"143","company_symbol":"DSSA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2026-04-08","stocksplit_exdate":"2026-04-09","stocksplit_factor":"25","stocksplit_id":"6089","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"25","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 25","stocksplit_recdate":"2026-04-10","event_note":""}]},"message":"Successfully retrieved stock split data"}

View file

@ -0,0 +1 @@
{"data":{"stocksplit":[{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2021-10-12","stocksplit_exdate":"2021-10-13","stocksplit_factor":"5","stocksplit_id":"3091","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"5","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 5","stocksplit_recdate":"2021-10-14","event_note":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2008-01-25","stocksplit_exdate":"2008-01-28","stocksplit_factor":"2","stocksplit_id":"3","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"2","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 2","stocksplit_recdate":"2008-01-30","event_note":""},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"stocksplit_created":"","stocksplit_cumdate":"2004-06-07","stocksplit_exdate":"2004-06-08","stocksplit_factor":"2","stocksplit_id":"1061","stocksplit_iqp_id":"","stocksplit_lastupdate":"","stocksplit_lock":0,"stocksplit_new":"2","stocksplit_new_price":0,"stocksplit_new_share":0,"stocksplit_old":"1","stocksplit_ratio":"1 : 2","stocksplit_recdate":"2004-06-10","event_note":""}]},"message":"Successfully retrieved stock split data"}

View file

@ -0,0 +1 @@
{"data":{"tender":[{"company_id":"46","company_name":"Astra Otoparts Tbk.","company_symbol":"AUTO","corp_action_active":true,"tender_created":"","tender_datahash":"","tender_end":"2026-11-11","tender_id":"434","tender_paydate":"2026-11-18","tender_percentage":"4.95","tender_price":"3600","tender_shares":"238442293","tender_start":"2026-10-13","event_note":"","tender_price_formatted":"3,600"},{"company_id":"1000003921","company_name":"Hassana Boga Sejahtera Tbk.","company_symbol":"NAYZ","corp_action_active":true,"tender_created":"","tender_datahash":"","tender_end":"2026-11-03","tender_id":"433","tender_paydate":"2026-11-10","tender_percentage":"45.84","tender_price":"75","tender_shares":"1169011472","tender_start":"2026-10-05","event_note":"","tender_price_formatted":"75"},{"company_id":"1000004066","company_name":"Era Media Sejahtera Tbk.","company_symbol":"DOOH","corp_action_active":true,"tender_created":"","tender_datahash":"","tender_end":"2026-11-03","tender_id":"432","tender_paydate":"2026-11-09","tender_percentage":"33.91","tender_price":"148","tender_shares":"2624548624","tender_start":"2026-10-05","event_note":"","tender_price_formatted":"148"},{"company_id":"409","company_name":"Solusi Tunas Pratama Tbk.","company_symbol":"SUPR","corp_action_active":true,"tender_created":"","tender_datahash":"","tender_end":"2026-10-21","tender_id":"431","tender_paydate":"2026-11-02","tender_percentage":"74.39","tender_price":"45000","tender_shares":"729102","tender_start":"2026-09-24","event_note":"","tender_price_formatted":"45,000"},{"company_id":"278","company_name":"Mitrabahtera Segara Sejati Tbk.","company_symbol":"MBSS","corp_action_active":false,"tender_created":"","tender_datahash":"","tender_end":"2026-10-08","tender_id":"430","tender_paydate":"2026-10-15","tender_percentage":"17.50","tender_price":"2915","tender_shares":"306259839","tender_start":"2026-09-09","event_note":"","tender_price_formatted":"2,915"},{"company_id":"194","company_name":"Inti Bangun Sejahtera Tbk.","company_symbol":"IBST","corp_action_active":false,"tender_created":"","tender_datahash":"","tender_end":"2026-10-02","tender_id":"429","tender_paydate":"2026-10-14","tender_percentage":"0.05","tender_price":"5400","tender_shares":"650832","tender_start":"2026-09-04","event_note":"","tender_price_formatted":"5,400"}]},"message":"Successfully retrieved tender offer data"}

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,7 @@
use idx_core::analysis::bandar::TIERS; use idx_core::analysis::bandar::TIERS;
use idx_core::api::types::{Interval, Period}; use idx_core::api::types::{Interval, Period};
use idx_core::broker::types::{BrokerBoard, BrokerInvestor}; use idx_core::broker::types::{BrokerBoard, BrokerInvestor};
use idx_core::corporate::types::ActionKind;
use idx_core::flow::types::FlowSource; use idx_core::flow::types::FlowSource;
use idx_core::market::{ScreenFilter, ScreenRegion}; use idx_core::market::{ScreenFilter, ScreenRegion};
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema}; use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
@ -228,6 +229,41 @@ pub struct FlowParams {
pub source: Option<String>, pub source: Option<String>,
} }
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CorporateActionsParams {
/// IDX ticker, e.g. "BBCA".
pub symbol: String,
/// Kinds to list (default: every kind but "ipo"). Each kind is one request to Stockbit.
#[serde(default)]
#[schemars(schema_with = "action_kinds_schema")]
pub kinds: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CorporateCalendarParams {
/// First day, YYYY-MM-DD (default today).
#[serde(default)]
pub from: Option<String>,
/// Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days).
#[serde(default)]
pub to: Option<String>,
/// Kinds to list (default: every kind). Each kind is one request to Stockbit.
#[serde(default)]
#[schemars(schema_with = "action_kinds_schema")]
pub kinds: Option<Vec<String>>,
}
/// Parses `kinds`, or `default` when absent.
pub fn action_kinds(
kinds: Option<Vec<String>>,
default: &[ActionKind],
) -> Result<Vec<ActionKind>, idx_core::error::IdxError> {
match kinds {
None => Ok(default.to_vec()),
Some(kinds) => kinds.iter().map(|k| k.parse()).collect(),
}
}
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
pub struct ShareholdersParams { pub struct ShareholdersParams {
/// IDX ticker, e.g. "BBCA". /// IDX ticker, e.g. "BBCA".
@ -305,6 +341,11 @@ fn flow_source_schema(_: &mut SchemaGenerator) -> Schema {
string_enum(FlowSource::ALL.map(FlowSource::as_str).to_vec()) string_enum(FlowSource::ALL.map(FlowSource::as_str).to_vec())
} }
fn action_kinds_schema(_: &mut SchemaGenerator) -> Schema {
let kinds: Vec<&str> = ActionKind::ALL.iter().map(|k| k.as_str()).collect();
json_schema!({ "type": "array", "items": { "type": "string", "enum": kinds } })
}
fn login_method_schema(_: &mut SchemaGenerator) -> Schema { fn login_method_schema(_: &mut SchemaGenerator) -> Schema {
string_enum(LoginMethod::ALL.map(LoginMethod::as_str).to_vec()) string_enum(LoginMethod::ALL.map(LoginMethod::as_str).to_vec())
} }

View file

@ -10,6 +10,7 @@ use idx_core::analysis::fundamental::FundamentalReport;
use idx_core::api::stockbit::{self, signin}; use idx_core::api::stockbit::{self, signin};
use idx_core::broker::types::BrokerRange; use idx_core::broker::types::BrokerRange;
use idx_core::config::CoreConfig; use idx_core::config::CoreConfig;
use idx_core::corporate::types::{ActionKind, CalendarRange};
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::financial::{EarningsFilter, StatementFilter}; use idx_core::financial::{EarningsFilter, StatementFilter};
use idx_core::flow::types::FlowRange; use idx_core::flow::types::FlowRange;
@ -27,9 +28,9 @@ use tokio::sync::Semaphore;
use crate::params::{ use crate::params::{
AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams, AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
CompanySection, EntityParams, FlowParams, HistoryParams, IntradayParams, LoginMethod, CompanySection, CorporateActionsParams, CorporateCalendarParams, EntityParams, FlowParams,
LoginParams, OrderBookParams, ScreenParams, SearchParams, ShareholdersParams, SymbolsParams, HistoryParams, IntradayParams, LoginMethod, LoginParams, OrderBookParams, ScreenParams,
SyncParams, SearchParams, ShareholdersParams, SymbolsParams, SyncParams, action_kinds,
}; };
const SYNC_TOOL: &str = "ownership_sync"; const SYNC_TOOL: &str = "ownership_sync";
@ -352,6 +353,54 @@ average price, the close, `change_bps` and `non_regular_bps`."
.await .await
} }
#[tool(
description = "Corporate actions of one IDX stock from the user's own Stockbit \
session, newest first: `kind` (dividend, stock-dividend, bonus, rights, warrant, split, \
reverse-split, meeting, expose, ipo, tender), `date` (the cum date for dividends, bonuses, \
rights and splits, i.e. the last day to buy and be entitled; else the event's first day), \
`ex_date`, `record_date`, `pay_date`, and per kind `cash_per_share` (IDR, may have decimals), \
`ratio` {old, new} (1:5 split = 1 share becomes 5; 655:104 rights = 104 rights per 655 held), \
`factor` (price adjustment), `price` (rights, warrant exercise, tender or IPO price, IDR), \
`period` and `exercise` {start, end}, `shares`, `percentage_bps`, `time`, `venue`. Absent \
fields are omitted. One Stockbit request per kind (about 1 s each on a cold cache, then cached \
6 hours). AUTHERROR means there is no session: call stockbit_status, then stockbit_login."
)]
async fn get_corporate_actions(
&self,
params: Parameters<CorporateActionsParams>,
) -> ToolResult {
let CorporateActionsParams { symbol, kinds } = params.0;
self.live(move |idx| {
let kinds = action_kinds(kinds, &ActionKind::STOCK_DEFAULT)?;
idx.corporate()
.actions(&symbol, &kinds, CachePolicy::Default)
})
.await
}
#[tool(
description = "Corporate actions across IDX from the user's own Stockbit session, \
oldest first, dated from `from` to `to` (default today and the next 30 days): upcoming cum \
dates, meetings, exposes, rights, warrants, IPOs and tender offers, and the past year. Each \
item has the same fields as get_corporate_actions, with `symbol`. One Stockbit request per \
kind (about 1 s each on a cold cache, then cached 6 hours); pass `kinds` to ask for less. \
AUTHERROR means there is no session: call stockbit_status, then stockbit_login."
)]
async fn get_corporate_calendar(
&self,
params: Parameters<CorporateCalendarParams>,
) -> ToolResult {
let CorporateCalendarParams { from, to, kinds } = params.0;
self.live(move |idx| {
let corporate = idx.corporate();
let range =
CalendarRange::from_args(from.as_deref(), to.as_deref(), corporate.today())?;
let kinds = action_kinds(kinds, &ActionKind::ALL)?;
corporate.calendar(range, &kinds, CachePolicy::Default)
})
.await
}
#[tool( #[tool(
description = "Company data for one IDX ticker: profile, financial statements, \ description = "Company data for one IDX ticker: profile, financial statements, \
earnings, analyst sentiment, insights, and news. News items carry `relevance`: \ earnings, analyst sentiment, insights, and news. News items carry `relevance`: \

View file

@ -235,6 +235,91 @@
}, },
"name": "get_company" "name": "get_company"
}, },
{
"description": "Corporate actions of one IDX stock from the user's own Stockbit session, newest first: `kind` (dividend, stock-dividend, bonus, rights, warrant, split, reverse-split, meeting, expose, ipo, tender), `date` (the cum date for dividends, bonuses, rights and splits, i.e. the last day to buy and be entitled; else the event's first day), `ex_date`, `record_date`, `pay_date`, and per kind `cash_per_share` (IDR, may have decimals), `ratio` {old, new} (1:5 split = 1 share becomes 5; 655:104 rights = 104 rights per 655 held), `factor` (price adjustment), `price` (rights, warrant exercise, tender or IPO price, IDR), `period` and `exercise` {start, end}, `shares`, `percentage_bps`, `time`, `venue`. Absent fields are omitted. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours). AUTHERROR means there is no session: call stockbit_status, then stockbit_login.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"kinds": {
"default": null,
"description": "Kinds to list (default: every kind but \"ipo\"). Each kind is one request to Stockbit.",
"items": {
"enum": [
"dividend",
"stock-dividend",
"bonus",
"rights",
"warrant",
"split",
"reverse-split",
"meeting",
"expose",
"ipo",
"tender"
],
"type": "string"
},
"type": "array"
},
"symbol": {
"description": "IDX ticker, e.g. \"BBCA\".",
"type": "string"
}
},
"required": [
"symbol"
],
"type": "object"
},
"name": "get_corporate_actions"
},
{
"description": "Corporate actions across IDX from the user's own Stockbit session, oldest first, dated from `from` to `to` (default today and the next 30 days): upcoming cum dates, meetings, exposes, rights, warrants, IPOs and tender offers, and the past year. Each item has the same fields as get_corporate_actions, with `symbol`. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours); pass `kinds` to ask for less. AUTHERROR means there is no session: call stockbit_status, then stockbit_login.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"from": {
"default": null,
"description": "First day, YYYY-MM-DD (default today).",
"type": [
"string",
"null"
]
},
"kinds": {
"default": null,
"description": "Kinds to list (default: every kind). Each kind is one request to Stockbit.",
"items": {
"enum": [
"dividend",
"stock-dividend",
"bonus",
"rights",
"warrant",
"split",
"reverse-split",
"meeting",
"expose",
"ipo",
"tender"
],
"type": "string"
},
"type": "array"
},
"to": {
"default": null,
"description": "Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days).",
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"name": "get_corporate_calendar"
},
{ {
"description": "Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` \"ipot\" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With \"idx\": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`.", "description": "Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` \"ipot\" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With \"idx\": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`.",
"inputSchema": { "inputSchema": {

View file

@ -9,12 +9,14 @@ use idx_core::config::CoreConfig;
use idx_core::investor::{ImportSource, Investor}; use idx_core::investor::{ImportSource, Investor};
use serde_json::{Value, json}; use serde_json::{Value, json};
const TOOLS: [&str; 18] = [ const TOOLS: [&str; 20] = [
"analyze_bandar", "analyze_bandar",
"analyze_stock", "analyze_stock",
"compare_stocks", "compare_stocks",
"get_broker_summary", "get_broker_summary",
"get_company", "get_company",
"get_corporate_actions",
"get_corporate_calendar",
"get_foreign_flow", "get_foreign_flow",
"get_intraday", "get_intraday",
"get_investor_holdings", "get_investor_holdings",
@ -502,6 +504,53 @@ fn the_order_book_needs_a_session_and_status_never_shows_tokens() {
} }
} }
#[test]
fn corporate_actions_need_a_session_and_keep_kinds_apart() {
let mut client = Client::start(&[], false);
let (is_error, body) = client.call("get_corporate_actions", json!({"symbol": "BBCA"}));
assert!(is_error, "{body}");
assert_eq!(body["code"], "AUTHERROR");
let mut client = Client::start_with_env(
&[],
false,
&[TEST_SESSION, ("IDX_MOCK_TODAY", "2026-11-03")],
);
let actions = client.ok(
"get_corporate_actions",
json!({"symbol": "BBCA", "kinds": ["dividend", "split"]}),
);
assert_fetched(&actions);
assert_eq!(actions["source"], "stockbit");
let rows = actions["data"].as_array().unwrap();
assert_eq!(rows.len(), 40, "37 dividends and 3 splits");
assert_eq!(rows[0]["kind"], "dividend");
assert_eq!(rows[0]["date"], "2026-08-28");
assert_eq!(rows[0]["cash_per_share"], 25.0);
assert!(rows[0].get("ratio").is_none(), "absent fields are omitted");
let split = rows.iter().find(|r| r["kind"] == "split").unwrap();
assert_eq!(split["ratio"], json!({"old": 1, "new": 5}));
let calendar = client.ok(
"get_corporate_calendar",
json!({"to": "2026-11-12", "kinds": ["rights", "dividend"]}),
);
let rows = calendar["data"].as_array().unwrap();
assert_eq!(rows[0]["symbol"], "MGLV.JK");
assert_eq!(rows[0]["price"], 8880);
assert!(
rows.windows(2)
.all(|w| w[0]["date"].as_str() <= w[1]["date"].as_str())
);
let (is_error, body) = client.call(
"get_corporate_actions",
json!({"symbol": "BBCA", "kinds": ["rups"]}),
);
assert!(is_error, "{body}");
assert_eq!(body["code"], "INVALIDINPUT");
}
#[test] #[test]
fn errors_carry_cli_codes() { fn errors_carry_cli_codes() {
let mut client = Client::start(&[], false); let mut client = Client::start(&[], false);

View file

@ -283,6 +283,19 @@ WIB as the fallback.
provider uses Stockbit's daily windows (`5d` to `1y`, trimmed from `1m`, provider uses Stockbit's daily windows (`5d` to `1y`, trimmed from `1m`,
`3m` or `1y`), Yahoo otherwise. Without one, quotes use the public page and `3m` or `1y`), Yahoo otherwise. Without one, quotes use the public page and
the order book is `AUTHERROR`. the order book is `AUTHERROR`.
- Corporate actions (`corporate/`, `api/stockbit/corpaction.rs`):
`/corpaction/{type}` per kind (`dividend`, `stock_dividend`, `bonus`,
`rightissue`, `warrant`, `stocksplit`, `reversesplit`, `rups`, `pubex`,
`ipo`, `tenderoffer`), with `?symbol=` for one stock's history or without
for the market (about a year back and what is announced ahead). Each type
has its own field prefix and the list key is not always the path's, so
entitlements are read by field suffix (`_cumdate`, `_exdate`, ...). One
request per kind, cached 6 hours under `stockbit-corpaction`
(`<kind>:<SYMBOL.JK>` or `<kind>:market`); `stocks actions` /
`get_corporate_actions` sort newest first, `stocks calendar` /
`get_corporate_calendar` filter by `CorporateAction::date` (the cum date
for entitlements) and sort oldest first. Endpoint notes and the wider
design: `docs/STOCKBIT.md`.
### Capability Gating ### Capability Gating
``` ```
@ -296,6 +309,7 @@ Factory functions:
trading_summary_provider() → Box<dyn TradingSummaryProvider> trading_summary_provider() → Box<dyn TradingSummaryProvider>
intraday_provider() → Box<dyn IntradayProvider> intraday_provider() → Box<dyn IntradayProvider>
orderbook_provider() → Box<dyn OrderBookProvider> orderbook_provider() → Box<dyn OrderBookProvider>
corporate_action_provider() → Box<dyn CorporateActionProvider>
``` ```
`ProviderKind::Stockbit` serves quotes (and, with a session, daily history): `ProviderKind::Stockbit` serves quotes (and, with a session, daily history):

View file

@ -44,7 +44,7 @@ All on `https://exodus.stockbit.com`, bearer access token, HTTP 200 on
| `/company-price-feed/market-time/session` | market state | 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=1m\|3m\|1y` | daily candles | shipped (#74) |
| `/charts/{SYM}/daily?timeframe=1w` | hourly candles, 5 sessions | P2 | | `/charts/{SYM}/daily?timeframe=1w` | hourly candles, 5 sessions | P2 |
| `/corpaction/{type}[?symbol=SYM]` | corporate actions and calendar | **P1** | | `/corpaction/{type}[?symbol=SYM]` | corporate actions and calendar | shipped: `stocks actions`, `stocks calendar` |
| `/insider/company/majorholder` | insider / major-holder movements | **P1** | | `/insider/company/majorholder` | insider / major-holder movements | **P1** |
| `/insider/shareholding/composition/companies/{SYM}` | holder composition by period | P2 | | `/insider/shareholding/composition/companies/{SYM}` | holder composition by period | P2 |
| `/marketdetectors/{SYM}` | broker summary, Stockbit's bandar detector | P2 | | `/marketdetectors/{SYM}` | broker summary, Stockbit's bandar detector | P2 |
@ -95,9 +95,12 @@ 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 hourly `Interval`, across Yahoo, MSN, output, goldens and the MCP schema; that
is its own change. is its own change.
## P1: corporate actions ## P1: corporate actions (shipped)
idx has none today (only the `high-dividend` screen and Yahoo's dividend 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 yield). They explain price gaps in history (splits, bonuses, rights) and give
a calendar of cum dates. a calendar of cum dates.
@ -151,10 +154,10 @@ lists are not errors. A row whose dates are all placeholders is dropped.
### Surface ### Surface
- `idx stocks actions SYM [--kind dividend,rights,...] [--since DATE]`: one - `idx stocks actions SYM [--kind dividend,rights,...]`: one stock's
stock's history, newest first. history, newest first; every kind but `ipo` by default.
- `idx calendar [--kind ...] [--from DATE] [--to DATE]`: market-wide, default - `idx stocks calendar [--kind ...] [--from DATE] [--to DATE]`: market-wide,
today to +30 days, sorted by the next relevant date. default today to +30 days, oldest first by `date`.
- MCP: `get_corporate_actions {symbol, kinds?, since?}`, - MCP: `get_corporate_actions {symbol, kinds?, since?}`,
`get_corporate_calendar {kinds?, from?, to?}`. `get_corporate_calendar {kinds?, from?, to?}`.
- Later: `stocks history` notes ex-dates of splits, bonuses and rights in the - Later: `stocks history` notes ex-dates of splits, bonuses and rights in the
@ -231,7 +234,8 @@ Yahoo prices, forward 5/20/60-session returns against IHSG).
1. Close #74: the open-state name in trading hours, fixtures for the observed 1. Close #74: the open-state name in trading hours, fixtures for the observed
states (done 2026-11-03). states (done 2026-11-03).
2. Corporate actions (parsers, `stocks actions`, `calendar`, MCP, goldens). 2. Corporate actions (parsers, `stocks actions`, `stocks calendar`, MCP,
goldens) (done).
3. Shareholder movements, rows first, then `--summary`. 3. Shareholder movements, rows first, then `--summary`.
4. Event study for the movement score; P2 by demand. 4. Event study for the movement score; P2 by demand.

View file

@ -68,6 +68,8 @@ Commands:
quote Get real-time stock quotes quote Get real-time stock quotes
intraday Today's session price by the minute (Stockbit) intraday Today's session price by the minute (Stockbit)
orderbook Order book depth and foreign flow (Stockbit login) orderbook Order book depth and foreign flow (Stockbit login)
actions Dividends, rights, splits and other corporate actions of a stock (Stockbit login)
calendar Upcoming and recent corporate actions across the market (Stockbit login)
history Get historical OHLC data history Get historical OHLC data
technical Run technical analysis on a stock technical Run technical analysis on a stock
growth Run growth analysis on a stock growth Run growth analysis on a stock
@ -223,6 +225,132 @@ Examples:
idx -o json stocks orderbook TLKM | jq '.offers[0]' idx -o json stocks orderbook TLKM | jq '.offers[0]'
``` ```
### `idx stocks actions`
```text
One IDX stock's corporate actions from your own Stockbit session (`idx stockbit login`), newest first: cash and stock dividends, bonus shares, rights issues, warrants, splits and reverse splits, shareholders' meetings, public exposes and tender offers (`--kind ipo` adds its IPO). Stockbit serves one kind per request, 1 s apart, so the first call takes about a second per kind; answers are cached for 6 hours.
Usage: idx stocks actions [OPTIONS] <SYMBOL>
Arguments:
<SYMBOL>
Single ticker symbol (e.g. BBCA)
Options:
--kind <KINDS>...
Kinds to list (default: all but ipo)
Possible values:
- dividend: Cash dividend
- stock-dividend
- bonus: Bonus shares
- rights: Rights issue (HMETD)
- warrant
- split: Stock split
- reverse-split
- meeting: Shareholders' meeting (RUPS)
- expose: Public expose
- ipo
- tender
-o, --output <OUTPUT>
Output format [default: auto = table on a terminal, json when piped]
[possible values: auto, table, json, jsonl]
--no-color
Disable colored output
-q, --quiet
Suppress non-essential output
-v, --verbose...
Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline
Run without network requests
--no-cache
Bypass local cache
-h, --help
Print help (see a summary with '-h')
Examples:
idx stocks actions BBCA
idx stocks actions TLKM --kind dividend
idx -o json stocks actions BBRI --kind dividend,split | jq '.[0]'
```
### `idx stocks calendar`
```text
Corporate actions across IDX from your own Stockbit session (`idx stockbit login`), oldest first, dated within --from and --to (default: today and the next 30 days). An action's date is its cum date for dividends, bonuses, rights and splits, else the event's first day. Stockbit lists about a year back and what is announced ahead. One request per kind, 1 s apart; answers are cached for 6 hours.
Usage: idx stocks calendar [OPTIONS]
Options:
--from <FROM>
First day, YYYY-MM-DD. Default: today
-o, --output <OUTPUT>
Output format [default: auto = table on a terminal, json when piped]
[possible values: auto, table, json, jsonl]
--no-color
Disable colored output
--to <TO>
Last day, YYYY-MM-DD. Default: 30 days after --from (at most 366)
--kind <KINDS>...
Kinds to list (default: all)
Possible values:
- dividend: Cash dividend
- stock-dividend
- bonus: Bonus shares
- rights: Rights issue (HMETD)
- warrant
- split: Stock split
- reverse-split
- meeting: Shareholders' meeting (RUPS)
- expose: Public expose
- ipo
- tender
-q, --quiet
Suppress non-essential output
-v, --verbose...
Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline
Run without network requests
--no-cache
Bypass local cache
-h, --help
Print help (see a summary with '-h')
Examples:
idx stocks calendar
idx stocks calendar --kind dividend,rights
idx stocks calendar --from 2026-12-01 --to 2026-12-31 --kind meeting
```
### `idx stocks history` ### `idx stocks history`
```text ```text

View file

@ -75,6 +75,8 @@ idx stockbit login # paste the credentialStorage cookie
idx stockbit status # logged in? when do the tokens expire? idx stockbit status # logged in? when do the tokens expire?
idx stocks orderbook BBCA # depth, queued orders, foreign flow idx stocks orderbook BBCA # depth, queued orders, foreign flow
idx stocks orderbook BBRI --depth 5 idx stocks orderbook BBRI --depth 5
idx stocks actions BBCA # dividends, splits, meetings, ...
idx stocks calendar --kind dividend # cum dates in the next 30 days
idx --provider stockbit stocks quote BBCA # live, not delayed idx --provider stockbit stocks quote BBCA # live, not delayed
idx --provider stockbit stocks history BBCA --period 1y # Stockbit's daily candles idx --provider stockbit stocks history BBCA --period 1y # Stockbit's daily candles
idx stockbit logout idx stockbit logout
@ -198,6 +200,7 @@ What the login adds:
| `stocks orderbook` | `AUTHERROR` | every level with lots and queued orders, OHLC, value, trades; foreign buy/sell/net once Stockbit marks them current | | `stocks orderbook` | `AUTHERROR` | every level with lots and queued orders, OHLC, value, trades; foreign buy/sell/net once Stockbit marks them current |
| `stocks history` under `--provider stockbit` | Yahoo | Stockbit's daily candles for `5d` to `1y`; Yahoo for other windows | | `stocks history` under `--provider stockbit` | Yahoo | Stockbit's daily candles for `5d` to `1y`; Yahoo for other windows |
| `stocks intraday` | public page | public page | | `stocks intraday` | public page | public page |
| `stocks actions`, `stocks calendar` | `AUTHERROR` | corporate actions: one stock's history, or the market's calendar |
The order book is what Stockbit's own app shows. Outside trading it is the The order book is what Stockbit's own app shows. Outside trading it is the
book as the market left it, and the output says so. Foreign buy, sell and net book as the market left it, and the output says so. Foreign buy, sell and net
@ -218,7 +221,7 @@ Stockbit. Use this for looking at a stock now and then, not for crawling the
market or republishing prices. With your login, automated use is tied to your market or republishing prices. With your login, automated use is tied to your
account: keep it human-paced. account: keep it human-paced.
Over MCP, the same data comes from `get_intraday`, `get_orderbook` and Over MCP, the same data comes from `get_intraday`, `get_orderbook`,
`stockbit_status`, and from `get_quotes` and `get_price_history` when the `get_corporate_actions`, `get_corporate_calendar` and `stockbit_status`, and from `get_quotes` and `get_price_history` when the
server runs with `IDX_PROVIDER=stockbit`. The server never logs in; run server runs with `IDX_PROVIDER=stockbit`. The server never logs in; run
`idx stockbit login` in a terminal. `idx stockbit login` in a terminal.

View file

@ -46,6 +46,28 @@ value and trades, and foreign buy, sell and net once Stockbit marks them
current (not during trading, when they lag a day). Without one it answers current (not during trading, when they lag a day). Without one it answers
`AUTHERROR`. See [Stockbit](/cli/stockbit/#with-your-stockbit-login). `AUTHERROR`. See [Stockbit](/cli/stockbit/#with-your-stockbit-login).
## Corporate actions (Stockbit login)
```bash
idx stocks actions BBCA # newest first
idx stocks actions TLKM --kind dividend
idx stocks calendar # today and the next 30 days
idx stocks calendar --kind dividend,rights --to 2026-12-31
```
`actions` lists one stock's cash and stock dividends, bonus shares, rights
issues, warrants, splits and reverse splits, shareholders' meetings, public
exposes and tender offers (`--kind ipo` adds its IPO). `calendar` lists them
across the market between `--from` and `--to` (at most 366 days), including
what is announced ahead. An action's date is its cum date for dividends,
bonuses, rights and splits: buy by then to be entitled. For the others it is
the event's first day. Dividends per share keep their decimals (`Rp 3.8`).
Both need your Stockbit session and answer `AUTHERROR` without one. Stockbit
serves one kind per request, 1 s apart, so a first call with every kind takes
about ten seconds; answers are then cached for 6 hours. `--kind` asks for
less.
## Company data ## Company data
| Command | What it shows | | Command | What it shows |

View file

@ -5,9 +5,9 @@ description: idx-mcp serves idx's stock and ownership data to AI agents over MCP
`idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) `idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
server over stdio. It gives an agent the same data as the `idx` CLI, as server over stdio. It gives an agent the same data as the `idx` CLI, as
[17 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's [20 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's
session by the minute, the order book (with a Stockbit login), price session by the minute, the order book and corporate actions (with a Stockbit
history, screening, company data, analysis, comparison, broker summaries, login), price history, screening, company data, analysis, comparison, broker summaries,
bandarmology, foreign flow, and the KSEI ownership queries. bandarmology, foreign flow, and the KSEI ownership queries.
It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps), It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps),

View file

@ -63,6 +63,25 @@ Company data for one IDX ticker: profile, financial statements, earnings, analys
| `sections` | array of `profile` / `financials` / `earnings` / `sentiment` / `insights` / `news` | no | Sections to fetch (default: all). | | `sections` | array of `profile` / `financials` / `earnings` / `sentiment` / `insights` / `news` | no | Sections to fetch (default: all). |
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". | | `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
## `get_corporate_actions`
Corporate actions of one IDX stock from the user's own Stockbit session, newest first: `kind` (dividend, stock-dividend, bonus, rights, warrant, split, reverse-split, meeting, expose, ipo, tender), `date` (the cum date for dividends, bonuses, rights and splits, i.e. the last day to buy and be entitled; else the event's first day), `ex_date`, `record_date`, `pay_date`, and per kind `cash_per_share` (IDR, may have decimals), `ratio` {old, new} (1:5 split = 1 share becomes 5; 655:104 rights = 104 rights per 655 held), `factor` (price adjustment), `price` (rights, warrant exercise, tender or IPO price, IDR), `period` and `exercise` {start, end}, `shares`, `percentage_bps`, `time`, `venue`. Absent fields are omitted. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours). AUTHERROR means there is no session: call stockbit_status, then stockbit_login.
| Input | Type | Required | Description |
| --- | --- | --- | --- |
| `kinds` | array of `dividend` / `stock-dividend` / `bonus` / `rights` / `warrant` / `split` / `reverse-split` / `meeting` / `expose` / `ipo` / `tender` | no | Kinds to list (default: every kind but "ipo"). Each kind is one request to Stockbit. |
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
## `get_corporate_calendar`
Corporate actions across IDX from the user's own Stockbit session, oldest first, dated from `from` to `to` (default today and the next 30 days): upcoming cum dates, meetings, exposes, rights, warrants, IPOs and tender offers, and the past year. Each item has the same fields as get_corporate_actions, with `symbol`. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours); pass `kinds` to ask for less. AUTHERROR means there is no session: call stockbit_status, then stockbit_login.
| Input | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | no | First day, YYYY-MM-DD (default today). |
| `kinds` | array of `dividend` / `stock-dividend` / `bonus` / `rights` / `warrant` / `split` / `reverse-split` / `meeting` / `expose` / `ipo` / `tender` | no | Kinds to list (default: every kind). Each kind is one request to Stockbit. |
| `to` | string | no | Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days). |
## `get_foreign_flow` ## `get_foreign_flow`
Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` "ipot" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With "idx": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`. Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` "ipot" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With "idx": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`.