mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
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:
parent
7c8151a3a6
commit
860d4daf77
25 changed files with 939 additions and 13 deletions
|
|
@ -12,7 +12,7 @@ use crate::api::ipot::BrokerSummaryRaw;
|
|||
use crate::api::{broker_provider, today_wib};
|
||||
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
|
||||
use crate::error::IdxError;
|
||||
use types::{BrokerBoard, BrokerInvestor, BrokerQuery, BrokerSummary};
|
||||
use types::{BrokerBoard, BrokerInvestor, BrokerQuery, BrokerRange, BrokerSummary};
|
||||
|
||||
const SOURCE: &str = "ipot";
|
||||
/// Longest range one summary may cover.
|
||||
|
|
@ -47,6 +47,35 @@ pub struct Broker<'a> {
|
|||
}
|
||||
|
||||
impl Broker<'_> {
|
||||
/// Today in WIB, the date `BrokerRange::from_args` defaults `to` to.
|
||||
pub fn today(&self) -> NaiveDate {
|
||||
today_wib()
|
||||
}
|
||||
|
||||
/// The summary a frontend asked for: the latest session or a range.
|
||||
pub fn fetch(
|
||||
&self,
|
||||
symbol: &str,
|
||||
range: BrokerRange,
|
||||
investor: BrokerInvestor,
|
||||
board: BrokerBoard,
|
||||
policy: CachePolicy,
|
||||
) -> Result<Fetched<BrokerSummary>, IdxError> {
|
||||
match range {
|
||||
BrokerRange::Latest => self.latest(symbol, investor, board, policy),
|
||||
BrokerRange::Dates { from, to } => self.summary(
|
||||
symbol,
|
||||
&BrokerQuery {
|
||||
from,
|
||||
to,
|
||||
investor,
|
||||
board,
|
||||
},
|
||||
policy,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The broker summary for `symbol` over `query`'s range. An empty
|
||||
/// summary means no trades (weekend, holiday, suspension).
|
||||
pub fn summary(
|
||||
|
|
@ -247,6 +276,38 @@ mod tests {
|
|||
assert_eq!(filtered.sellers[0].value, Some(2_000_000_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_broker_range_from_args() {
|
||||
let today = date(2026, 10, 18);
|
||||
let range = |date, from, to| BrokerRange::from_args(date, from, to, today);
|
||||
assert_eq!(range(None, None, None).unwrap(), BrokerRange::Latest);
|
||||
assert_eq!(
|
||||
range(Some("2026-10-16"), None, None).unwrap(),
|
||||
BrokerRange::Dates {
|
||||
from: date(2026, 10, 16),
|
||||
to: date(2026, 10, 16)
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
range(None, Some("2026-10-12"), None).unwrap(),
|
||||
BrokerRange::Dates {
|
||||
from: date(2026, 10, 12),
|
||||
to: today
|
||||
}
|
||||
);
|
||||
for (d, f, t) in [
|
||||
(None, None, Some("2026-10-16")),
|
||||
(Some("2026-10-16"), Some("2026-10-12"), None),
|
||||
(Some("16/10/2026"), None, None),
|
||||
(None, Some("2026-10-12"), Some("tomorrow")),
|
||||
] {
|
||||
assert!(
|
||||
matches!(range(d, f, t), Err(IdxError::InvalidInput(_))),
|
||||
"{d:?} {f:?} {t:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_range_bounds() {
|
||||
let today = date(2026, 10, 18);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue