mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
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:
parent
8a1b29043c
commit
43ff4aa433
55 changed files with 2026 additions and 34 deletions
|
|
@ -97,7 +97,8 @@ idx ownership changes --from 2026-07-31 --to 2026-08-31
|
|||
minute) and `--provider stockbit` quotes with the best bid/offer while the
|
||||
market is open, delayed at least 10 minutes. With your own account
|
||||
(`idx stockbit login`): `stocks orderbook` depth, live
|
||||
quotes and Stockbit daily history. Personal use under Stockbit's terms; see
|
||||
quotes, Stockbit daily history, and corporate actions (`stocks actions`,
|
||||
`stocks calendar`: dividends, rights, splits, meetings, IPOs and more). Personal use under Stockbit's terms; see
|
||||
the [Stockbit page](https://idx.rasyidanaf.com/cli/stockbit/)
|
||||
- Local file cache with TTL, and `--offline` mode with stale-cache fallback
|
||||
|
||||
|
|
@ -165,8 +166,9 @@ change it to `auto` with `idx config set general.output auto`.
|
|||
|
||||
## MCP server
|
||||
|
||||
`idx-mcp` serves the same data to MCP clients over stdio, as 17 tools:
|
||||
`get_quotes`, `get_intraday`, `get_orderbook`, `stockbit_status`, `get_price_history`, `screen_stocks`, `get_company`,
|
||||
`idx-mcp` serves the same data to MCP clients over stdio, as 20 tools:
|
||||
`get_quotes`, `get_intraday`, `get_orderbook`, `get_corporate_actions`,
|
||||
`get_corporate_calendar`, `stockbit_status`, `stockbit_login`, `get_price_history`, `screen_stocks`, `get_company`,
|
||||
`analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`,
|
||||
`get_foreign_flow`, `get_shareholders`, `get_investor_holdings`,
|
||||
`search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ history lives in git.
|
|||
- Multi-period financial statements (today `financials` is single-period)
|
||||
- Order book depth without a login: no free source exists (IDX sells delayed
|
||||
depth to vendors); `stocks orderbook` needs the user's Stockbit login.
|
||||
- Corporate actions (`stocks actions`, `calendar`) and shareholder movements
|
||||
(`ownership movements`) from the Stockbit session; design and endpoints in
|
||||
`docs/STOCKBIT.md`.
|
||||
- Shareholder movements (`ownership movements`) from the Stockbit session;
|
||||
design and endpoints in `docs/STOCKBIT.md`. Corporate-action follow-ups:
|
||||
ex-dates of splits, bonuses and rights noted in `stocks history`, and the
|
||||
`stock_dividend` / `reversesplit` row shapes once Stockbit lists one.
|
||||
- Stockbit login follow-ups: hourly OHLCV from the session's `1w` chart (the
|
||||
`1d` chart is per-minute closes only); confirming session 1 is
|
||||
`STATE_NAME_SESSION_1` (session 2, `BREAK` and `MARKET_CLOSED` were
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
exit: 4
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: auth error: no Stockbit session; run `idx stockbit login` (or set IDX_STOCKBIT_REFRESH_TOKEN)
|
||||
|
|
@ -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)"}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: invalid input: to (2026-11-01) is before from (2026-11-10)
|
||||
|
|
@ -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
|
|
@ -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
|
||||
4
crates/idx-cli/tests/golden/stockbit-actions.json.golden
Normal file
4
crates/idx-cli/tests/golden/stockbit-actions.json.golden
Normal 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
|
||||
43
crates/idx-cli/tests/golden/stockbit-actions.table.golden
Normal file
43
crates/idx-cli/tests/golden/stockbit-actions.table.golden
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
37
crates/idx-cli/tests/golden/stockbit-calendar.table.golden
Normal file
37
crates/idx-cli/tests/golden/stockbit-calendar.table.golden
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
405
crates/idx-core/src/api/stockbit/corpaction.rs
Normal file
405
crates/idx-core/src/api/stockbit/corpaction.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
108
crates/idx-core/src/corporate/mod.rs
Normal file
108
crates/idx-core/src/corporate/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
292
crates/idx-core/src/corporate/types.rs
Normal file
292
crates/idx-core/src/corporate/types.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_bonus.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_bonus.json
vendored
Normal 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"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend_bbca.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend_bbca.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_ipo.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_ipo.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex_bbca.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex_bbca.json
vendored
Normal 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"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_reversesplit.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_reversesplit.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"data":{"stock_reverse":[]},"message":"Successfully retrieved reverse split data"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rightissue.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rightissue.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups_bbca.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups_bbca.json
vendored
Normal 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"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stock_dividend.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stock_dividend.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"data":{"stock_dividend":[]},"message":"Successfully retrieved stock dividend data"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit.json
vendored
Normal 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"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit_bbca.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit_bbca.json
vendored
Normal 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"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_tenderoffer.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_tenderoffer.json
vendored
Normal 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"}
|
||||
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_warrant.json
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_warrant.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`: \
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -283,6 +283,19 @@ WIB as the fallback.
|
|||
provider uses Stockbit's daily windows (`5d` to `1y`, trimmed from `1m`,
|
||||
`3m` or `1y`), Yahoo otherwise. Without one, quotes use the public page and
|
||||
the order book is `AUTHERROR`.
|
||||
- Corporate actions (`corporate/`, `api/stockbit/corpaction.rs`):
|
||||
`/corpaction/{type}` per kind (`dividend`, `stock_dividend`, `bonus`,
|
||||
`rightissue`, `warrant`, `stocksplit`, `reversesplit`, `rups`, `pubex`,
|
||||
`ipo`, `tenderoffer`), with `?symbol=` for one stock's history or without
|
||||
for the market (about a year back and what is announced ahead). Each type
|
||||
has its own field prefix and the list key is not always the path's, so
|
||||
entitlements are read by field suffix (`_cumdate`, `_exdate`, ...). One
|
||||
request per kind, cached 6 hours under `stockbit-corpaction`
|
||||
(`<kind>:<SYMBOL.JK>` or `<kind>:market`); `stocks actions` /
|
||||
`get_corporate_actions` sort newest first, `stocks calendar` /
|
||||
`get_corporate_calendar` filter by `CorporateAction::date` (the cum date
|
||||
for entitlements) and sort oldest first. Endpoint notes and the wider
|
||||
design: `docs/STOCKBIT.md`.
|
||||
|
||||
### Capability Gating
|
||||
```
|
||||
|
|
@ -296,6 +309,7 @@ Factory functions:
|
|||
trading_summary_provider() → Box<dyn TradingSummaryProvider>
|
||||
intraday_provider() → Box<dyn IntradayProvider>
|
||||
orderbook_provider() → Box<dyn OrderBookProvider>
|
||||
corporate_action_provider() → Box<dyn CorporateActionProvider>
|
||||
```
|
||||
|
||||
`ProviderKind::Stockbit` serves quotes (and, with a session, daily history):
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ All on `https://exodus.stockbit.com`, bearer access token, HTTP 200 on
|
|||
| `/company-price-feed/market-time/session` | market state | shipped (#74) |
|
||||
| `/charts/{SYM}/daily?timeframe=1m\|3m\|1y` | daily candles | shipped (#74) |
|
||||
| `/charts/{SYM}/daily?timeframe=1w` | hourly candles, 5 sessions | P2 |
|
||||
| `/corpaction/{type}[?symbol=SYM]` | corporate actions and calendar | **P1** |
|
||||
| `/corpaction/{type}[?symbol=SYM]` | corporate actions and calendar | shipped: `stocks actions`, `stocks calendar` |
|
||||
| `/insider/company/majorholder` | insider / major-holder movements | **P1** |
|
||||
| `/insider/shareholding/composition/companies/{SYM}` | holder composition by period | P2 |
|
||||
| `/marketdetectors/{SYM}` | broker summary, Stockbit's bandar detector | P2 |
|
||||
|
|
@ -95,9 +95,12 @@ Hourly history needs a datetime bar (`Ohlc.date` is a `NaiveDate`) and an
|
|||
hourly `Interval`, across Yahoo, MSN, output, goldens and the MCP schema; that
|
||||
is its own change.
|
||||
|
||||
## P1: corporate actions
|
||||
## P1: corporate actions (shipped)
|
||||
|
||||
idx has none today (only the `high-dividend` screen and Yahoo's dividend
|
||||
Shipped as `idx stocks actions`, `idx stocks calendar` (not a top-level
|
||||
`calendar`: it sits beside `stocks screen`) and the MCP tools
|
||||
`get_corporate_actions`, `get_corporate_calendar`; the notes below are the
|
||||
endpoint as recorded. Before, idx had none (only the `high-dividend` screen and Yahoo's dividend
|
||||
yield). They explain price gaps in history (splits, bonuses, rights) and give
|
||||
a calendar of cum dates.
|
||||
|
||||
|
|
@ -151,10 +154,10 @@ lists are not errors. A row whose dates are all placeholders is dropped.
|
|||
|
||||
### Surface
|
||||
|
||||
- `idx stocks actions SYM [--kind dividend,rights,...] [--since DATE]`: one
|
||||
stock's history, newest first.
|
||||
- `idx calendar [--kind ...] [--from DATE] [--to DATE]`: market-wide, default
|
||||
today to +30 days, sorted by the next relevant date.
|
||||
- `idx stocks actions SYM [--kind dividend,rights,...]`: one stock's
|
||||
history, newest first; every kind but `ipo` by default.
|
||||
- `idx stocks calendar [--kind ...] [--from DATE] [--to DATE]`: market-wide,
|
||||
default today to +30 days, oldest first by `date`.
|
||||
- MCP: `get_corporate_actions {symbol, kinds?, since?}`,
|
||||
`get_corporate_calendar {kinds?, from?, to?}`.
|
||||
- Later: `stocks history` notes ex-dates of splits, bonuses and rights in the
|
||||
|
|
@ -231,7 +234,8 @@ Yahoo prices, forward 5/20/60-session returns against IHSG).
|
|||
|
||||
1. Close #74: the open-state name in trading hours, fixtures for the observed
|
||||
states (done 2026-11-03).
|
||||
2. Corporate actions (parsers, `stocks actions`, `calendar`, MCP, goldens).
|
||||
2. Corporate actions (parsers, `stocks actions`, `stocks calendar`, MCP,
|
||||
goldens) (done).
|
||||
3. Shareholder movements, rows first, then `--summary`.
|
||||
4. Event study for the movement score; P2 by demand.
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,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
|
||||
|
|
@ -223,6 +225,132 @@ Examples:
|
|||
idx -o json stocks orderbook TLKM | jq '.offers[0]'
|
||||
```
|
||||
|
||||
### `idx stocks actions`
|
||||
|
||||
```text
|
||||
One IDX stock's corporate actions from your own Stockbit session (`idx stockbit login`), newest first: cash and stock dividends, bonus shares, rights issues, warrants, splits and reverse splits, shareholders' meetings, public exposes and tender offers (`--kind ipo` adds its IPO). Stockbit serves one kind per request, 1 s apart, so the first call takes about a second per kind; answers are cached for 6 hours.
|
||||
|
||||
Usage: idx stocks actions [OPTIONS] <SYMBOL>
|
||||
|
||||
Arguments:
|
||||
<SYMBOL>
|
||||
Single ticker symbol (e.g. BBCA)
|
||||
|
||||
Options:
|
||||
--kind <KINDS>...
|
||||
Kinds to list (default: all but ipo)
|
||||
|
||||
Possible values:
|
||||
- dividend: Cash dividend
|
||||
- stock-dividend
|
||||
- bonus: Bonus shares
|
||||
- rights: Rights issue (HMETD)
|
||||
- warrant
|
||||
- split: Stock split
|
||||
- reverse-split
|
||||
- meeting: Shareholders' meeting (RUPS)
|
||||
- expose: Public expose
|
||||
- ipo
|
||||
- tender
|
||||
|
||||
-o, --output <OUTPUT>
|
||||
Output format [default: auto = table on a terminal, json when piped]
|
||||
|
||||
[possible values: auto, table, json, jsonl]
|
||||
|
||||
--no-color
|
||||
Disable colored output
|
||||
|
||||
-q, --quiet
|
||||
Suppress non-essential output
|
||||
|
||||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
--no-cache
|
||||
Bypass local cache
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
Examples:
|
||||
idx stocks actions BBCA
|
||||
idx stocks actions TLKM --kind dividend
|
||||
idx -o json stocks actions BBRI --kind dividend,split | jq '.[0]'
|
||||
```
|
||||
|
||||
### `idx stocks calendar`
|
||||
|
||||
```text
|
||||
Corporate actions across IDX from your own Stockbit session (`idx stockbit login`), oldest first, dated within --from and --to (default: today and the next 30 days). An action's date is its cum date for dividends, bonuses, rights and splits, else the event's first day. Stockbit lists about a year back and what is announced ahead. One request per kind, 1 s apart; answers are cached for 6 hours.
|
||||
|
||||
Usage: idx stocks calendar [OPTIONS]
|
||||
|
||||
Options:
|
||||
--from <FROM>
|
||||
First day, YYYY-MM-DD. Default: today
|
||||
|
||||
-o, --output <OUTPUT>
|
||||
Output format [default: auto = table on a terminal, json when piped]
|
||||
|
||||
[possible values: auto, table, json, jsonl]
|
||||
|
||||
--no-color
|
||||
Disable colored output
|
||||
|
||||
--to <TO>
|
||||
Last day, YYYY-MM-DD. Default: 30 days after --from (at most 366)
|
||||
|
||||
--kind <KINDS>...
|
||||
Kinds to list (default: all)
|
||||
|
||||
Possible values:
|
||||
- dividend: Cash dividend
|
||||
- stock-dividend
|
||||
- bonus: Bonus shares
|
||||
- rights: Rights issue (HMETD)
|
||||
- warrant
|
||||
- split: Stock split
|
||||
- reverse-split
|
||||
- meeting: Shareholders' meeting (RUPS)
|
||||
- expose: Public expose
|
||||
- ipo
|
||||
- tender
|
||||
|
||||
-q, --quiet
|
||||
Suppress non-essential output
|
||||
|
||||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
--no-cache
|
||||
Bypass local cache
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
Examples:
|
||||
idx stocks calendar
|
||||
idx stocks calendar --kind dividend,rights
|
||||
idx stocks calendar --from 2026-12-01 --to 2026-12-31 --kind meeting
|
||||
```
|
||||
|
||||
### `idx stocks history`
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ idx stockbit login # paste the credentialStorage cookie
|
|||
idx stockbit status # logged in? when do the tokens expire?
|
||||
idx stocks orderbook BBCA # depth, queued orders, foreign flow
|
||||
idx stocks orderbook BBRI --depth 5
|
||||
idx stocks actions BBCA # dividends, splits, meetings, ...
|
||||
idx stocks calendar --kind dividend # cum dates in the next 30 days
|
||||
idx --provider stockbit stocks quote BBCA # live, not delayed
|
||||
idx --provider stockbit stocks history BBCA --period 1y # Stockbit's daily candles
|
||||
idx stockbit logout
|
||||
|
|
@ -198,6 +200,7 @@ What the login adds:
|
|||
| `stocks orderbook` | `AUTHERROR` | every level with lots and queued orders, OHLC, value, trades; foreign buy/sell/net once Stockbit marks them current |
|
||||
| `stocks history` under `--provider stockbit` | Yahoo | Stockbit's daily candles for `5d` to `1y`; Yahoo for other windows |
|
||||
| `stocks intraday` | public page | public page |
|
||||
| `stocks actions`, `stocks calendar` | `AUTHERROR` | corporate actions: one stock's history, or the market's calendar |
|
||||
|
||||
The order book is what Stockbit's own app shows. Outside trading it is the
|
||||
book as the market left it, and the output says so. Foreign buy, sell and net
|
||||
|
|
@ -218,7 +221,7 @@ Stockbit. Use this for looking at a stock now and then, not for crawling the
|
|||
market or republishing prices. With your login, automated use is tied to your
|
||||
account: keep it human-paced.
|
||||
|
||||
Over MCP, the same data comes from `get_intraday`, `get_orderbook` and
|
||||
`stockbit_status`, and from `get_quotes` and `get_price_history` when the
|
||||
Over MCP, the same data comes from `get_intraday`, `get_orderbook`,
|
||||
`get_corporate_actions`, `get_corporate_calendar` and `stockbit_status`, and from `get_quotes` and `get_price_history` when the
|
||||
server runs with `IDX_PROVIDER=stockbit`. The server never logs in; run
|
||||
`idx stockbit login` in a terminal.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,28 @@ value and trades, and foreign buy, sell and net once Stockbit marks them
|
|||
current (not during trading, when they lag a day). Without one it answers
|
||||
`AUTHERROR`. See [Stockbit](/cli/stockbit/#with-your-stockbit-login).
|
||||
|
||||
## Corporate actions (Stockbit login)
|
||||
|
||||
```bash
|
||||
idx stocks actions BBCA # newest first
|
||||
idx stocks actions TLKM --kind dividend
|
||||
idx stocks calendar # today and the next 30 days
|
||||
idx stocks calendar --kind dividend,rights --to 2026-12-31
|
||||
```
|
||||
|
||||
`actions` lists one stock's cash and stock dividends, bonus shares, rights
|
||||
issues, warrants, splits and reverse splits, shareholders' meetings, public
|
||||
exposes and tender offers (`--kind ipo` adds its IPO). `calendar` lists them
|
||||
across the market between `--from` and `--to` (at most 366 days), including
|
||||
what is announced ahead. An action's date is its cum date for dividends,
|
||||
bonuses, rights and splits: buy by then to be entitled. For the others it is
|
||||
the event's first day. Dividends per share keep their decimals (`Rp 3.8`).
|
||||
|
||||
Both need your Stockbit session and answer `AUTHERROR` without one. Stockbit
|
||||
serves one kind per request, 1 s apart, so a first call with every kind takes
|
||||
about ten seconds; answers are then cached for 6 hours. `--kind` asks for
|
||||
less.
|
||||
|
||||
## Company data
|
||||
|
||||
| Command | What it shows |
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ description: idx-mcp serves idx's stock and ownership data to AI agents over MCP
|
|||
|
||||
`idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
|
||||
server over stdio. It gives an agent the same data as the `idx` CLI, as
|
||||
[17 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's
|
||||
session by the minute, the order book (with a Stockbit login), price
|
||||
history, screening, company data, analysis, comparison, broker summaries,
|
||||
[20 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's
|
||||
session by the minute, the order book and corporate actions (with a Stockbit
|
||||
login), price history, screening, company data, analysis, comparison, broker summaries,
|
||||
bandarmology, foreign flow, and the KSEI ownership queries.
|
||||
|
||||
It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps),
|
||||
|
|
|
|||
|
|
@ -63,6 +63,25 @@ Company data for one IDX ticker: profile, financial statements, earnings, analys
|
|||
| `sections` | array of `profile` / `financials` / `earnings` / `sentiment` / `insights` / `news` | no | Sections to fetch (default: all). |
|
||||
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
|
||||
|
||||
## `get_corporate_actions`
|
||||
|
||||
Corporate actions of one IDX stock from the user's own Stockbit session, newest first: `kind` (dividend, stock-dividend, bonus, rights, warrant, split, reverse-split, meeting, expose, ipo, tender), `date` (the cum date for dividends, bonuses, rights and splits, i.e. the last day to buy and be entitled; else the event's first day), `ex_date`, `record_date`, `pay_date`, and per kind `cash_per_share` (IDR, may have decimals), `ratio` {old, new} (1:5 split = 1 share becomes 5; 655:104 rights = 104 rights per 655 held), `factor` (price adjustment), `price` (rights, warrant exercise, tender or IPO price, IDR), `period` and `exercise` {start, end}, `shares`, `percentage_bps`, `time`, `venue`. Absent fields are omitted. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours). AUTHERROR means there is no session: call stockbit_status, then stockbit_login.
|
||||
|
||||
| Input | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `kinds` | array of `dividend` / `stock-dividend` / `bonus` / `rights` / `warrant` / `split` / `reverse-split` / `meeting` / `expose` / `ipo` / `tender` | no | Kinds to list (default: every kind but "ipo"). Each kind is one request to Stockbit. |
|
||||
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
|
||||
|
||||
## `get_corporate_calendar`
|
||||
|
||||
Corporate actions across IDX from the user's own Stockbit session, oldest first, dated from `from` to `to` (default today and the next 30 days): upcoming cum dates, meetings, exposes, rights, warrants, IPOs and tender offers, and the past year. Each item has the same fields as get_corporate_actions, with `symbol`. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours); pass `kinds` to ask for less. AUTHERROR means there is no session: call stockbit_status, then stockbit_login.
|
||||
|
||||
| Input | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `from` | string | no | First day, YYYY-MM-DD (default today). |
|
||||
| `kinds` | array of `dividend` / `stock-dividend` / `bonus` / `rights` / `warrant` / `split` / `reverse-split` / `meeting` / `expose` / `ipo` / `tender` | no | Kinds to list (default: every kind). Each kind is one request to Stockbit. |
|
||||
| `to` | string | no | Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days). |
|
||||
|
||||
## `get_foreign_flow`
|
||||
|
||||
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`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue