mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat(core): broker summaries from IPOT
Adds a BrokerSummaryProvider capability served by Indo Premier's public broker summary page: the top 10 buyers and sellers of a stock by lots over a date range, with totals. No login or key. - api/ipot: pure parser (decimal scaling of `1.6 M`-style figures), a client that retries the server's spurious 404s, and a fixture mock. - broker: Broker::summary/latest over the cache (7 days for closed ranges, 5 minutes for today), range checks, and IDX member names bundled from GetBrokerCodeList. - foreign_net_value is null for filtered summaries: the source does not apply the filters to it. The endpoint's behavior was first mapped by fzn0x/idx (MIT). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
ebdba13a41
commit
2c93c73b6a
16 changed files with 1767 additions and 1 deletions
101
crates/idx-core/src/api/ipot/mod.rs
Normal file
101
crates/idx-core/src/api/ipot/mod.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
//! Indo Premier Sekuritas (IPOT) broker summary: which brokers bought and
|
||||
//! sold a stock. No login or key.
|
||||
//!
|
||||
//! `GET https://www.indopremier.com/module/saham/include/data-brokersummary.php`
|
||||
//! `?code=BBCA&start=MM/DD/YYYY&end=MM/DD/YYYY&fd=all|F|D&board=all|RG|TN|NG`
|
||||
//! answers an HTML fragment with the top 10 buyers and the top 10 sellers,
|
||||
//! ranked by lots, and totals for the range. Figures of a million and more
|
||||
//! are abbreviated to one decimal (`1.6 M`). A range without trades gives
|
||||
//! blank rows and zero totals; an unknown code gives `Invalid stock code!`
|
||||
//! with HTTP 200. The endpoint's behavior was first mapped by fzn0x/idx
|
||||
//! (MIT).
|
||||
|
||||
mod client;
|
||||
pub mod parse;
|
||||
|
||||
use client::IpotClient;
|
||||
pub use parse::{BrokerSummaryRaw, IpotPage, parse_broker_summary, parse_figure};
|
||||
|
||||
use super::BrokerSummaryProvider;
|
||||
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
|
||||
use crate::error::IdxError;
|
||||
|
||||
const ENDPOINT: &str = "https://www.indopremier.com/module/saham/include/data-brokersummary.php";
|
||||
|
||||
/// The request URL for `code` (a bare IDX code such as `BBCA`).
|
||||
pub fn broker_summary_url(code: &str, query: &BrokerQuery) -> String {
|
||||
let investor = match query.investor {
|
||||
BrokerInvestor::All => "all",
|
||||
BrokerInvestor::Foreign => "F",
|
||||
BrokerInvestor::Domestic => "D",
|
||||
};
|
||||
let board = match query.board {
|
||||
BrokerBoard::All => "all",
|
||||
BrokerBoard::Regular => "RG",
|
||||
BrokerBoard::Cash => "TN",
|
||||
BrokerBoard::Negotiated => "NG",
|
||||
};
|
||||
format!(
|
||||
"{ENDPOINT}?code={code}&start={}&end={}&fd={investor}&board={board}",
|
||||
query.from.format("%m/%d/%Y"),
|
||||
query.to.format("%m/%d/%Y"),
|
||||
)
|
||||
}
|
||||
|
||||
/// A parsed page as a summary; an unknown code is `SymbolNotFound`.
|
||||
pub(crate) fn summary_from_html(code: &str, html: &str) -> Result<BrokerSummaryRaw, IdxError> {
|
||||
match parse_broker_summary(html)? {
|
||||
IpotPage::Invalid => Err(IdxError::SymbolNotFound(code.to_string())),
|
||||
IpotPage::Summary(raw) => Ok(raw),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IpotProvider {
|
||||
client: IpotClient,
|
||||
}
|
||||
|
||||
impl IpotProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: IpotClient::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IpotProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BrokerSummaryProvider for IpotProvider {
|
||||
fn broker_summary(
|
||||
&self,
|
||||
code: &str,
|
||||
query: &BrokerQuery,
|
||||
) -> Result<BrokerSummaryRaw, IdxError> {
|
||||
let html = self.client.fetch_html(&broker_summary_url(code, query))?;
|
||||
summary_from_html(code, &html)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_broker_summary_url_maps_filters_to_source_codes() {
|
||||
let query = BrokerQuery {
|
||||
from: NaiveDate::from_ymd_opt(2026, 10, 12).unwrap(),
|
||||
to: NaiveDate::from_ymd_opt(2026, 10, 16).unwrap(),
|
||||
investor: BrokerInvestor::Foreign,
|
||||
board: BrokerBoard::Regular,
|
||||
};
|
||||
assert_eq!(
|
||||
broker_summary_url("BBCA", &query),
|
||||
"https://www.indopremier.com/module/saham/include/data-brokersummary.php?code=BBCA&start=10/12/2026&end=10/16/2026&fd=F&board=RG"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue