From 43ff4aa433bd743c19ba0b3bb77a7af945b8defb Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Tue, 3 Nov 2026 14:35:20 +0700 Subject: [PATCH] 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 --- README.md | 8 +- ROADMAP.md | 7 +- crates/idx-cli/src/cli/stocks.rs | 67 ++- crates/idx-cli/src/output/mod.rs | 23 + crates/idx-cli/src/output/table.rs | 185 ++++++++ crates/idx-cli/tests/golden/cases.toml | 33 ++ .../tests/golden/completions-bash.golden | 148 ++++++- .../error-actions-no-session.json.golden | 4 + .../error-actions-no-session.table.golden | 4 + .../golden/error-calendar-range.json.golden | 4 + .../golden/error-calendar-range.table.golden | 4 + .../idx-cli/tests/golden/help-stocks.golden | 2 + .../stockbit-actions-dividends.json.golden | 4 + .../stockbit-actions-dividends.table.golden | 83 ++++ .../tests/golden/stockbit-actions.json.golden | 4 + .../golden/stockbit-actions.table.golden | 43 ++ .../stockbit-calendar-meetings.json.golden | 4 + .../stockbit-calendar-meetings.table.golden | 29 ++ .../golden/stockbit-calendar.json.golden | 4 + .../golden/stockbit-calendar.table.golden | 37 ++ crates/idx-core/src/api/mock.rs | 36 +- crates/idx-core/src/api/mod.rs | 19 + .../idx-core/src/api/stockbit/corpaction.rs | 405 ++++++++++++++++++ crates/idx-core/src/api/stockbit/exodus.rs | 20 + crates/idx-core/src/api/stockbit/mod.rs | 15 +- crates/idx-core/src/client.rs | 4 + crates/idx-core/src/corporate/mod.rs | 108 +++++ crates/idx-core/src/corporate/types.rs | 292 +++++++++++++ crates/idx-core/src/lib.rs | 1 + .../stockbit_exodus_corpaction_bonus.json | 1 + .../stockbit_exodus_corpaction_dividend.json | 1 + ...ckbit_exodus_corpaction_dividend_bbca.json | 1 + .../stockbit_exodus_corpaction_ipo.json | 1 + .../stockbit_exodus_corpaction_pubex.json | 1 + ...stockbit_exodus_corpaction_pubex_bbca.json | 1 + ...ockbit_exodus_corpaction_reversesplit.json | 1 + ...stockbit_exodus_corpaction_rightissue.json | 1 + .../stockbit_exodus_corpaction_rups.json | 1 + .../stockbit_exodus_corpaction_rups_bbca.json | 1 + ...kbit_exodus_corpaction_stock_dividend.json | 1 + ...stockbit_exodus_corpaction_stocksplit.json | 1 + ...bit_exodus_corpaction_stocksplit_bbca.json | 1 + ...tockbit_exodus_corpaction_tenderoffer.json | 1 + .../stockbit_exodus_corpaction_warrant.json | 1 + crates/idx-mcp/src/params.rs | 41 ++ crates/idx-mcp/src/server.rs | 55 ++- crates/idx-mcp/tests/golden/tools.json | 85 ++++ crates/idx-mcp/tests/mcp.rs | 51 ++- docs/ARCHITECTURE.md | 14 + docs/STOCKBIT.md | 20 +- web/src/content/docs/cli/reference.md | 128 ++++++ web/src/content/docs/cli/stockbit.md | 7 +- web/src/content/docs/cli/stocks.md | 22 + web/src/content/docs/mcp/index.md | 6 +- web/src/content/docs/mcp/tools.md | 19 + 55 files changed, 2026 insertions(+), 34 deletions(-) create mode 100644 crates/idx-cli/tests/golden/error-actions-no-session.json.golden create mode 100644 crates/idx-cli/tests/golden/error-actions-no-session.table.golden create mode 100644 crates/idx-cli/tests/golden/error-calendar-range.json.golden create mode 100644 crates/idx-cli/tests/golden/error-calendar-range.table.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-actions-dividends.json.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-actions-dividends.table.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-actions.json.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-actions.table.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-calendar-meetings.json.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-calendar-meetings.table.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-calendar.json.golden create mode 100644 crates/idx-cli/tests/golden/stockbit-calendar.table.golden create mode 100644 crates/idx-core/src/api/stockbit/corpaction.rs create mode 100644 crates/idx-core/src/corporate/mod.rs create mode 100644 crates/idx-core/src/corporate/types.rs create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_bonus.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend_bbca.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_ipo.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex_bbca.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_reversesplit.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rightissue.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups_bbca.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stock_dividend.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit_bbca.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_tenderoffer.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_warrant.json diff --git a/README.md b/README.md index 5bf505b..8b5281c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 593bcec..88d1ea7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/crates/idx-cli/src/cli/stocks.rs b/crates/idx-cli/src/cli/stocks.rs index 84ec04c..718d7bb 100644 --- a/crates/idx-cli/src/cli/stocks.rs +++ b/crates/idx-cli/src/cli/stocks.rs @@ -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, + }, + #[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, + /// Last day, YYYY-MM-DD. Default: 30 days after --from (at most 366). + #[arg(long)] + to: Option, + /// Kinds to list (default: all). + #[arg(long = "kind", value_enum, value_delimiter = ',', num_args = 1..)] + kinds: Vec, + }, #[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, diff --git a/crates/idx-cli/src/output/mod.rs b/crates/idx-cli/src/output/mod.rs index ac13fc6..83dd257 100644 --- a/crates/idx-cli/src/output/mod.rs +++ b/crates/idx-cli/src/output/mod.rs @@ -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, diff --git a/crates/idx-cli/src/output/table.rs b/crates/idx-cli/src/output/table.rs index 9773fc8..994c454 100644 --- a/crates/idx-cli/src/output/table.rs +++ b/crates/idx-cli/src/output/table.rs @@ -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| range.map(|r| format!("{} to {}", r.start, r.end)); + let price = action.price.map(|p| format!("Rp {}", format_idr(p))); + let mut parts: Vec = 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) -> 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::{ diff --git a/crates/idx-cli/tests/golden/cases.toml b/crates/idx-cli/tests/golden/cases.toml index ded196e..942f618 100644 --- a/crates/idx-cli/tests/golden/cases.toml +++ b/crates/idx-cli/tests/golden/cases.toml @@ -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"] diff --git a/crates/idx-cli/tests/golden/completions-bash.golden b/crates/idx-cli/tests/golden/completions-bash.golden index bd541ff..603c3e0 100644 --- a/crates/idx-cli/tests/golden/completions-bash.golden +++ b/crates/idx-cli/tests/golden/completions-bash.golden @@ -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 " + 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 ..." 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 ..." 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 diff --git a/crates/idx-cli/tests/golden/error-actions-no-session.json.golden b/crates/idx-cli/tests/golden/error-actions-no-session.json.golden new file mode 100644 index 0000000..d38ac26 --- /dev/null +++ b/crates/idx-cli/tests/golden/error-actions-no-session.json.golden @@ -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)"} diff --git a/crates/idx-cli/tests/golden/error-actions-no-session.table.golden b/crates/idx-cli/tests/golden/error-actions-no-session.table.golden new file mode 100644 index 0000000..e1fcda0 --- /dev/null +++ b/crates/idx-cli/tests/golden/error-actions-no-session.table.golden @@ -0,0 +1,4 @@ +exit: 4 +--- stdout +--- stderr +Error: auth error: no Stockbit session; run `idx stockbit login` (or set IDX_STOCKBIT_REFRESH_TOKEN) diff --git a/crates/idx-cli/tests/golden/error-calendar-range.json.golden b/crates/idx-cli/tests/golden/error-calendar-range.json.golden new file mode 100644 index 0000000..e6c9ad9 --- /dev/null +++ b/crates/idx-cli/tests/golden/error-calendar-range.json.golden @@ -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)"} diff --git a/crates/idx-cli/tests/golden/error-calendar-range.table.golden b/crates/idx-cli/tests/golden/error-calendar-range.table.golden new file mode 100644 index 0000000..3fc65af --- /dev/null +++ b/crates/idx-cli/tests/golden/error-calendar-range.table.golden @@ -0,0 +1,4 @@ +exit: 2 +--- stdout +--- stderr +Error: invalid input: to (2026-11-01) is before from (2026-11-10) diff --git a/crates/idx-cli/tests/golden/help-stocks.golden b/crates/idx-cli/tests/golden/help-stocks.golden index 5970553..e004151 100644 --- a/crates/idx-cli/tests/golden/help-stocks.golden +++ b/crates/idx-cli/tests/golden/help-stocks.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-actions-dividends.json.golden b/crates/idx-cli/tests/golden/stockbit-actions-dividends.json.golden new file mode 100644 index 0000000..3b6bd49 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-actions-dividends.json.golden @@ -0,0 +1,4 @@ +exit: 0 +--- stdout +[{"symbol":"BBCA.JK","kind":"dividend","date":"2026-08-28","cum_date":"2026-08-28","ex_date":"2026-08-31","record_date":"2026-09-01","pay_date":"2026-09-16","cash_per_share":25.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2026-06-15","cum_date":"2026-06-15","ex_date":"2026-06-17","record_date":"2026-06-18","pay_date":"2026-06-26","cash_per_share":20.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2026-03-27","cum_date":"2026-03-27","ex_date":"2026-03-30","record_date":"2026-03-31","pay_date":"2026-04-08","cash_per_share":281.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2025-12-02","cum_date":"2025-12-02","ex_date":"2025-12-03","record_date":"2025-12-04","pay_date":"2025-12-22","cash_per_share":55.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2025-03-20","cum_date":"2025-03-20","ex_date":"2025-03-21","record_date":"2025-03-24","pay_date":"2025-04-11","cash_per_share":250.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2024-11-20","cum_date":"2024-11-20","ex_date":"2024-11-21","record_date":"2024-11-22","pay_date":"2024-12-11","cash_per_share":50.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2024-03-22","cum_date":"2024-03-22","ex_date":"2024-03-25","record_date":"2024-03-26","pay_date":"2024-04-04","cash_per_share":227.5},{"symbol":"BBCA.JK","kind":"dividend","date":"2023-12-01","cum_date":"2023-12-01","ex_date":"2023-12-04","record_date":"2023-12-05","pay_date":"2023-12-20","cash_per_share":42.5},{"symbol":"BBCA.JK","kind":"dividend","date":"2023-03-28","cum_date":"2023-03-28","ex_date":"2023-03-29","record_date":"2023-03-30","pay_date":"2023-04-14","cash_per_share":170.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2022-12-01","cum_date":"2022-12-01","ex_date":"2022-12-02","record_date":"2022-12-05","pay_date":"2022-12-20","cash_per_share":35.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2022-03-25","cum_date":"2022-03-25","ex_date":"2022-03-28","record_date":"2022-03-29","pay_date":"2022-04-19","cash_per_share":120.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2021-11-16","cum_date":"2021-11-16","ex_date":"2021-11-17","record_date":"2021-11-18","pay_date":"2021-12-07","cash_per_share":25.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2021-04-07","cum_date":"2021-04-07","ex_date":"2021-04-08","record_date":"2021-04-09","pay_date":"2021-04-28","cash_per_share":432.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2020-12-07","cum_date":"2020-12-07","ex_date":"2020-12-08","record_date":"2020-12-10","pay_date":"2020-12-22","cash_per_share":98.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2020-04-20","cum_date":"2020-04-20","ex_date":"2020-04-21","record_date":"2020-04-22","pay_date":"2020-05-11","cash_per_share":455.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2019-12-05","cum_date":"2019-12-05","ex_date":"2019-12-06","record_date":"2019-12-09","pay_date":"2019-12-20","cash_per_share":100.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2019-04-23","cum_date":"2019-04-23","ex_date":"2019-04-24","record_date":"2019-04-25","pay_date":"2019-04-30","cash_per_share":255.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2018-11-30","cum_date":"2018-11-30","ex_date":"2018-12-03","record_date":"2018-12-04","pay_date":"2018-12-21","cash_per_share":85.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2018-04-12","cum_date":"2018-04-12","ex_date":"2018-04-13","record_date":"2018-04-17","pay_date":"2018-04-30","cash_per_share":175.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2017-11-28","cum_date":"2017-11-28","ex_date":"2017-11-29","record_date":"2017-12-04","pay_date":"2017-12-20","cash_per_share":80.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2017-04-13","cum_date":"2017-04-13","ex_date":"2017-04-17","record_date":"2017-04-20","pay_date":"2017-04-28","cash_per_share":130.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2016-11-30","cum_date":"2016-11-30","ex_date":"2016-12-01","record_date":"2016-12-05","pay_date":"2016-12-22","cash_per_share":70.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2016-04-14","cum_date":"2016-04-14","ex_date":"2016-04-15","record_date":"2016-04-19","pay_date":"2016-04-29","cash_per_share":105.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2015-11-12","cum_date":"2015-11-12","ex_date":"2015-11-13","record_date":"2015-11-17","pay_date":"2015-12-09","cash_per_share":55.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2015-04-16","cum_date":"2015-04-16","ex_date":"2015-04-17","record_date":"2015-04-21","pay_date":"2015-05-13","cash_per_share":98.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2014-12-04","cum_date":"2014-12-04","ex_date":"2014-12-05","record_date":"2014-12-09","pay_date":"2014-12-23","cash_per_share":50.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2014-04-29","cum_date":"2014-04-29","ex_date":"2014-04-30","record_date":"2014-05-05","pay_date":"2014-05-20","cash_per_share":75.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2013-11-28","cum_date":"2013-11-28","ex_date":"2013-11-29","record_date":"2013-12-03","pay_date":"2013-12-17","cash_per_share":45.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2013-05-28","cum_date":"2013-05-28","ex_date":"2013-05-29","record_date":"2013-05-31","pay_date":"2013-06-17","cash_per_share":71.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2012-12-03","cum_date":"2012-12-03","ex_date":"2012-12-04","record_date":"2012-12-06","pay_date":"2012-12-20","cash_per_share":43.5},{"symbol":"BBCA.JK","kind":"dividend","date":"2012-06-08","cum_date":"2012-06-08","ex_date":"2012-06-11","record_date":"2012-06-13","pay_date":"2012-06-27","cash_per_share":70.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2011-12-06","cum_date":"2011-12-06","ex_date":"2011-12-07","record_date":"2011-12-09","pay_date":"2011-12-23","cash_per_share":43.5},{"symbol":"BBCA.JK","kind":"dividend","date":"2011-06-06","cum_date":"2011-06-06","ex_date":"2011-06-07","record_date":"2011-06-09","pay_date":"2011-06-23","cash_per_share":70.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2010-11-19","cum_date":"2010-11-19","ex_date":"2010-11-22","record_date":"2010-11-24","pay_date":"2010-12-09","cash_per_share":42.5},{"symbol":"BBCA.JK","kind":"dividend","date":"2010-05-31","cum_date":"2010-05-31","ex_date":"2010-06-01","record_date":"2010-06-03","pay_date":"2010-06-17","cash_per_share":70.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2009-11-12","cum_date":"2009-11-12","ex_date":"2009-11-13","record_date":"2009-11-17","pay_date":"2009-12-02","cash_per_share":40.0},{"symbol":"BBCA.JK","kind":"dividend","date":"2009-06-09","cum_date":"2009-06-09","ex_date":"2009-06-10","record_date":"2009-06-12","pay_date":"2009-06-26","cash_per_share":65.0}] +--- stderr diff --git a/crates/idx-cli/tests/golden/stockbit-actions-dividends.table.golden b/crates/idx-cli/tests/golden/stockbit-actions-dividends.table.golden new file mode 100644 index 0000000..85b4d46 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-actions-dividends.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-actions.json.golden b/crates/idx-cli/tests/golden/stockbit-actions.json.golden new file mode 100644 index 0000000..d5f9ad6 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-actions.json.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-actions.table.golden b/crates/idx-cli/tests/golden/stockbit-actions.table.golden new file mode 100644 index 0000000..c7ac018 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-actions.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-calendar-meetings.json.golden b/crates/idx-cli/tests/golden/stockbit-calendar-meetings.json.golden new file mode 100644 index 0000000..28caa37 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-calendar-meetings.json.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-calendar-meetings.table.golden b/crates/idx-cli/tests/golden/stockbit-calendar-meetings.table.golden new file mode 100644 index 0000000..e48c3b5 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-calendar-meetings.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-calendar.json.golden b/crates/idx-cli/tests/golden/stockbit-calendar.json.golden new file mode 100644 index 0000000..7455de0 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-calendar.json.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-calendar.table.golden b/crates/idx-cli/tests/golden/stockbit-calendar.table.golden new file mode 100644 index 0000000..7eb5aba --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-calendar.table.golden @@ -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 diff --git a/crates/idx-core/src/api/mock.rs b/crates/idx-core/src/api/mock.rs index f392967..6a76d4d 100644 --- a/crates/idx-core/src/api/mock.rs +++ b/crates/idx-core/src/api/mock.rs @@ -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_.json` for the market, +/// `...__.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, 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, diff --git a/crates/idx-core/src/api/mod.rs b/crates/idx-core/src/api/mod.rs index ae9cd0f..b3fb07a 100644 --- a/crates/idx-core/src/api/mod.rs +++ b/crates/idx-core/src/api/mod.rs @@ -78,6 +78,17 @@ pub trait OrderBookProvider { fn orderbook(&self, symbol: &str) -> Result; } +/// 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, IdxError>; +} + pub trait FundamentalsProvider { fn fundamentals(&self, symbol: &str) -> Result; } @@ -345,6 +356,14 @@ pub fn orderbook_provider() -> Box { Box::new(stockbit::StockbitProvider::new()) } +pub fn corporate_action_provider() -> Box { + #[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")] diff --git a/crates/idx-core/src/api/stockbit/corpaction.rs b/crates/idx-core/src/api/stockbit/corpaction.rs new file mode 100644 index 0000000..7dfb0f1 --- /dev/null +++ b/crates/idx-core/src/api/stockbit/corpaction.rs @@ -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, 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) -> Option { + 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, +) -> Option { + 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) -> Option { + 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 `_`; the shortest name +/// wins should several end so. +fn suffixed<'a>(row: &'a Map, 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) -> Option { + let pair = |old: Option, new: Option| { + 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 { + 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 { + 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 { + 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 { + number(value).map(|n| n.round() as i64) +} + +fn count(value: Option<&Value>) -> Option { + 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::>().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::>().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"); + } +} diff --git a/crates/idx-core/src/api/stockbit/exodus.rs b/crates/idx-core/src/api/stockbit/exodus.rs index c2030ff..ddd0fca 100644 --- a/crates/idx-core/src/api/stockbit/exodus.rs +++ b/crates/idx-core/src/api/stockbit/exodus.rs @@ -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, 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 { diff --git a/crates/idx-core/src/api/stockbit/mod.rs b/crates/idx-core/src/api/stockbit/mod.rs index 59262c0..a00abf1 100644 --- a/crates/idx-core/src/api/stockbit/mod.rs +++ b/crates/idx-core/src/api/stockbit/mod.rs @@ -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, IdxError> { + self.require_session()?.corporate_actions(kind, symbol) + } +} + impl HistoryProvider for StockbitProvider { fn history( &self, diff --git a/crates/idx-core/src/client.rs b/crates/idx-core/src/client.rs index f1551ce..aef1c39 100644 --- a/crates/idx-core/src/client.rs +++ b/crates/idx-core/src/client.rs @@ -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 } } diff --git a/crates/idx-core/src/corporate/mod.rs b/crates/idx-core/src/corporate/mod.rs new file mode 100644 index 0000000..4ddfe01 --- /dev/null +++ b/crates/idx-core/src/corporate/mod.rs @@ -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>, 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>, 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>, 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) + } +} diff --git a/crates/idx-core/src/corporate/types.rs b/crates/idx-core/src/corporate/types.rs new file mode 100644 index 0000000..97984e3 --- /dev/null +++ b/crates/idx-core/src/corporate/types.rs @@ -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::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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ex_date: Option, + /// Recording date; for a meeting, the date that decides who may attend. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub record_date: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pay_date: Option, + /// 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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ratio: Option, + /// 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, + /// 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, + /// 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, + /// When warrants can be exercised. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exercise: Option, + /// Shares offered (IPO) or sought (tender offer). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub shares: Option, + /// Share of the company a tender offer seeks, basis points. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub percentage_bps: Option, + /// Meeting or expose time, as Stockbit writes it (WIB). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub time: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub venue: Option, + /// The issuer's name, when Stockbit gives it (IPO, tender offer). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +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 { + 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::().unwrap(), kind); + } + assert!("rups".parse::().is_err()); + } +} diff --git a/crates/idx-core/src/lib.rs b/crates/idx-core/src/lib.rs index bd01c1c..cd49711 100644 --- a/crates/idx-core/src/lib.rs +++ b/crates/idx-core/src/lib.rs @@ -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; diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_bonus.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_bonus.json new file mode 100644 index 0000000..f673ef1 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_bonus.json @@ -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"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend.json new file mode 100644 index 0000000..e1d6256 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend.json @@ -0,0 +1 @@ +{"data":{"dividend":[{"company_id":"290","company_symbol":"MIDI","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2026-11-11","dividend_datahash":"","dividend_exdate":"2026-11-12","dividend_id":"118120","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-12-01","dividend_recdate":"2026-11-13","dividend_value":"3.8","lastprice":"244","event_note":"","dividend_value_formatted":"Rp 3.8","lastprice_formatted":"244","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000002614","company_symbol":"IFII","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2026-11-11","dividend_datahash":"","dividend_exdate":"2026-11-12","dividend_id":"118119","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-26","dividend_recdate":"2026-11-13","dividend_value":"5","lastprice":"198","event_note":"","dividend_value_formatted":"Rp 5","lastprice_formatted":"198","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"224","company_symbol":"ITMG","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-10","dividend_datahash":"","dividend_exdate":"2026-11-11","dividend_id":"118118","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-18","dividend_recdate":"2026-11-12","dividend_value":"1191.09","lastprice":"24725","event_note":"","dividend_value_formatted":"Rp 1191.09","lastprice_formatted":"24,725","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"107","company_symbol":"BWPT","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-10","dividend_datahash":"","dividend_exdate":"2026-11-11","dividend_id":"118117","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-25","dividend_recdate":"2026-11-12","dividend_value":"1","lastprice":"75","event_note":"","dividend_value_formatted":"Rp 1","lastprice_formatted":"75","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"501","company_symbol":"SSMS","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-10","dividend_datahash":"","dividend_exdate":"2026-11-11","dividend_id":"118116","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-12-02","dividend_recdate":"2026-11-12","dividend_value":"52.49","lastprice":"970","event_note":"","dividend_value_formatted":"Rp 52.49","lastprice_formatted":"970","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"283","company_symbol":"MEDC","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-10","dividend_datahash":"","dividend_exdate":"2026-11-11","dividend_id":"118115","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-12-02","dividend_recdate":"2026-11-12","dividend_value":"21.65","lastprice":"1295","event_note":"","dividend_value_formatted":"Rp 21.65","lastprice_formatted":"1,295","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000003271","company_symbol":"TAPG","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-10","dividend_datahash":"","dividend_exdate":"2026-11-11","dividend_id":"118114","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-27","dividend_recdate":"2026-11-12","dividend_value":"70","lastprice":"1875","event_note":"","dividend_value_formatted":"Rp 70","lastprice_formatted":"1,875","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000003485","company_symbol":"AVIA","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-10","dividend_datahash":"","dividend_exdate":"2026-11-11","dividend_id":"118113","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-19","dividend_recdate":"2026-11-12","dividend_value":"12","lastprice":"296","event_note":"","dividend_value_formatted":"Rp 12","lastprice_formatted":"296","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"394","company_symbol":"SMSM","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-09","dividend_datahash":"","dividend_exdate":"2026-11-10","dividend_id":"118112","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-26","dividend_recdate":"2026-11-11","dividend_value":"45","lastprice":"1585","event_note":"","dividend_value_formatted":"Rp 45","lastprice_formatted":"1,585","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000004196","company_symbol":"CNMA","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-06","dividend_datahash":"","dividend_exdate":"2026-11-09","dividend_id":"118110","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-19","dividend_recdate":"2026-11-10","dividend_value":"4","lastprice":"75","event_note":"","dividend_value_formatted":"Rp 4","lastprice_formatted":"75","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000004325","company_symbol":"PTPS","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-05","dividend_datahash":"","dividend_exdate":"2026-11-06","dividend_id":"118111","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-18","dividend_recdate":"2026-11-09","dividend_value":"2","lastprice":"123","event_note":"","dividend_value_formatted":"Rp 2","lastprice_formatted":"123","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"231","company_symbol":"JPFA","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-05","dividend_datahash":"","dividend_exdate":"2026-11-06","dividend_id":"118109","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-12","dividend_recdate":"2026-11-09","dividend_value":"90","lastprice":"1700","event_note":"","dividend_value_formatted":"Rp 90","lastprice_formatted":"1,700","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"447","company_symbol":"UNTR","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-05","dividend_datahash":"","dividend_exdate":"2026-11-06","dividend_id":"118108","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-23","dividend_recdate":"2026-11-09","dividend_value":"430","lastprice":"23225","event_note":"","dividend_value_formatted":"Rp 430","lastprice_formatted":"23,225","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"502","company_symbol":"SIDO","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-11-02","dividend_datahash":"","dividend_exdate":"2026-11-03","dividend_id":"118107","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-20","dividend_recdate":"2026-11-04","dividend_value":"10","lastprice":"318","event_note":"","dividend_value_formatted":"Rp 10","lastprice_formatted":"318","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"441","company_symbol":"TSPC","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-10-30","dividend_datahash":"","dividend_exdate":"2026-11-02","dividend_id":"118106","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-20","dividend_recdate":"2026-11-03","dividend_value":"75","lastprice":"2240","event_note":"","dividend_value_formatted":"Rp 75","lastprice_formatted":"2,240","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"532","company_symbol":"DMAS","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-10-28","dividend_datahash":"","dividend_exdate":"2026-10-29","dividend_id":"118105","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-16","dividend_recdate":"2026-10-30","dividend_value":"29","lastprice":"161","event_note":"","dividend_value_formatted":"Rp 29","lastprice_formatted":"161","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000003710","company_symbol":"AXIO","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-10-28","dividend_datahash":"","dividend_exdate":"2026-10-29","dividend_id":"118104","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-19","dividend_recdate":"2026-10-30","dividend_value":"8","lastprice":"123","event_note":"","dividend_value_formatted":"Rp 8","lastprice_formatted":"123","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"1000002303","company_symbol":"XSPI","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-10-28","dividend_datahash":"","dividend_exdate":"2026-10-29","dividend_id":"118103","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-05","dividend_recdate":"2026-10-30","dividend_value":"5.94","lastprice":"426","event_note":"","dividend_value_formatted":"Rp 5.94","lastprice_formatted":"426","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"371","company_symbol":"SCMA","corp_action_active":true,"dividend_created":"","dividend_cumdate":"2026-10-27","dividend_datahash":"","dividend_exdate":"2026-10-28","dividend_id":"118102","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-11-18","dividend_recdate":"2026-10-29","dividend_value":"8.4","lastprice":"126","event_note":"","dividend_value_formatted":"Rp 8.4","lastprice_formatted":"126","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"224","company_symbol":"ITMG","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2026-04-27","dividend_datahash":"","dividend_exdate":"2026-04-28","dividend_id":"117678","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-05-19","dividend_recdate":"2026-04-29","dividend_value":"992","lastprice":"24725","event_note":"","dividend_value_formatted":"Rp 992","lastprice_formatted":"24,725","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"224","company_symbol":"ITMG","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2025-11-12","dividend_datahash":"","dividend_exdate":"2025-11-13","dividend_id":"117592","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2025-11-26","dividend_recdate":"2025-11-14","dividend_value":"738","lastprice":"24725","event_note":"","dividend_value_formatted":"Rp 738","lastprice_formatted":"24,725","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0}],"today":"2026-11-03"},"message":"Successfully retrieved dividend data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend_bbca.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend_bbca.json new file mode 100644 index 0000000..f11d089 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_dividend_bbca.json @@ -0,0 +1 @@ +{"data":{"dividend":[{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2026-08-28","dividend_datahash":"","dividend_exdate":"2026-08-31","dividend_id":"118073","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-09-16","dividend_recdate":"2026-09-01","dividend_value":"25","lastprice":"","event_note":"","dividend_value_formatted":"Rp 25","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2026-06-15","dividend_datahash":"","dividend_exdate":"2026-06-17","dividend_id":"117860","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-06-26","dividend_recdate":"2026-06-18","dividend_value":"20","lastprice":"","event_note":"","dividend_value_formatted":"Rp 20","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2026-03-27","dividend_datahash":"","dividend_exdate":"2026-03-30","dividend_id":"117649","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2026-04-08","dividend_recdate":"2026-03-31","dividend_value":"281","lastprice":"","event_note":"","dividend_value_formatted":"Rp 281","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2025-12-02","dividend_datahash":"","dividend_exdate":"2025-12-03","dividend_id":"117616","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2025-12-22","dividend_recdate":"2025-12-04","dividend_value":"55","lastprice":"","event_note":"","dividend_value_formatted":"Rp 55","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2025-03-20","dividend_datahash":"","dividend_exdate":"2025-03-21","dividend_id":"117119","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2025-04-11","dividend_recdate":"2025-03-24","dividend_value":"250","lastprice":"","event_note":"","dividend_value_formatted":"Rp 250","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2024-11-20","dividend_datahash":"","dividend_exdate":"2024-11-21","dividend_id":"117067","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2024-12-11","dividend_recdate":"2024-11-22","dividend_value":"50","lastprice":"","event_note":"","dividend_value_formatted":"Rp 50","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2024-03-22","dividend_datahash":"","dividend_exdate":"2024-03-25","dividend_id":"114270","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2024-04-04","dividend_recdate":"2024-03-26","dividend_value":"227.5","lastprice":"","event_note":"","dividend_value_formatted":"Rp 227.5","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2023-12-01","dividend_datahash":"","dividend_exdate":"2023-12-04","dividend_id":"114214","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2023-12-20","dividend_recdate":"2023-12-05","dividend_value":"42.5","lastprice":"","event_note":"","dividend_value_formatted":"Rp 42.5","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2023-03-28","dividend_datahash":"","dividend_exdate":"2023-03-29","dividend_id":"113604","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2023-04-14","dividend_recdate":"2023-03-30","dividend_value":"170","lastprice":"","event_note":"","dividend_value_formatted":"Rp 170","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2022-12-01","dividend_datahash":"","dividend_exdate":"2022-12-02","dividend_id":"113554","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2022-12-20","dividend_recdate":"2022-12-05","dividend_value":"35","lastprice":"","event_note":"","dividend_value_formatted":"Rp 35","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2022-03-25","dividend_datahash":"","dividend_exdate":"2022-03-28","dividend_id":"113048","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2022-04-19","dividend_recdate":"2022-03-29","dividend_value":"120","lastprice":"","event_note":"","dividend_value_formatted":"Rp 120","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2021-11-16","dividend_datahash":"","dividend_exdate":"2021-11-17","dividend_id":"96673","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2021-12-07","dividend_recdate":"2021-11-18","dividend_value":"25","lastprice":"","event_note":"","dividend_value_formatted":"Rp 25","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2021-04-07","dividend_datahash":"","dividend_exdate":"2021-04-08","dividend_id":"84523","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2021-04-28","dividend_recdate":"2021-04-09","dividend_value":"432","lastprice":"","event_note":"","dividend_value_formatted":"Rp 432","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2020-12-07","dividend_datahash":"","dividend_exdate":"2020-12-08","dividend_id":"83964","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2020-12-22","dividend_recdate":"2020-12-10","dividend_value":"98","lastprice":"","event_note":"","dividend_value_formatted":"Rp 98","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2020-04-20","dividend_datahash":"","dividend_exdate":"2020-04-21","dividend_id":"68067","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2020-05-11","dividend_recdate":"2020-04-22","dividend_value":"455","lastprice":"","event_note":"","dividend_value_formatted":"Rp 455","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2019-12-05","dividend_datahash":"","dividend_exdate":"2019-12-06","dividend_id":"58204","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2019-12-20","dividend_recdate":"2019-12-09","dividend_value":"100","lastprice":"","event_note":"","dividend_value_formatted":"Rp 100","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2019-04-23","dividend_datahash":"","dividend_exdate":"2019-04-24","dividend_id":"33852","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2019-04-30","dividend_recdate":"2019-04-25","dividend_value":"255","lastprice":"","event_note":"","dividend_value_formatted":"Rp 255","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2018-11-30","dividend_datahash":"","dividend_exdate":"2018-12-03","dividend_id":"23024","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2018-12-21","dividend_recdate":"2018-12-04","dividend_value":"85","lastprice":"","event_note":"","dividend_value_formatted":"Rp 85","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2018-04-12","dividend_datahash":"","dividend_exdate":"2018-04-13","dividend_id":"4874","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2018-04-30","dividend_recdate":"2018-04-17","dividend_value":"175","lastprice":"","event_note":"","dividend_value_formatted":"Rp 175","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2017-11-28","dividend_datahash":"","dividend_exdate":"2017-11-29","dividend_id":"4531","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2017-12-20","dividend_recdate":"2017-12-04","dividend_value":"80","lastprice":"","event_note":"","dividend_value_formatted":"Rp 80","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2017-04-13","dividend_datahash":"","dividend_exdate":"2017-04-17","dividend_id":"2831","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2017-04-28","dividend_recdate":"2017-04-20","dividend_value":"130","lastprice":"","event_note":"","dividend_value_formatted":"Rp 130","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2016-11-30","dividend_datahash":"","dividend_exdate":"2016-12-01","dividend_id":"2599","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2016-12-22","dividend_recdate":"2016-12-05","dividend_value":"70","lastprice":"","event_note":"","dividend_value_formatted":"Rp 70","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2016-04-14","dividend_datahash":"","dividend_exdate":"2016-04-15","dividend_id":"1283","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2016-04-29","dividend_recdate":"2016-04-19","dividend_value":"105","lastprice":"","event_note":"","dividend_value_formatted":"Rp 105","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2015-11-12","dividend_datahash":"","dividend_exdate":"2015-11-13","dividend_id":"1109","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2015-12-09","dividend_recdate":"2015-11-17","dividend_value":"55","lastprice":"","event_note":"","dividend_value_formatted":"Rp 55","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2015-04-16","dividend_datahash":"","dividend_exdate":"2015-04-17","dividend_id":"996","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2015-05-13","dividend_recdate":"2015-04-21","dividend_value":"98","lastprice":"","event_note":"","dividend_value_formatted":"Rp 98","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2014-12-04","dividend_datahash":"","dividend_exdate":"2014-12-05","dividend_id":"11230","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2014-12-23","dividend_recdate":"2014-12-09","dividend_value":"50","lastprice":"","event_note":"","dividend_value_formatted":"Rp 50","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2014-04-29","dividend_datahash":"","dividend_exdate":"2014-04-30","dividend_id":"11019","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2014-05-20","dividend_recdate":"2014-05-05","dividend_value":"75","lastprice":"","event_note":"","dividend_value_formatted":"Rp 75","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2013-11-28","dividend_datahash":"","dividend_exdate":"2013-11-29","dividend_id":"10978","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2013-12-17","dividend_recdate":"2013-12-03","dividend_value":"45","lastprice":"","event_note":"","dividend_value_formatted":"Rp 45","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2013-05-28","dividend_datahash":"","dividend_exdate":"2013-05-29","dividend_id":"10806","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2013-06-17","dividend_recdate":"2013-05-31","dividend_value":"71","lastprice":"","event_note":"","dividend_value_formatted":"Rp 71","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2012-12-03","dividend_datahash":"","dividend_exdate":"2012-12-04","dividend_id":"10744","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2012-12-20","dividend_recdate":"2012-12-06","dividend_value":"43.5","lastprice":"","event_note":"","dividend_value_formatted":"Rp 43.5","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2012-06-08","dividend_datahash":"","dividend_exdate":"2012-06-11","dividend_id":"10604","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2012-06-27","dividend_recdate":"2012-06-13","dividend_value":"70","lastprice":"","event_note":"","dividend_value_formatted":"Rp 70","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2011-12-06","dividend_datahash":"","dividend_exdate":"2011-12-07","dividend_id":"10538","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2011-12-23","dividend_recdate":"2011-12-09","dividend_value":"43.5","lastprice":"","event_note":"","dividend_value_formatted":"Rp 43.5","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2011-06-06","dividend_datahash":"","dividend_exdate":"2011-06-07","dividend_id":"10364","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2011-06-23","dividend_recdate":"2011-06-09","dividend_value":"70","lastprice":"","event_note":"","dividend_value_formatted":"Rp 70","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2010-11-19","dividend_datahash":"","dividend_exdate":"2010-11-22","dividend_id":"10299","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2010-12-09","dividend_recdate":"2010-11-24","dividend_value":"42.5","lastprice":"","event_note":"","dividend_value_formatted":"Rp 42.5","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2010-05-31","dividend_datahash":"","dividend_exdate":"2010-06-01","dividend_id":"10149","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2010-06-17","dividend_recdate":"2010-06-03","dividend_value":"70","lastprice":"","event_note":"","dividend_value_formatted":"Rp 70","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2009-11-12","dividend_datahash":"","dividend_exdate":"2009-11-13","dividend_id":"10104","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2009-12-02","dividend_recdate":"2009-11-17","dividend_value":"40","lastprice":"","event_note":"","dividend_value_formatted":"Rp 40","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0},{"company_id":"54","company_symbol":"BBCA","corp_action_active":false,"dividend_created":"","dividend_cumdate":"2009-06-09","dividend_datahash":"","dividend_exdate":"2009-06-10","dividend_id":"9986","dividend_iqp_id":"","dividend_lastupdate":"","dividend_lock":0,"dividend_paydate":"2009-06-26","dividend_recdate":"2009-06-12","dividend_value":"65","lastprice":"","event_note":"","dividend_value_formatted":"Rp 65","lastprice_formatted":"","dividend_currency":"CURRENCY_IDR","dividend_fiscal_year":0,"dividend_value_adjusted":0}],"today":"2026-11-03"},"message":"Successfully retrieved dividend data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_ipo.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_ipo.json new file mode 100644 index 0000000..fe606a4 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_ipo.json @@ -0,0 +1 @@ +{"data":{"ipo":[{"ipo_id":"6851","company_symbol":"XKGS","company_name":"Reksa Dana Syariah KIM Gold ETF Sharia","ipo_listing_date":"2026-09-26","ipo_data":"{\"%\":\"0\",\"Allotment Date\":\"\",\"Offering End\":\"0001-01-01 00:00:00\",\"Offering Start\":\"0001-01-01 00:00:00\",\"Price\":\"0\",\"Refund Date\":\"\",\"Shares\":\"\",\"board\":\"\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"0","shares":"","percentage":"0","offering_start":"0001-01-01 00:00:00","offering_end":"0001-01-01 00:00:00","allotment_date":"","refund_date":"","price_formatted":"0","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":0}},{"ipo_id":"6847","company_symbol":"XDES","company_name":"Reksa Dana Syariah BRI MI Gold ETF Syariah","ipo_listing_date":"2026-08-10","ipo_data":"{\"%\":\"0\",\"Allotment Date\":\"\",\"Offering End\":\"0001-01-01 00:00:00\",\"Offering Start\":\"0001-01-01 00:00:00\",\"Price\":\"0\",\"Refund Date\":\"\",\"Shares\":\"\",\"board\":\"\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"0","shares":"","percentage":"0","offering_start":"0001-01-01 00:00:00","offering_end":"0001-01-01 00:00:00","allotment_date":"","refund_date":"","price_formatted":"0","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":0}},{"ipo_id":"6848","company_symbol":"XMES","company_name":"Reksa Dana Syariah Mandiri ETF Emas","ipo_listing_date":"2026-08-10","ipo_data":"{\"%\":\"0\",\"Allotment Date\":\"\",\"Offering End\":\"0001-01-01 00:00:00\",\"Offering Start\":\"0001-01-01 00:00:00\",\"Price\":\"0\",\"Refund Date\":\"\",\"Shares\":\"\",\"board\":\"\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"0","shares":"","percentage":"0","offering_start":"0001-01-01 00:00:00","offering_end":"0001-01-01 00:00:00","allotment_date":"","refund_date":"","price_formatted":"0","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":0}},{"ipo_id":"6849","company_symbol":"XSGO","company_name":"Reksa Dana Syariah Syailendra ETF Sharia Gold","ipo_listing_date":"2026-08-10","ipo_data":"{\"%\":\"0\",\"Allotment Date\":\"\",\"Offering End\":\"0001-01-01 00:00:00\",\"Offering Start\":\"0001-01-01 00:00:00\",\"Price\":\"0\",\"Refund Date\":\"\",\"Shares\":\"\",\"board\":\"\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"0","shares":"","percentage":"0","offering_start":"0001-01-01 00:00:00","offering_end":"0001-01-01 00:00:00","allotment_date":"","refund_date":"","price_formatted":"0","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":0}},{"ipo_id":"6850","company_symbol":"XTRA","company_name":"Reksa Dana Syariah Trimegah Syariah ETF Emas","ipo_listing_date":"2026-08-10","ipo_data":"{\"%\":\"0\",\"Allotment Date\":\"\",\"Offering End\":\"0001-01-01 00:00:00\",\"Offering Start\":\"0001-01-01 00:00:00\",\"Price\":\"0\",\"Refund Date\":\"\",\"Shares\":\"\",\"board\":\"\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"0","shares":"","percentage":"0","offering_start":"0001-01-01 00:00:00","offering_end":"0001-01-01 00:00:00","allotment_date":"","refund_date":"","price_formatted":"0","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":0}},{"ipo_id":"6834","company_symbol":"RANS","company_name":"PT Rans Entertainmen Indonesia","ipo_listing_date":"2026-07-10","ipo_data":"{\"%\":\"20.02\",\"Allotment Date\":\"\",\"Offering End\":\"2026-07-08 00:00:00\",\"Offering Start\":\"2026-07-02 00:00:00\",\"Price\":\"170\",\"Refund Date\":\"\",\"Shares\":\"2,525,000,000\",\"board\":\"\",\"bureau_administration\":\"PT Adimitra Jasa Korpora\",\"underwriter\":[\"LG - TRIMEGAH SEKURITAS INDONESIA TBK.\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"170","shares":"2,525,000,000","percentage":"20.02","offering_start":"2026-07-02 00:00:00","offering_end":"2026-07-08 00:00:00","allotment_date":"","refund_date":"","price_formatted":"170","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":170}},{"ipo_id":"6826","company_symbol":"PRDL","company_name":"PT Prodia Diagnostic Line","ipo_listing_date":"2026-07-09","ipo_data":"{\"%\":\"30\",\"Allotment Date\":\"\",\"Offering End\":\"2026-07-07 00:00:00\",\"Offering Start\":\"2026-07-01 00:00:00\",\"Price\":\"120\",\"Refund Date\":\"\",\"Shares\":\"522,900,000\",\"board\":\"\",\"bureau_administration\":\"PT Adimitra Jasa Korpora\",\"underwriter\":[\"AZ - SUCOR SEKURITAS\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"120","shares":"522,900,000","percentage":"30","offering_start":"2026-07-01 00:00:00","offering_end":"2026-07-07 00:00:00","allotment_date":"","refund_date":"","price_formatted":"120","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":120}},{"ipo_id":"6828","company_symbol":"EMMI","company_name":"PT Esa Medika Mandiri","ipo_listing_date":"2026-07-08","ipo_data":"{\"%\":\"30\",\"Allotment Date\":\"\",\"Offering End\":\"2026-07-06 00:00:00\",\"Offering Start\":\"2026-07-02 00:00:00\",\"Price\":\"470\",\"Refund Date\":\"\",\"Shares\":\"522,857,000\",\"board\":\"\",\"bureau_administration\":\"PT Datindo Entrycom\",\"underwriter\":[\"OD - BRI DANAREKSA SEKURITAS\",\"RB - INA SEKURITAS INDONESIA\",\"RS - YULIE SEKURITAS INDONESIA TBK.\",\"IN - INVESTINDO NUSANTARA SEKURITAS\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"470","shares":"522,857,000","percentage":"30","offering_start":"2026-07-02 00:00:00","offering_end":"2026-07-06 00:00:00","allotment_date":"","refund_date":"","price_formatted":"470","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":470}},{"ipo_id":"6830","company_symbol":"BACH","company_name":"PT Bach Multi Global","ipo_listing_date":"2026-07-08","ipo_data":"{\"%\":\"15.06\",\"Allotment Date\":\"\",\"Offering End\":\"2026-07-06 00:00:00\",\"Offering Start\":\"2026-07-02 00:00:00\",\"Price\":\"442\",\"Refund Date\":\"\",\"Shares\":\"615,000,000\",\"board\":\"\",\"bureau_administration\":\"PT Adimitra Jasa Korpora\",\"underwriter\":[\"AO - ERDIKHA ELIT SEKURITAS\",\"PO - PILARMAS INVESTINDO SEKURITAS\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"442","shares":"615,000,000","percentage":"15.06","offering_start":"2026-07-02 00:00:00","offering_end":"2026-07-06 00:00:00","allotment_date":"","refund_date":"","price_formatted":"442","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":442}},{"ipo_id":"6824","company_symbol":"JELI","company_name":"PT Niramas Utama","ipo_listing_date":"2026-07-07","ipo_data":"{\"%\":\"21.01\",\"Allotment Date\":\"\",\"Offering End\":\"2026-07-03 00:00:00\",\"Offering Start\":\"2026-07-01 00:00:00\",\"Price\":\"900\",\"Refund Date\":\"\",\"Shares\":\"266,000,000\",\"board\":\"\",\"bureau_administration\":\"PT Adimitra Jasa Korpora\",\"underwriter\":[\"AZ - SUCOR SEKURITAS\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"900","shares":"266,000,000","percentage":"21.01","offering_start":"2026-07-01 00:00:00","offering_end":"2026-07-03 00:00:00","allotment_date":"","refund_date":"","price_formatted":"900","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":900}},{"ipo_id":"6829","company_symbol":"JECX","company_name":"PT Nitrasanata Dharma","ipo_listing_date":"2026-07-07","ipo_data":"{\"%\":\"15\",\"Allotment Date\":\"\",\"Offering End\":\"2026-07-03 00:00:00\",\"Offering Start\":\"2026-07-01 00:00:00\",\"Price\":\"1250\",\"Refund Date\":\"\",\"Shares\":\"487,983,500\",\"board\":\"\",\"bureau_administration\":\"PT Adimitra Jasa Korpora\",\"underwriter\":[\"LG - TRIMEGAH SEKURITAS INDONESIA TBK.\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"1,250","shares":"487,983,500","percentage":"15","offering_start":"2026-07-01 00:00:00","offering_end":"2026-07-03 00:00:00","allotment_date":"","refund_date":"","price_formatted":"1,250","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":1250}},{"ipo_id":"6821","company_symbol":"WBSA","company_name":"PT BSA Logistics Indonesia","ipo_listing_date":"2026-04-10","ipo_data":"{\"%\": \"20.75\", \"Price\": \"168\", \"board\": \"\", \"Shares\": \"1,800,000,000\", \"Refund Date\": \"\", \"underwriter\": [\"OCBC Sekuritas Indonesia\", \"Semesta Indovest Sekuritas\", \"Indo Capital Sekuritas\"], \"Offering End\": \"2026-04-08 00:00:00\", \"Allotment Date\": \"\", \"Offering Start\": \"2026-04-02 00:00:00\", \"bureau_administration\": \"PT Adimitra Jasa Korpora\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"168","shares":"1,800,000,000","percentage":"20.75","offering_start":"2026-04-02 00:00:00","offering_end":"2026-04-08 00:00:00","allotment_date":"","refund_date":"","price_formatted":"168","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":168}},{"ipo_id":"6816","company_symbol":"SUPA","company_name":"PT Super Bank Indonesia","ipo_listing_date":"2025-12-17","ipo_data":"{\"%\": \"13\", \"Price\": \"635\", \"board\": \"\", \"Shares\": \"4,406,612,300\", \"Refund Date\": \"\", \"underwriter\": [\"Mandiri Sekuritas\", \"CLSA Sekuritas Indonesia\", \"Trimegah Sekuritas Indonesia Tbk.\", \"Sucor Sekuritas\", \"Bahana Sekuritas\", \"Korea Investment and Sekuritas Indonesia\"], \"Offering End\": \"2025-12-15 00:00:00\", \"Allotment Date\": \"\", \"Offering Start\": \"2025-12-10 00:00:00\", \"bureau_administration\": \"PT Bima Registra\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"635","shares":"4,406,612,300","percentage":"13","offering_start":"2025-12-10 00:00:00","offering_end":"2025-12-15 00:00:00","allotment_date":"","refund_date":"","price_formatted":"635","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":635}},{"ipo_id":"6814","company_symbol":"RLCO","company_name":"PT Abadi Lestari Indonesia","ipo_listing_date":"2025-12-08","ipo_data":"{\"%\": \"20\", \"Price\": \"168\", \"board\": \"\", \"Shares\": \"625,000,000\", \"Refund Date\": \"\", \"underwriter\": [\"Samuel Sekuritas Indonesia\"], \"Offering End\": \"2025-12-04 00:00:00\", \"Allotment Date\": \"\", \"Offering Start\": \"2025-12-02 00:00:00\", \"bureau_administration\": \"PT Adimitra Jasa Korpora\"}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"168","shares":"625,000,000","percentage":"20","offering_start":"2025-12-02 00:00:00","offering_end":"2025-12-04 00:00:00","allotment_date":"","refund_date":"","price_formatted":"168","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":168}},{"ipo_id":"6811","company_symbol":"PJHB","company_name":"PT Pelayaran Jaya Hidup Baru","ipo_listing_date":"2025-11-06","ipo_data":"{\"%\":\"25\",\"Allotment Date\":\"\",\"Offering End\":\"2025-11-04 00:00:00\",\"Offering Start\":\"2025-10-31 00:00:00\",\"Price\":\"330\",\"Refund Date\":\"\",\"Shares\":\"480,000,000\",\"board\":\"\",\"bureau_administration\":\"PT Sinartama Gunita\",\"underwriter\":[\"Pilarmas Investindo Sekuritas\"]}","ipo_created":"","ipo_iqp_id":"","ipo_data_detail":{"price":"330","shares":"480,000,000","percentage":"25","offering_start":"2025-10-31 00:00:00","offering_end":"2025-11-04 00:00:00","allotment_date":"","refund_date":"","price_formatted":"330","listing_board":"","underwriter":[],"bureau_administration":""},"corp_action_active":false,"company_id":"0","ipo_price":{"minimum":0,"maximum":0,"final":330}}]},"message":"Successfully retrieved IPO data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex.json new file mode 100644 index 0000000..dab4693 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex.json @@ -0,0 +1 @@ +{"data":{"pubex":[{"company_id":"1000004517","company_symbol":"MEJA","corp_action_active":false,"puexp_date":"2026-11-24","puexp_id":"156830","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"13:00:00","puexp_venue":"Kantor PT Harta Djaya Karya Tbk, Jl. Bintaro Raya No. 8A, Kec. Kebayoran Lama, Jakarta Selatan 12240"},{"company_id":"450","company_symbol":"VIVA","corp_action_active":false,"puexp_date":"2026-11-17","puexp_id":"156841","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"15:30:00","puexp_venue":"The Convergence Indonesia, Lantai 28, Kawasan Rasuna Epicentrum, Jl. HR. Rasuna Said, Karet Kuningan, Setiabudi, Jakarta Selatan, 12940, Indonesia atau Live event melalui aplikasi Zoom https://bit.ly/OnlinePubExVivaMdia2026"},{"company_id":"512","company_symbol":"MDIA","corp_action_active":false,"puexp_date":"2026-11-17","puexp_id":"156840","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"15:30:00","puexp_venue":"The Convergence Indonesia, Lantai 28, Kawasan Rasuna Epicentrum, Jl. HR. Rasuna Said, Karet Kuningan, Setiabudi, Jakarta Selatan, 12940, Indonesia atau Live event melalui aplikasi Zoom https://bit.ly/OnlinePubExVivaMdia2026"},{"company_id":"1000006367","company_symbol":"WBSA","corp_action_active":false,"puexp_date":"2026-11-12","puexp_id":"156836","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Online/Virtual via Google Meet. Registrasi melalui link: https://bit.ly/WBSApubex2026"},{"company_id":"347","company_symbol":"PTRO","corp_action_active":false,"puexp_date":"2026-11-12","puexp_id":"156833","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"10:00:00","puexp_venue":"Zoom webinar dengan tautan registrasi: / zoom webinar with the following registration link: https://bit.ly/PTRO-PE2026"},{"company_id":"811","company_symbol":"INET","corp_action_active":false,"puexp_date":"2026-11-12","puexp_id":"156835","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Sarana Media Elektronik : Para peserta Public Expose dapat menggunakan tautan akses berikut: https://us06web.zoom.us/j/81220631598?pwd=sbmlCyRX6hIhjuye rU96H4Yi04ageE.1 Meeting ID : 812 2063 1598 Passcode : inet26"},{"company_id":"1000003697","company_symbol":"CHEM","corp_action_active":false,"puexp_date":"2026-11-12","puexp_id":"156832","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"10:00:00","puexp_venue":"Jl Industri ubrug 70 Purwakarta Desa Cibinong Jatiluhur Purwakarta, Jawa Barat 41152"},{"company_id":"78","company_symbol":"BLTA","corp_action_active":false,"puexp_date":"2026-11-12","puexp_id":"156831","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Live event melalui aplikasi Zoom"},{"company_id":"482","company_symbol":"ACST","corp_action_active":false,"puexp_date":"2026-11-12","puexp_id":"156837","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"11:00:00","puexp_venue":"Webinar Zoom Meeting"},{"company_id":"1000004015","company_symbol":"MENN","corp_action_active":true,"puexp_date":"2026-11-10","puexp_id":"156828","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"15:00:00","puexp_venue":"ONLINE"},{"company_id":"1000004498","company_symbol":"UNTD","corp_action_active":true,"puexp_date":"2026-11-09","puexp_id":"156794","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"13:00:00","puexp_venue":"United Bike Building, 2nd Floor (Alam Sutera Boulevard Kav. 12A, Pakulonan, Serpong Utara"},{"company_id":"304","company_symbol":"MTLA","corp_action_active":true,"puexp_date":"2026-11-09","puexp_id":"156825","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Zoom Webinar Perseroan"},{"company_id":"1000000084","company_symbol":"CITY","corp_action_active":true,"puexp_date":"2026-11-09","puexp_id":"156826","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Hotel Nawana, Sentul City, Kab Bogor"},{"company_id":"303","company_symbol":"MTFN","corp_action_active":true,"puexp_date":"2026-11-06","puexp_id":"156823","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"11:00:00","puexp_venue":"Kantor Taman E3.3, Unit D3A, Lantai 5 Jl. DR. Ide Anak Agung Gde Agung, Kuningan Jakarta Selatan"},{"company_id":"23","company_symbol":"AMAG","corp_action_active":true,"puexp_date":"2026-11-06","puexp_id":"156827","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"15:00:00","puexp_venue":"Virtual Zoom Webinar, di PT Asuransi Multi Artha Guna Tbk, Gedung TCC Batavia Tower One, Jalan KH Mas Mansyur Kav 126, Jakarta Pusat"},{"company_id":"46","company_symbol":"AUTO","corp_action_active":true,"puexp_date":"2026-11-05","puexp_id":"156821","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"10:00:00","puexp_venue":"Zoom Webinar"},{"company_id":"1000002677","company_symbol":"AMOR","corp_action_active":true,"puexp_date":"2026-11-04","puexp_id":"156820","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"17:00:00","puexp_venue":"Live Event Melalui aplikasi Zoom https://bit.ly/PUBEXASHMORE2026"},{"company_id":"416","company_symbol":"TFCO","corp_action_active":true,"puexp_date":"2026-11-03","puexp_id":"156838","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Online Google Meeting"},{"company_id":"349","company_symbol":"PTSP","corp_action_active":true,"puexp_date":"2026-11-03","puexp_id":"156839","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"10:00:00","puexp_venue":"Sarana media elektronik video conference melalui aplikasi Zoom Link : https://zoom.us/j/6732025385 Meeting ID : 673 202 5385 Passcode : iCLEAN"},{"company_id":"1000000033","company_symbol":"IPCC","corp_action_active":true,"puexp_date":"2026-11-03","puexp_id":"156818","puexp_iqp_id":"","puexp_lastupdate":"","puexp_time":"14:00:00","puexp_venue":"Ruang Inovation Room PT Indonesia Kendaraan Terminal Tbk, Jalan Sindang Laut No 100 Jakarta Utara"}]},"message":"Successfully retrieved Public Expose data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex_bbca.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex_bbca.json new file mode 100644 index 0000000..e8fe0d1 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_pubex_bbca.json @@ -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"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_reversesplit.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_reversesplit.json new file mode 100644 index 0000000..2afad37 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_reversesplit.json @@ -0,0 +1 @@ +{"data":{"stock_reverse":[]},"message":"Successfully retrieved reverse split data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rightissue.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rightissue.json new file mode 100644 index 0000000..48b2567 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rightissue.json @@ -0,0 +1 @@ +{"data":{"rightissue":[{"company_id":"492","company_symbol":"BMAS","corp_action_active":false,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-11-23","rightissue_exdate":"2026-11-24","rightissue_factor":"1.15878","rightissue_id":"11572","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"104","rightissue_new_share":0,"rightissue_old":"655","rightissue_price":350,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"655 : 104","rightissue_recdate":"2026-11-25","rightissue_subdate":"","rightissue_trading_end":"2026-12-03","rightissue_trading_start":"2026-11-27","event_note":"","rightissue_price_formatted":"350","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0},{"company_id":"1000003328","company_symbol":"MGLV","corp_action_active":true,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-11-04","rightissue_exdate":"2026-11-05","rightissue_factor":"1.15","rightissue_id":"11583","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"15","rightissue_new_share":0,"rightissue_old":"100","rightissue_price":8880,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"100 : 15","rightissue_recdate":"2026-11-06","rightissue_subdate":"","rightissue_trading_end":"2026-11-23","rightissue_trading_start":"2026-11-10","event_note":"","rightissue_price_formatted":"8,880","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0},{"company_id":"105","company_symbol":"BUVA","corp_action_active":false,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-10-13","rightissue_exdate":"2026-10-14","rightissue_factor":"1.25","rightissue_id":"11582","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"1","rightissue_new_share":0,"rightissue_old":"4","rightissue_price":250,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"4 : 1","rightissue_recdate":"2026-10-15","rightissue_subdate":"","rightissue_trading_end":"2026-10-30","rightissue_trading_start":"2026-10-19","event_note":"","rightissue_price_formatted":"250","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0},{"company_id":"92","company_symbol":"BRNA","corp_action_active":false,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-10-08","rightissue_exdate":"2026-10-09","rightissue_factor":"1.5556","rightissue_id":"11581","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"5","rightissue_new_share":0,"rightissue_old":"9","rightissue_price":685,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"9 : 5","rightissue_recdate":"2026-10-12","rightissue_subdate":"","rightissue_trading_end":"2026-10-21","rightissue_trading_start":"2026-10-14","event_note":"","rightissue_price_formatted":"685","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0},{"company_id":"12","company_symbol":"AHAP","corp_action_active":false,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-10-08","rightissue_exdate":"2026-10-09","rightissue_factor":"1.7143","rightissue_id":"11580","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"5","rightissue_new_share":0,"rightissue_old":"7","rightissue_price":50,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"7 : 5","rightissue_recdate":"2026-10-12","rightissue_subdate":"","rightissue_trading_end":"2026-10-20","rightissue_trading_start":"2026-10-14","event_note":"","rightissue_price_formatted":"50","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0},{"company_id":"1000004126","company_symbol":"VKTR","corp_action_active":false,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-10-06","rightissue_exdate":"2026-10-07","rightissue_factor":"1.3429","rightissue_id":"11578","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"12","rightissue_new_share":0,"rightissue_old":"35","rightissue_price":200,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"35 : 12","rightissue_recdate":"2026-10-08","rightissue_subdate":"","rightissue_trading_end":"2026-10-23","rightissue_trading_start":"2026-10-12","event_note":"","rightissue_price_formatted":"200","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0},{"company_id":"151","company_symbol":"ENRG","corp_action_active":false,"rightissue_adj_factor":0,"rightissue_created":"","rightissue_cumdate":"2026-10-02","rightissue_exdate":"2026-10-05","rightissue_factor":"1.5","rightissue_id":"11577","rightissue_iqp_id":"","rightissue_lastupdate":"","rightissue_lock":0,"rightissue_new":"1","rightissue_new_share":0,"rightissue_old":"2","rightissue_price":310,"rightissue_price_adj":0,"rightissue_price_factor":0,"rightissue_ratio":"2 : 1","rightissue_recdate":"2026-10-06","rightissue_subdate":"","rightissue_trading_end":"2026-10-21","rightissue_trading_start":"2026-10-08","event_note":"","rightissue_price_formatted":"310","rightissue_foreign_percentage":0,"rightissue_local_percentage":0,"rightissue_number_of_securities":0,"rightissue_company_id":"0","rightissue_total":0}]},"message":"Successfully retrieved rights issue data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups.json new file mode 100644 index 0000000..8054f90 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups.json @@ -0,0 +1 @@ +{"data":{"rups":[{"company_id":"35","company_symbol":"ARTI","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-12-09","rups_id":"1461637","rups_time":"10: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":"Gedung Ratu Prabu 1 Jl. TB. Simatupang Kav. 20 Jakarta.","rups_eligible_date":"2026-11-12","company_name":"","company_icon_url":""},{"company_id":"1000004211","company_symbol":"MSIE","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-20","rups_id":"1461587","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":"IPC Room, IPH Schools 2nd Floor, Jl. Raya Kedung Baruk 112-114 Surabaya","rups_eligible_date":"2026-10-26","company_name":"","company_icon_url":""},{"company_id":"32","company_symbol":"ARII","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-20","rups_id":"1461582","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":"Jl. Kemang Raya No. 43 Jakarta Selatan diumumkan dan sesuai iklan)","rups_eligible_date":"2026-10-26","company_name":"","company_icon_url":""},{"company_id":"38","company_symbol":"ASGR","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-20","rups_id":"1461631","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":"Mandarin Oriental Hotel - Ballroom, di Jl. M.H. Thamrin, RT.1/RW.5, Menteng, Kec. Menteng, Kota Jakarta Pusat, Daerah Kh","rups_eligible_date":"2026-10-26","company_name":"","company_icon_url":""},{"company_id":"624","company_symbol":"MARK","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-19","rups_id":"1461579","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":"Wing Hotel Kualanamu diumumkan dan sesuai iklan)","rups_eligible_date":"2026-10-23","company_name":"","company_icon_url":""},{"company_id":"644","company_symbol":"GMFI","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-19","rups_id":"1461624","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":"Ruang Auditorium, Gedung Manajemen Garuda, Lantai Dasar, Garuda City, Area Perkantoran Bandar Udara International Soekar","rups_eligible_date":"2026-10-23","company_name":"","company_icon_url":""},{"company_id":"168","company_symbol":"GDYR","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-19","rups_id":"1461581","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":"Ruang Edelweis, Hotel Grand Savero, Bogor, Jl. Padjajaran No 27, Bogor.","rups_eligible_date":"2026-10-23","company_name":"","company_icon_url":""},{"company_id":"1000003658","company_symbol":"ASHA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-19","rups_id":"1461580","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":"Gedung PT Sutioso Bersaudara Lantai 3 Jl Tuna II Blok D 2, RT.20/RW.17, Penjaringan, Kec. Penjaringan, Jkt Utara, Daerah","rups_eligible_date":"2026-10-23","company_name":"","company_icon_url":""},{"company_id":"1000003819","company_symbol":"NINE","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-18","rups_id":"1461577","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":"HARRIS Suites Puri Mansion Puri Mansion Estate, Jl. Puri Lkr. Luar, RT.13/RW.2, Duri Kosambi, Kec. Kembangan, Kota Jakar","rups_eligible_date":"2026-10-22","company_name":"","company_icon_url":""},{"company_id":"181","company_symbol":"GTBO","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-18","rups_id":"1461622","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":"Ballroom Krakatau Permata Kuningan Building Lantai 2 Jl. Kuningan Mulia Kav. 9C, Guntur, Jakarta 12960, Indonesia","rups_eligible_date":"2026-10-22","company_name":"","company_icon_url":""},{"company_id":"1000003334","company_symbol":"TRUE","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-18","rups_id":"1461578","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":"Kota Tangerang, Banten diumumkan dan sesuai iklan)","rups_eligible_date":"2026-10-22","company_name":"","company_icon_url":""},{"company_id":"1000003365","company_symbol":"FLMC","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-18","rups_id":"1461575","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":"Artotel Suites Aquila, Jl. Dr Djunjunan No 116 Bandung","rups_eligible_date":"2026-10-22","company_name":"","company_icon_url":""},{"company_id":"207","company_symbol":"INCO","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-18","rups_id":"1461574","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":"Sudirman Hall, Sequis Tower, Level 2, Jl. Jend. Sudirman Kav. 71, Jakarta 12190, Indonesia","rups_eligible_date":"2026-10-22","company_name":"","company_icon_url":""},{"company_id":"390","company_symbol":"SMMA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-17","rups_id":"1461567","rups_time":"16: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":"The Grand Mansion, Jalan Borobudur Nomor 10, Pengangsaan , Menteng, Jakarta Pusat 10320","rups_eligible_date":"2026-10-21","company_name":"","company_icon_url":""},{"company_id":"169","company_symbol":"GEMA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-17","rups_id":"1461568","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":"SOUTH78 JL BOULEVARD GADING SERPONG BLOK O NO 7-8 MEDANG PEGEDANGAN TANGERANG","rups_eligible_date":"2026-10-21","company_name":"","company_icon_url":""},{"company_id":"450","company_symbol":"VIVA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-17","rups_id":"1461570","rups_time":"13: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":"The Convergence Indonesia, Lantai 28, Kawasan Rasuna Epicentrum, Jl. HR. Rasuna Said, Karet Kuningan, Setiabudi, Jakarta","rups_eligible_date":"2026-10-21","company_name":"","company_icon_url":""},{"company_id":"512","company_symbol":"MDIA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-17","rups_id":"1461569","rups_time":"11: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":"The Convergence Indonesia, Lantai 28, Kawasan Rasuna Epicentrum, Jl. HR. Rasuna Said, Karet Kuningan, Setiabudi, Jakarta","rups_eligible_date":"2026-10-21","company_name":"","company_icon_url":""},{"company_id":"1000004142","company_symbol":"AMMN","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-17","rups_id":"1461571","rups_time":"10: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":"Mengakses fasilitas Electronic General Meeting System pada platform eASY.KSEI","rups_eligible_date":"2026-10-21","company_name":"","company_icon_url":""},{"company_id":"385","company_symbol":"SMAR","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-17","rups_id":"1461572","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":"Ruang Danamas, Plaza Sinar Mas Land, Menara 2, Lantai 39 Jalan MH. Thamrin No. 51, Jakarta Pusat 10350","rups_eligible_date":"2026-10-21","company_name":"","company_icon_url":""},{"company_id":"429","company_symbol":"TOTL","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-16","rups_id":"1461565","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":"PT Total Bangun Persada Tbk, Jakarta dan secara elektronik melalui eASY.KSEI Kehadiran fisik yaitu hanya untuk profesi p","rups_eligible_date":"2026-10-20","company_name":"","company_icon_url":""},{"company_id":"1000003200","company_symbol":"FAPA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-16","rups_id":"1461563","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":"Gedung Gold Coast, Tower Liberty Lt. 16 A- H, Jl Pantai Indah Kapuk Kelurahan Kamal Muara Kecamatan Penjaringan, Jakarta","rups_eligible_date":"2026-10-20","company_name":"","company_icon_url":""},{"company_id":"111","company_symbol":"CENT","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-13","rups_id":"1461559","rups_time":"15: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":"Ruang Meeting Perseroan, TCC Batavia Tower One 16th Jl. KH. Mas Mansyur Kav. 126 Jakarta Pusat DKI Jakarta 10220 Indones","rups_eligible_date":"2026-10-19","company_name":"","company_icon_url":""},{"company_id":"7","company_symbol":"ADHI","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-13","rups_id":"1461561","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":"ADHI Tower, Jl. M.T. Haryono Kavling 27, Cawang, Jakarta Timur","rups_eligible_date":"2026-10-19","company_name":"","company_icon_url":""},{"company_id":"1000003481","company_symbol":"ADCP","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-13","rups_id":"1461562","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":"Kantor Pusat ADHI Jalan Raya Pasar Minggu KM 18 Jakarta Selatan 18510","rups_eligible_date":"2026-10-19","company_name":"","company_icon_url":""},{"company_id":"1000003328","company_symbol":"MGLV","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-12","rups_id":"1461557","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":"Function Hall Pacific Century Place Lantai B1 Room B Jl. Jend. Sudirman Kav. 52-53, Senayan, Kebayoran Baru, Jakarta Sel","rups_eligible_date":"2026-10-16","company_name":"","company_icon_url":""},{"company_id":"1000003422","company_symbol":"IDEA","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-12","rups_id":"1461551","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":"Aleesha Venue, Jl. Warung Sila No. 1, RT.001/RW.005, Cipedak, Kec. Jagakarsa, Kota Jakarta Selatan, Jakarta 12630","rups_eligible_date":"2026-10-16","company_name":"","company_icon_url":""},{"company_id":"1000003821","company_symbol":"KETR","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-12","rups_id":"1461538","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":"Secara Fisik Rosewood Room, Royal Kuningan Hotel Jl. Kuningan Persada No.2, Kelurahan Guntur, Kecamatan Setiabudi, Jaka","rups_eligible_date":"2026-10-08","company_name":"","company_icon_url":""},{"company_id":"1000003894","company_symbol":"BMBL","corp_action_active":false,"rups_created":"","rups_datahash":"","rups_date":"2026-11-11","rups_id":"1461588","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":"Hotel Daima Suites Margonda diumumkan dan sesuai iklan)","rups_eligible_date":"2026-10-15","company_name":"","company_icon_url":""},{"company_id":"1000003621","company_symbol":"WIRG","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-10","rups_id":"1461640","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":"WANG Plaza (Hall of Grace 2 Lantai 2) Jalan Panjang Kav. 17, Kedoya Utara, Kebon Jeruk, Jakarta Barat 11520","rups_eligible_date":"2026-10-28","company_name":"","company_icon_url":""},{"company_id":"361","company_symbol":"RIGS","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-10","rups_id":"1461554","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":"Generali Tower, Gran Rubina Business Park 19th Floor Unit C, Kawasan Rasuna Epicentrum, Jalan H.R. Rasuna Said, Keluraha","rups_eligible_date":"2026-10-14","company_name":"","company_icon_url":""},{"company_id":"1000004066","company_symbol":"DOOH","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-09","rups_id":"1461552","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":"Ruang Meeting Slipi, Hotel Mercure Batavia Jakarta, Jl. Kali Besar Barat No.44-46, Roa Malaka, Kec. Tambora, Jakarta Bar","rups_eligible_date":"2026-10-13","company_name":"","company_icon_url":""},{"company_id":"1000002301","company_symbol":"ITIC","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-09","rups_id":"1461548","rups_time":"11: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":"Teak Room 9th Floor at Mercure Hotel Jakarta Pantai Indah Kapuk, PIK Avenue Mall, Pantai Indah Kapuk Boulevard, Jakarta","rups_eligible_date":"2026-10-13","company_name":"","company_icon_url":""},{"company_id":"1000004498","company_symbol":"UNTD","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-09","rups_id":"1461549","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":"United Building (Grand Hall Lantai 2), Jalan Boulevard Alam Sutera No. 12 A, Pakulonan, Kec. Serpong Utara, Kota Tangera","rups_eligible_date":"2026-10-13","company_name":"","company_icon_url":""},{"company_id":"447","company_symbol":"UNTR","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-09","rups_id":"1461553","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":"United Tractors Grand Ballroom, Jl. Raya Bekasi Km 22, Cakung Barat, Kecamatan Cakung, Kota Jakarta Timur, DKI Jakarta 1","rups_eligible_date":"2026-10-13","company_name":"","company_icon_url":""},{"company_id":"1000003820","company_symbol":"KDTN","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-06","rups_id":"1461546","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":"PT Puri Sentul Permai Tbk diumumkan dan sesuai iklan)","rups_eligible_date":"2026-10-12","company_name":"","company_icon_url":""},{"company_id":"5035","company_symbol":"INPS","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-06","rups_id":"1461545","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":"Gedung Inprase, Jl. Sunter Garden Raya Blok D8 No. 3G-3H, Sunter Agung, Tanjung Priok, Jakarta Utara, DKI Jakarta 14350","rups_eligible_date":"2026-10-07","company_name":"","company_icon_url":""},{"company_id":"172","company_symbol":"GIAA","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-06","rups_id":"1461547","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":"Secara elektronik melalui sistem eASY.KSEI (https//akses.ksei.co.id/)","rups_eligible_date":"2026-10-12","company_name":"","company_icon_url":""},{"company_id":"443","company_symbol":"ULTJ","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-06","rups_id":"1461528","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":"Kantor Perseroan, Jalan Raya Cimareme No 131 Bandung Barat, Jawa Barat Indonesia","rups_eligible_date":"2026-08-31","company_name":"","company_icon_url":""},{"company_id":"303","company_symbol":"MTFN","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-06","rups_id":"1461544","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":"Kantor Taman E3.3, Unit D3A, Lantai 5, Jl. DR. Ide Anak Agung Gde Agung, Kuningan Jakarta Selatan","rups_eligible_date":"2026-10-12","company_name":"","company_icon_url":""},{"company_id":"1000000005","company_symbol":"BRIS","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-06","rups_id":"1461596","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":"Jakarta Pusat, melalui fasilitas Electronic General Meeting System KSEI (eASY.KSEI) dalam tautan https//akses.ksei.co.id","rups_eligible_date":"2026-10-12","company_name":"","company_icon_url":""},{"company_id":"52","company_symbol":"BATA","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-05","rups_id":"1461542","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":"Gedung Ventura, lantai 7. Jl. Raden Ajeng Kartini No.26, Cilandak Barat, Jakarta Selatan","rups_eligible_date":"2026-10-09","company_name":"","company_icon_url":""},{"company_id":"1000002677","company_symbol":"AMOR","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-04","rups_id":"1461539","rups_time":"16: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":"Secara online melalui fasilitas Electronic General Meeting System KSEI (eASY.KSEI) dalam tautan https//akses.ksei.co.id","rups_eligible_date":"2026-10-08","company_name":"","company_icon_url":""},{"company_id":"358","company_symbol":"RDTX","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-04","rups_id":"1461537","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":"RDTX SQUARE PODIUM LANTAI 2 JL. PROF. DR. SATRIO NO. 164, JAKARTA SELATAN 12930","rups_eligible_date":"2026-10-08","company_name":"","company_icon_url":""},{"company_id":"1000005447","company_symbol":"MINE","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-04","rups_id":"1461583","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":"The Function Room (Ruang Anggrek 5) Lantai 3A, North Tower, Sampoerna Strategic Square, Jl. Jenderal Sudirman No.45 - 46","rups_eligible_date":"2026-10-08","company_name":"","company_icon_url":""},{"company_id":"5028","company_symbol":"BOSS","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-04","rups_id":"1461634","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":"Wisma 77, Tower I, Lt 8, Jl Letjend S Parman, Slipi, Jakarta Barat","rups_eligible_date":"2026-10-02","company_name":"","company_icon_url":""},{"company_id":"1000002176","company_symbol":"MTPS","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-03","rups_id":"1461612","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":"Meta Epsi Building Jl. Mayjen D.I Panjaitan Kav. 2, Rawa Bunga, Jatinegara - Jakarta Timur 13350","rups_eligible_date":"2026-10-22","company_name":"","company_icon_url":""},{"company_id":"1000002971","company_symbol":"EPAC","corp_action_active":true,"rups_created":"","rups_datahash":"","rups_date":"2026-11-03","rups_id":"1461536","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":"IBIS Style Jakarta Tanah Abang, Jl. Fachrudin No. 22, Kp. Bali, Tanah Abang, Jakarta Pusat 10250","rups_eligible_date":"2026-10-07","company_name":"","company_icon_url":""}]},"message":"Successfully retrieved RUPS/Shareholders meeting data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups_bbca.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups_bbca.json new file mode 100644 index 0000000..80fbfce --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_rups_bbca.json @@ -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"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stock_dividend.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stock_dividend.json new file mode 100644 index 0000000..11eb9e0 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stock_dividend.json @@ -0,0 +1 @@ +{"data":{"stock_dividend":[]},"message":"Successfully retrieved stock dividend data"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit.json new file mode 100644 index 0000000..a1b3346 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit.json @@ -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"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit_bbca.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit_bbca.json new file mode 100644 index 0000000..ecbd334 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_stocksplit_bbca.json @@ -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"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_tenderoffer.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_tenderoffer.json new file mode 100644 index 0000000..8d6c778 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_tenderoffer.json @@ -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"} diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_warrant.json b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_warrant.json new file mode 100644 index 0000000..04c3ecc --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_corpaction_warrant.json @@ -0,0 +1 @@ +{"data":{"warrant":[{"company_id":"92","company_symbol":"BRNA","corp_action_active":true,"wrant_exc_end":"2029-10-15","wrant_exc_from":"2027-04-16","wrant_exc_price":"800","wrant_id":"3570","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2029-10-10","wrant_trading_from":"2026-10-16","event_note":"","wrant_exc_price_formatted":"800","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007050","company_symbol":"ACESYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"3000","wrant_id":"3586","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"3,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007051","company_symbol":"AKRAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3587","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007052","company_symbol":"ARTOYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3588","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007053","company_symbol":"ASIIYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"12000","wrant_id":"3589","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"12,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007054","company_symbol":"CUANYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3590","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007055","company_symbol":"DEWAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3591","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007056","company_symbol":"ELSAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3592","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007057","company_symbol":"EMTKYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3593","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007058","company_symbol":"ERAAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3594","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007059","company_symbol":"ESSAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3595","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007060","company_symbol":"EXCLYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3596","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007061","company_symbol":"HRTAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3597","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007062","company_symbol":"INKPYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"30000","wrant_id":"3598","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"30,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007063","company_symbol":"JPFAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3599","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007064","company_symbol":"MAPIYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3600","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007065","company_symbol":"NCKLYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3601","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007066","company_symbol":"PGASYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"5000","wrant_id":"3602","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"5,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007067","company_symbol":"TAPGYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"10000","wrant_id":"3603","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"10,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007068","company_symbol":"TLKMYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"8000","wrant_id":"3604","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"8,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007069","company_symbol":"TPIAYUCK7A","corp_action_active":true,"wrant_exc_end":"2027-05-28","wrant_exc_from":"2026-10-14","wrant_exc_price":"15000","wrant_id":"3605","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-05-25","wrant_trading_from":"2026-10-14","event_note":"","wrant_exc_price_formatted":"15,000","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007032","company_symbol":"ARTOZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"900","wrant_id":"3571","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"900","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007033","company_symbol":"ASIIZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"5094","wrant_id":"3572","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"5,094","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007034","company_symbol":"BRMSZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"700","wrant_id":"3573","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"700","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007035","company_symbol":"BRPTZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1700","wrant_id":"3574","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,700","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007036","company_symbol":"CTRAZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"600","wrant_id":"3575","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"600","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007037","company_symbol":"CUANZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1100","wrant_id":"3576","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,100","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007038","company_symbol":"EMTKZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"300","wrant_id":"3577","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"300","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007039","company_symbol":"ENRGZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1300","wrant_id":"3578","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,300","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007040","company_symbol":"HRUMZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1050","wrant_id":"3579","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,050","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007041","company_symbol":"KIJAZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"190","wrant_id":"3580","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"190","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007042","company_symbol":"MDKAZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"3300","wrant_id":"3581","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"3,300","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007043","company_symbol":"MEDCZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1700","wrant_id":"3582","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,700","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007044","company_symbol":"NCKLZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1050","wrant_id":"3583","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,050","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007045","company_symbol":"TOWRZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"390","wrant_id":"3584","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"390","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"},{"company_id":"1000007046","company_symbol":"WIFIZPCN7A","corp_action_active":true,"wrant_exc_end":"2027-07-30","wrant_exc_from":"2026-10-09","wrant_exc_price":"1600","wrant_id":"3585","wrant_iqp_id":"","wrant_lastupdate":"","wrant_serie":"","wrant_total":"","wrant_trading_end":"2027-07-27","wrant_trading_from":"2026-10-09","event_note":"","wrant_exc_price_formatted":"1,600","wrant_foreign_percentage":0,"wrant_local_percentage":0,"wrant_number_of_securities":0,"wrant_company_id":"0"}]},"message":"Successfully retrieved warrant data"} diff --git a/crates/idx-mcp/src/params.rs b/crates/idx-mcp/src/params.rs index 50c4d42..3142955 100644 --- a/crates/idx-mcp/src/params.rs +++ b/crates/idx-mcp/src/params.rs @@ -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, } +#[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>, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct CorporateCalendarParams { + /// First day, YYYY-MM-DD (default today). + #[serde(default)] + pub from: Option, + /// Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days). + #[serde(default)] + pub to: Option, + /// Kinds to list (default: every kind). Each kind is one request to Stockbit. + #[serde(default)] + #[schemars(schema_with = "action_kinds_schema")] + pub kinds: Option>, +} + +/// Parses `kinds`, or `default` when absent. +pub fn action_kinds( + kinds: Option>, + default: &[ActionKind], +) -> Result, 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()) } diff --git a/crates/idx-mcp/src/server.rs b/crates/idx-mcp/src/server.rs index 3b9b4a0..bcf375c 100644 --- a/crates/idx-mcp/src/server.rs +++ b/crates/idx-mcp/src/server.rs @@ -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, + ) -> 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, + ) -> 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`: \ diff --git a/crates/idx-mcp/tests/golden/tools.json b/crates/idx-mcp/tests/golden/tools.json index 51ce573..b4a8878 100644 --- a/crates/idx-mcp/tests/golden/tools.json +++ b/crates/idx-mcp/tests/golden/tools.json @@ -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": { diff --git a/crates/idx-mcp/tests/mcp.rs b/crates/idx-mcp/tests/mcp.rs index 94321e6..5274ffc 100644 --- a/crates/idx-mcp/tests/mcp.rs +++ b/crates/idx-mcp/tests/mcp.rs @@ -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); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 158163d..0997cd4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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` + (`:` or `: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 intraday_provider() → Box orderbook_provider() → Box + corporate_action_provider() → Box ``` `ProviderKind::Stockbit` serves quotes (and, with a session, daily history): diff --git a/docs/STOCKBIT.md b/docs/STOCKBIT.md index c2f14aa..037de85 100644 --- a/docs/STOCKBIT.md +++ b/docs/STOCKBIT.md @@ -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. diff --git a/web/src/content/docs/cli/reference.md b/web/src/content/docs/cli/reference.md index 46a4df4..e02f3a4 100644 --- a/web/src/content/docs/cli/reference.md +++ b/web/src/content/docs/cli/reference.md @@ -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] + +Arguments: + + Single ticker symbol (e.g. BBCA) + +Options: + --kind ... + 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 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 + 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 + First day, YYYY-MM-DD. Default: today + + -o, --output + Output format [default: auto = table on a terminal, json when piped] + + [possible values: auto, table, json, jsonl] + + --no-color + Disable colored output + + --to + Last day, YYYY-MM-DD. Default: 30 days after --from (at most 366) + + --kind ... + 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 + 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 diff --git a/web/src/content/docs/cli/stockbit.md b/web/src/content/docs/cli/stockbit.md index 4ab02a0..5c54f8f 100644 --- a/web/src/content/docs/cli/stockbit.md +++ b/web/src/content/docs/cli/stockbit.md @@ -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. diff --git a/web/src/content/docs/cli/stocks.md b/web/src/content/docs/cli/stocks.md index 3a9ecb5..634dfb6 100644 --- a/web/src/content/docs/cli/stocks.md +++ b/web/src/content/docs/cli/stocks.md @@ -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 | diff --git a/web/src/content/docs/mcp/index.md b/web/src/content/docs/mcp/index.md index 81e8628..d73870a 100644 --- a/web/src/content/docs/mcp/index.md +++ b/web/src/content/docs/mcp/index.md @@ -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), diff --git a/web/src/content/docs/mcp/tools.md b/web/src/content/docs/mcp/tools.md index ca4c98a..19e1260 100644 --- a/web/src/content/docs/mcp/tools.md +++ b/web/src/content/docs/mcp/tools.md @@ -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`.