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

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

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

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

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

View file

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