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
|
|
@ -235,6 +235,91 @@
|
|||
},
|
||||
"name": "get_company"
|
||||
},
|
||||
{
|
||||
"description": "Corporate actions of one IDX stock from the user's own Stockbit session, newest first: `kind` (dividend, stock-dividend, bonus, rights, warrant, split, reverse-split, meeting, expose, ipo, tender), `date` (the cum date for dividends, bonuses, rights and splits, i.e. the last day to buy and be entitled; else the event's first day), `ex_date`, `record_date`, `pay_date`, and per kind `cash_per_share` (IDR, may have decimals), `ratio` {old, new} (1:5 split = 1 share becomes 5; 655:104 rights = 104 rights per 655 held), `factor` (price adjustment), `price` (rights, warrant exercise, tender or IPO price, IDR), `period` and `exercise` {start, end}, `shares`, `percentage_bps`, `time`, `venue`. Absent fields are omitted. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours). AUTHERROR means there is no session: call stockbit_status, then stockbit_login.",
|
||||
"inputSchema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"kinds": {
|
||||
"default": null,
|
||||
"description": "Kinds to list (default: every kind but \"ipo\"). Each kind is one request to Stockbit.",
|
||||
"items": {
|
||||
"enum": [
|
||||
"dividend",
|
||||
"stock-dividend",
|
||||
"bonus",
|
||||
"rights",
|
||||
"warrant",
|
||||
"split",
|
||||
"reverse-split",
|
||||
"meeting",
|
||||
"expose",
|
||||
"ipo",
|
||||
"tender"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"symbol": {
|
||||
"description": "IDX ticker, e.g. \"BBCA\".",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"symbol"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "get_corporate_actions"
|
||||
},
|
||||
{
|
||||
"description": "Corporate actions across IDX from the user's own Stockbit session, oldest first, dated from `from` to `to` (default today and the next 30 days): upcoming cum dates, meetings, exposes, rights, warrants, IPOs and tender offers, and the past year. Each item has the same fields as get_corporate_actions, with `symbol`. One Stockbit request per kind (about 1 s each on a cold cache, then cached 6 hours); pass `kinds` to ask for less. AUTHERROR means there is no session: call stockbit_status, then stockbit_login.",
|
||||
"inputSchema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"from": {
|
||||
"default": null,
|
||||
"description": "First day, YYYY-MM-DD (default today).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"kinds": {
|
||||
"default": null,
|
||||
"description": "Kinds to list (default: every kind). Each kind is one request to Stockbit.",
|
||||
"items": {
|
||||
"enum": [
|
||||
"dividend",
|
||||
"stock-dividend",
|
||||
"bonus",
|
||||
"rights",
|
||||
"warrant",
|
||||
"split",
|
||||
"reverse-split",
|
||||
"meeting",
|
||||
"expose",
|
||||
"ipo",
|
||||
"tender"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"to": {
|
||||
"default": null,
|
||||
"description": "Last day, YYYY-MM-DD (default 30 days after `from`; at most 366 days).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"name": "get_corporate_calendar"
|
||||
},
|
||||
{
|
||||
"description": "Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` \"ipot\" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With \"idx\": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`.",
|
||||
"inputSchema": {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ use idx_core::config::CoreConfig;
|
|||
use idx_core::investor::{ImportSource, Investor};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const TOOLS: [&str; 18] = [
|
||||
const TOOLS: [&str; 20] = [
|
||||
"analyze_bandar",
|
||||
"analyze_stock",
|
||||
"compare_stocks",
|
||||
"get_broker_summary",
|
||||
"get_company",
|
||||
"get_corporate_actions",
|
||||
"get_corporate_calendar",
|
||||
"get_foreign_flow",
|
||||
"get_intraday",
|
||||
"get_investor_holdings",
|
||||
|
|
@ -502,6 +504,53 @@ fn the_order_book_needs_a_session_and_status_never_shows_tokens() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corporate_actions_need_a_session_and_keep_kinds_apart() {
|
||||
let mut client = Client::start(&[], false);
|
||||
let (is_error, body) = client.call("get_corporate_actions", json!({"symbol": "BBCA"}));
|
||||
assert!(is_error, "{body}");
|
||||
assert_eq!(body["code"], "AUTHERROR");
|
||||
|
||||
let mut client = Client::start_with_env(
|
||||
&[],
|
||||
false,
|
||||
&[TEST_SESSION, ("IDX_MOCK_TODAY", "2026-11-03")],
|
||||
);
|
||||
let actions = client.ok(
|
||||
"get_corporate_actions",
|
||||
json!({"symbol": "BBCA", "kinds": ["dividend", "split"]}),
|
||||
);
|
||||
assert_fetched(&actions);
|
||||
assert_eq!(actions["source"], "stockbit");
|
||||
let rows = actions["data"].as_array().unwrap();
|
||||
assert_eq!(rows.len(), 40, "37 dividends and 3 splits");
|
||||
assert_eq!(rows[0]["kind"], "dividend");
|
||||
assert_eq!(rows[0]["date"], "2026-08-28");
|
||||
assert_eq!(rows[0]["cash_per_share"], 25.0);
|
||||
assert!(rows[0].get("ratio").is_none(), "absent fields are omitted");
|
||||
let split = rows.iter().find(|r| r["kind"] == "split").unwrap();
|
||||
assert_eq!(split["ratio"], json!({"old": 1, "new": 5}));
|
||||
|
||||
let calendar = client.ok(
|
||||
"get_corporate_calendar",
|
||||
json!({"to": "2026-11-12", "kinds": ["rights", "dividend"]}),
|
||||
);
|
||||
let rows = calendar["data"].as_array().unwrap();
|
||||
assert_eq!(rows[0]["symbol"], "MGLV.JK");
|
||||
assert_eq!(rows[0]["price"], 8880);
|
||||
assert!(
|
||||
rows.windows(2)
|
||||
.all(|w| w[0]["date"].as_str() <= w[1]["date"].as_str())
|
||||
);
|
||||
|
||||
let (is_error, body) = client.call(
|
||||
"get_corporate_actions",
|
||||
json!({"symbol": "BBCA", "kinds": ["rups"]}),
|
||||
);
|
||||
assert!(is_error, "{body}");
|
||||
assert_eq!(body["code"], "INVALIDINPUT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_carry_cli_codes() {
|
||||
let mut client = Client::start(&[], false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue