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

@ -4,15 +4,17 @@ use clap::{Args, Subcommand};
use crate::config::{HistoryProviderKind, IdxConfig};
use crate::output::{
render_bandar, render_broker_summary, render_compare, render_earnings, render_financials,
render_foreign_flow, render_fundamental, render_growth, render_history, render_insights,
render_intraday, render_news, render_orderbook, render_profile, render_quotes, render_risk,
render_screener, render_sentiment, render_technical, render_valuation,
render_bandar, render_broker_summary, render_compare, render_corporate_actions,
render_corporate_calendar, render_earnings, render_financials, render_foreign_flow,
render_fundamental, render_growth, render_history, render_insights, render_intraday,
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::bandar::DEFAULT_TOP;
use idx_core::api::types::{Interval, Period};
use idx_core::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange};
use idx_core::corporate::types::{ActionKind, CalendarRange};
use idx_core::error::IdxError;
use idx_core::financial::{EarningsFilter, StatementFilter, StatementKind};
use idx_core::flow::types::{FlowRange, FlowSource};
@ -163,6 +165,42 @@ the output says so.",
#[arg(long, default_value_t = 10)]
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(
about = "Get historical OHLC data",
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);
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 {
symbol,
period,

View file

@ -19,6 +19,7 @@ use idx_core::api::types::{
};
use idx_core::broker::types::BrokerSummary;
use idx_core::config::ProviderKind;
use idx_core::corporate::types::{CalendarRange, CorporateAction};
use idx_core::error::IdxError;
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(
summary: &BrokerSummary,
format: &OutputFormat,

View file

@ -15,6 +15,7 @@ use idx_core::api::types::{
Intraday, NewsItem, Ohlc, OrderBook, Quote, SentimentData,
};
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::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)]
mod tests {
use super::{

View file

@ -285,6 +285,30 @@ env = { IDX_STOCKBIT_REFRESH_TOKEN = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0OD
name = "stockbit-status-logged-out"
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) --------------------
[[case]]
@ -392,6 +416,15 @@ args = ["--provider", "stockbit", "stocks", "fundamental", "BBCA"]
name = "error-orderbook-no-session"
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]]
name = "error-stockbit-history-window"
args = ["stocks", "history", "BBCA", "--period", "5y", "--history-provider", "stockbit"]

View file

@ -189,12 +189,18 @@ _idx() {
idx__help__stockbit,status)
cmd="idx__help__stockbit__status"
;;
idx__help__stocks,actions)
cmd="idx__help__stocks__actions"
;;
idx__help__stocks,bandar)
cmd="idx__help__stocks__bandar"
;;
idx__help__stocks,broker)
cmd="idx__help__stocks__broker"
;;
idx__help__stocks,calendar)
cmd="idx__help__stocks__calendar"
;;
idx__help__stocks,compare)
cmd="idx__help__stocks__compare"
;;
@ -390,12 +396,18 @@ _idx() {
idx__stockbit__help,status)
cmd="idx__stockbit__help__status"
;;
idx__stocks,actions)
cmd="idx__stocks__actions"
;;
idx__stocks,bandar)
cmd="idx__stocks__bandar"
;;
idx__stocks,broker)
cmd="idx__stocks__broker"
;;
idx__stocks,calendar)
cmd="idx__stocks__calendar"
;;
idx__stocks,compare)
cmd="idx__stocks__compare"
;;
@ -453,12 +465,18 @@ _idx() {
idx__stocks,valuation)
cmd="idx__stocks__valuation"
;;
idx__stocks__help,actions)
cmd="idx__stocks__help__actions"
;;
idx__stocks__help,bandar)
cmd="idx__stocks__help__bandar"
;;
idx__stocks__help,broker)
cmd="idx__stocks__help__broker"
;;
idx__stocks__help,calendar)
cmd="idx__stocks__help__calendar"
;;
idx__stocks__help,compare)
cmd="idx__stocks__help__compare"
;;
@ -1371,7 +1389,7 @@ _idx() {
return 0
;;
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
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@ -1384,6 +1402,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -1412,6 +1444,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -2689,7 +2735,7 @@ _idx() {
return 0
;;
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
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@ -2714,6 +2760,36 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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)
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
@ -2810,6 +2886,44 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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)
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@ -2987,7 +3101,7 @@ _idx() {
return 0
;;
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
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@ -3000,6 +3114,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -3028,6 +3156,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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)
opts=""
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
intraday Today's session price by the minute (Stockbit)
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
technical Run technical 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
//! `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::{
BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, IntradayProvider,
OrderBookProvider, QuoteProvider, TradingSummaryProvider, idx, ipot, msn, stockbit, yahoo,
BrokerSummaryProvider, CorporateActionProvider, FundamentalsProvider, HistoryProvider,
IntradayProvider, OrderBookProvider, QuoteProvider, TradingSummaryProvider, idx, ipot, msn,
stockbit, yahoo,
};
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
use crate::config::ProviderKind;
use crate::corporate::types::{ActionKind, CorporateAction};
use crate::error::IdxError;
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 {
fn history(
&self,

View file

@ -78,6 +78,17 @@ pub trait OrderBookProvider {
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 {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
}
@ -345,6 +356,14 @@ pub fn orderbook_provider() -> Box<dyn OrderBookProvider> {
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.
pub(crate) fn today_wib() -> chrono::NaiveDate {
#[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 super::auth::SessionStore;
use super::corpaction;
use super::parse::number;
use super::{USER_AGENT, pace, stockbit_code};
use crate::api::types::{BookLevel, Interval, Ohlc, OrderBook, Period, Quote};
use crate::corporate::types::{ActionKind, CorporateAction};
use crate::error::IdxError;
const BASE_URL: &str = "https://exodus.stockbit.com";
@ -120,6 +122,24 @@ impl Exodus {
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.
/// Unknown on any failure, which hides bid/offer.
fn market_status(&self) -> Option<String> {

View file

@ -17,6 +17,7 @@
pub mod auth;
pub mod browser;
pub mod corpaction;
pub mod exodus;
pub mod loopback;
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::{
FundamentalsProvider, HistoryProvider, IntradayProvider, OrderBookProvider, QuoteProvider,
CorporateActionProvider, FundamentalsProvider, HistoryProvider, IntradayProvider,
OrderBookProvider, QuoteProvider,
};
use crate::corporate::types::{ActionKind, CorporateAction};
use crate::error::IdxError;
use auth::SessionStore;
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 {
fn history(
&self,

View file

@ -94,6 +94,10 @@ impl Idx {
crate::flow::Flow { idx: self }
}
pub fn corporate(&self) -> crate::corporate::Corporate<'_> {
crate::corporate::Corporate { idx: self }
}
pub fn financial(&self) -> crate::financial::Financial<'_> {
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;
mod client;
pub mod config;
pub mod corporate;
pub mod curl_impersonate;
pub mod error;
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::api::types::{Interval, Period};
use idx_core::broker::types::{BrokerBoard, BrokerInvestor};
use idx_core::corporate::types::ActionKind;
use idx_core::flow::types::FlowSource;
use idx_core::market::{ScreenFilter, ScreenRegion};
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
@ -228,6 +229,41 @@ pub struct FlowParams {
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)]
pub struct ShareholdersParams {
/// 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())
}
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 {
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::broker::types::BrokerRange;
use idx_core::config::CoreConfig;
use idx_core::corporate::types::{ActionKind, CalendarRange};
use idx_core::error::IdxError;
use idx_core::financial::{EarningsFilter, StatementFilter};
use idx_core::flow::types::FlowRange;
@ -27,9 +28,9 @@ use tokio::sync::Semaphore;
use crate::params::{
AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
CompanySection, EntityParams, FlowParams, HistoryParams, IntradayParams, LoginMethod,
LoginParams, OrderBookParams, ScreenParams, SearchParams, ShareholdersParams, SymbolsParams,
SyncParams,
CompanySection, CorporateActionsParams, CorporateCalendarParams, EntityParams, FlowParams,
HistoryParams, IntradayParams, LoginMethod, LoginParams, OrderBookParams, ScreenParams,
SearchParams, ShareholdersParams, SymbolsParams, SyncParams, action_kinds,
};
const SYNC_TOOL: &str = "ownership_sync";
@ -352,6 +353,54 @@ average price, the close, `change_bps` and `non_regular_bps`."
.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(
description = "Company data for one IDX ticker: profile, financial statements, \
earnings, analyst sentiment, insights, and news. News items carry `relevance`: \

View file

@ -235,6 +235,91 @@
},
"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`.",
"inputSchema": {

View file

@ -9,12 +9,14 @@ use idx_core::config::CoreConfig;
use idx_core::investor::{ImportSource, Investor};
use serde_json::{Value, json};
const TOOLS: [&str; 18] = [
const TOOLS: [&str; 20] = [
"analyze_bandar",
"analyze_stock",
"compare_stocks",
"get_broker_summary",
"get_company",
"get_corporate_actions",
"get_corporate_calendar",
"get_foreign_flow",
"get_intraday",
"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]
fn errors_carry_cli_codes() {
let mut client = Client::start(&[], false);