feat(cli,mcp): idx stocks broker and get_broker_summary

`idx stocks broker SYMBOL` shows the top 10 buyers and sellers of a stock
by lots for the latest session with trades, one `--date`, or a
`--from`/`--to` range, filtered by `--investor` and `--board`. The MCP
server exposes the same as get_broker_summary.

- Range and filter parsing lives in idx-core (BrokerRange::from_args,
  FromStr for the filters), so both frontends accept the same input.
- Without dates, the latest session is found by walking back over
  weekdays; offline, uncached days are skipped.
- Table output lists foreign and state-owned brokers, and says when the
  source rounded figures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
hermes 2026-10-18 04:38:58 +00:00
commit 860d4daf77
25 changed files with 939 additions and 13 deletions

View file

@ -57,6 +57,68 @@
},
"name": "compare_stocks"
},
{
"description": "Broker summary for one IDX ticker: the top 10 buyers and top 10 sellers by lots over a session or date range, each with lots (1 lot = 100 shares), value and average price in IDR, plus totals. Buyers and sellers are ranked separately and gross, so a broker can appear on both sides. End of day only (IDX hides broker codes during trading); `kind` is the broker's own status (local, foreign, state_owned), not its clients'. `rounded` = true means figures of a million and more carry one decimal of precision.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"board": {
"default": null,
"description": "Which board's trades to count (default \"all\"); \"regular\" leaves out\nnegotiated block trades.",
"enum": [
"all",
"regular",
"cash",
"negotiated"
],
"type": "string"
},
"date": {
"default": null,
"description": "One session, YYYY-MM-DD. Omit date, from and to for the latest session with trades.",
"type": [
"string",
"null"
]
},
"from": {
"default": null,
"description": "First session of a range, YYYY-MM-DD (at most 366 days).",
"type": [
"string",
"null"
]
},
"investor": {
"default": null,
"description": "Whose trades to count, by investor domicile (default \"all\").",
"enum": [
"all",
"foreign",
"domestic"
],
"type": "string"
},
"symbol": {
"description": "IDX ticker, e.g. \"BBCA\".",
"type": "string"
},
"to": {
"default": null,
"description": "Last session of the range, YYYY-MM-DD (default today).",
"type": [
"string",
"null"
]
}
},
"required": [
"symbol"
],
"type": "object"
},
"name": "get_broker_summary"
},
{
"description": "Company data for one IDX ticker: profile, financial statements, earnings, analyst sentiment, insights, and news. News items carry `relevance`: `company` (about this ticker) first, then `market` (general market news). Sections that fail are listed under `errors`; the call fails only if every section fails.",
"inputSchema": {

View file

@ -9,9 +9,10 @@ use idx_core::config::CoreConfig;
use idx_core::investor::{ImportSource, Investor};
use serde_json::{Value, json};
const TOOLS: [&str; 11] = [
const TOOLS: [&str; 12] = [
"analyze_stock",
"compare_stocks",
"get_broker_summary",
"get_company",
"get_investor_holdings",
"get_ownership_changes",
@ -247,6 +248,16 @@ fn live_tools_wrap_data_with_freshness() {
assert_fetched(&analysis[kind]);
}
let brokers = client.ok(
"get_broker_summary",
json!({"symbol": "BBCA", "date": "2026-10-16"}),
);
assert_fetched(&brokers);
assert_eq!(brokers["source"], "ipot");
assert_eq!(brokers["data"]["buyers"][0]["code"], "MG");
assert_eq!(brokers["data"]["sellers"][0]["name"], "MANDIRI SEKURITAS");
assert_eq!(brokers["data"]["foreign_net_value"], -2_300_000_000_000_i64);
let compared = client.ok("compare_stocks", json!({"symbols": ["BBCA", "BBRI"]}));
assert_eq!(compared["reports"].as_array().unwrap().len(), 2);
assert_eq!(compared["failures"], json!([]));
@ -273,6 +284,14 @@ fn errors_carry_cli_codes() {
("compare_stocks", json!({"symbols": []})),
("get_company", json!({"symbol": "BBCA", "sections": []})),
("analyze_stock", json!({"symbol": "BBCA", "kinds": []})),
(
"get_broker_summary",
json!({"symbol": "BBCA", "date": "2026-10-16", "from": "2026-10-12"}),
),
(
"get_broker_summary",
json!({"symbol": "BBCA", "date": "2026-10-16", "board": "dark"}),
),
] {
let (is_error, body) = client.call(tool, arguments);
assert!(is_error, "{tool}: {body}");