mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat: corporate actions from the Stockbit session (stocks actions, stocks calendar)
`idx stocks actions SYM [--kind ...]`: one stock's cash and stock
dividends, bonus shares, rights, warrants, splits and reverse splits,
meetings, public exposes and tender offers (`--kind ipo` adds its IPO),
newest first. `idx stocks calendar [--from] [--to] [--kind ...]`: the
market's actions dated in the range (default today and 30 days ahead),
oldest first. MCP: get_corporate_actions, get_corporate_calendar.
Stockbit's `/corpaction/{type}` serves one kind per request; each type has
its own field prefix and list key, so entitlements are read by field
suffix. An action's `date` is the cum date for entitlements, else the
event's first day. Dividends keep their decimals. One request per kind,
cached 6 hours. AUTHERROR without a session.
Fixtures are the 2026-11-03 recordings (market-wide ones trimmed to
October-December). Goldens added; help-stocks and completions-bash gain
the two subcommands, MCP tools.json the two tools (regenerated with
UPDATE_GOLDEN=1). The MCP docs counted 17 tools without stockbit_login;
now 20.
Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY
This commit is contained in:
parent
8a1b29043c
commit
43ff4aa433
55 changed files with 2026 additions and 34 deletions
|
|
@ -4,6 +4,7 @@
|
|||
use idx_core::analysis::bandar::TIERS;
|
||||
use idx_core::api::types::{Interval, Period};
|
||||
use idx_core::broker::types::{BrokerBoard, BrokerInvestor};
|
||||
use idx_core::corporate::types::ActionKind;
|
||||
use idx_core::flow::types::FlowSource;
|
||||
use idx_core::market::{ScreenFilter, ScreenRegion};
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
|
|
@ -228,6 +229,41 @@ pub struct FlowParams {
|
|||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct CorporateActionsParams {
|
||||
/// IDX ticker, e.g. "BBCA".
|
||||
pub symbol: String,
|
||||
/// Kinds to list (default: every kind but "ipo"). Each kind is one request to Stockbit.
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "action_kinds_schema")]
|
||||
pub kinds: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct CorporateCalendarParams {
|
||||
/// First day, YYYY-MM-DD (default today).
|
||||
#[serde(default)]
|
||||
pub from: Option<String>,
|
||||
/// Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days).
|
||||
#[serde(default)]
|
||||
pub to: Option<String>,
|
||||
/// Kinds to list (default: every kind). Each kind is one request to Stockbit.
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "action_kinds_schema")]
|
||||
pub kinds: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Parses `kinds`, or `default` when absent.
|
||||
pub fn action_kinds(
|
||||
kinds: Option<Vec<String>>,
|
||||
default: &[ActionKind],
|
||||
) -> Result<Vec<ActionKind>, idx_core::error::IdxError> {
|
||||
match kinds {
|
||||
None => Ok(default.to_vec()),
|
||||
Some(kinds) => kinds.iter().map(|k| k.parse()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct ShareholdersParams {
|
||||
/// IDX ticker, e.g. "BBCA".
|
||||
|
|
@ -305,6 +341,11 @@ fn flow_source_schema(_: &mut SchemaGenerator) -> Schema {
|
|||
string_enum(FlowSource::ALL.map(FlowSource::as_str).to_vec())
|
||||
}
|
||||
|
||||
fn action_kinds_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
let kinds: Vec<&str> = ActionKind::ALL.iter().map(|k| k.as_str()).collect();
|
||||
json_schema!({ "type": "array", "items": { "type": "string", "enum": kinds } })
|
||||
}
|
||||
|
||||
fn login_method_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
string_enum(LoginMethod::ALL.map(LoginMethod::as_str).to_vec())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use idx_core::analysis::fundamental::FundamentalReport;
|
|||
use idx_core::api::stockbit::{self, signin};
|
||||
use idx_core::broker::types::BrokerRange;
|
||||
use idx_core::config::CoreConfig;
|
||||
use idx_core::corporate::types::{ActionKind, CalendarRange};
|
||||
use idx_core::error::IdxError;
|
||||
use idx_core::financial::{EarningsFilter, StatementFilter};
|
||||
use idx_core::flow::types::FlowRange;
|
||||
|
|
@ -27,9 +28,9 @@ use tokio::sync::Semaphore;
|
|||
|
||||
use crate::params::{
|
||||
AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
|
||||
CompanySection, EntityParams, FlowParams, HistoryParams, IntradayParams, LoginMethod,
|
||||
LoginParams, OrderBookParams, ScreenParams, SearchParams, ShareholdersParams, SymbolsParams,
|
||||
SyncParams,
|
||||
CompanySection, CorporateActionsParams, CorporateCalendarParams, EntityParams, FlowParams,
|
||||
HistoryParams, IntradayParams, LoginMethod, LoginParams, OrderBookParams, ScreenParams,
|
||||
SearchParams, ShareholdersParams, SymbolsParams, SyncParams, action_kinds,
|
||||
};
|
||||
|
||||
const SYNC_TOOL: &str = "ownership_sync";
|
||||
|
|
@ -352,6 +353,54 @@ average price, the close, `change_bps` and `non_regular_bps`."
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Corporate actions of one IDX stock from the user's own Stockbit \
|
||||
session, newest first: `kind` (dividend, stock-dividend, bonus, rights, warrant, split, \
|
||||
reverse-split, meeting, expose, ipo, tender), `date` (the cum date for dividends, bonuses, \
|
||||
rights and splits, i.e. the last day to buy and be entitled; else the event's first day), \
|
||||
`ex_date`, `record_date`, `pay_date`, and per kind `cash_per_share` (IDR, may have decimals), \
|
||||
`ratio` {old, new} (1:5 split = 1 share becomes 5; 655:104 rights = 104 rights per 655 held), \
|
||||
`factor` (price adjustment), `price` (rights, warrant exercise, tender or IPO price, IDR), \
|
||||
`period` and `exercise` {start, end}, `shares`, `percentage_bps`, `time`, `venue`. Absent \
|
||||
fields are omitted. One Stockbit request per kind (about 1 s each on a cold cache, then cached \
|
||||
6 hours). AUTHERROR means there is no session: call stockbit_status, then stockbit_login."
|
||||
)]
|
||||
async fn get_corporate_actions(
|
||||
&self,
|
||||
params: Parameters<CorporateActionsParams>,
|
||||
) -> ToolResult {
|
||||
let CorporateActionsParams { symbol, kinds } = params.0;
|
||||
self.live(move |idx| {
|
||||
let kinds = action_kinds(kinds, &ActionKind::STOCK_DEFAULT)?;
|
||||
idx.corporate()
|
||||
.actions(&symbol, &kinds, CachePolicy::Default)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Corporate actions across IDX from the user's own Stockbit session, \
|
||||
oldest first, dated from `from` to `to` (default today and the next 30 days): upcoming cum \
|
||||
dates, meetings, exposes, rights, warrants, IPOs and tender offers, and the past year. Each \
|
||||
item has the same fields as get_corporate_actions, with `symbol`. One Stockbit request per \
|
||||
kind (about 1 s each on a cold cache, then cached 6 hours); pass `kinds` to ask for less. \
|
||||
AUTHERROR means there is no session: call stockbit_status, then stockbit_login."
|
||||
)]
|
||||
async fn get_corporate_calendar(
|
||||
&self,
|
||||
params: Parameters<CorporateCalendarParams>,
|
||||
) -> ToolResult {
|
||||
let CorporateCalendarParams { from, to, kinds } = params.0;
|
||||
self.live(move |idx| {
|
||||
let corporate = idx.corporate();
|
||||
let range =
|
||||
CalendarRange::from_args(from.as_deref(), to.as_deref(), corporate.today())?;
|
||||
let kinds = action_kinds(kinds, &ActionKind::ALL)?;
|
||||
corporate.calendar(range, &kinds, CachePolicy::Default)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Company data for one IDX ticker: profile, financial statements, \
|
||||
earnings, analyst sentiment, insights, and news. News items carry `relevance`: \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue