mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat: bandar and broker totals for one investor side
IPOT's investor filter splits each broker row by client domicile, but its footer then gives only that side's buying (BBCA 2026-10-16: foreign 1.7 T, domestic 4.9 T of 6.6 T). The foreign net gives the selling on all boards: foreign 1.7 + 2.3 = 4.0 T (IDX: about 3.95 T regular), domestic 4.9 - 2.3 = 2.6 T. - BrokerSummary gains buy_value and sell_value: the total for all investors, the footer and the derived selling for one side (selling is null with a board filter, where the net covers other boards). - Bandar weighs the top N buyers' share of the value bought against the top N sellers' share of the value sold, so `--investor foreign|domestic` reads that side alone; for all investors the readings are unchanged. One side on one board is refused, as there is no selling to weigh. - Tables print what the side bought and sold; fixtures for foreign and domestic on all boards were recorded from IPOT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
83783684cb
commit
5f64319157
23 changed files with 717 additions and 86 deletions
|
|
@ -207,7 +207,8 @@ does not show broker codes during trading. Without dates, the latest session wit
|
|||
long_about = "Bandarmology from the broker summary: the top 1, 3, 5 and 10 buyers' value \
|
||||
against the same number of sellers', as a share of value traded, with a signal from big \
|
||||
accumulation to big distribution. Several symbols give a screen ranked by the headline tier \
|
||||
(at most 100, one request each). Counts all investors (`--investor` must be `all`). A \
|
||||
(at most 100, one request each). `--investor foreign|domestic` weighs that side's buyers \
|
||||
against its buying and its sellers against its selling (needs `--board all`). A \
|
||||
heuristic over end-of-day broker codes, not advice.",
|
||||
after_help = "Examples:\n idx stocks bandar BBCA\n idx stocks bandar BBCA --from 2026-10-12 --to 2026-10-16 --top 3\n idx stocks bandar BBCA,BBRI,BMRI,TLKM\n idx stocks screen --filter high-volume | jq -r '.[].symbol' | idx stocks bandar -"
|
||||
)]
|
||||
|
|
|
|||
|
|
@ -952,6 +952,42 @@ pub(crate) fn broker_scope(
|
|||
scope
|
||||
}
|
||||
|
||||
/// What one investor side bought and sold, when a summary covers one side.
|
||||
fn side_totals_line(
|
||||
investor: BrokerInvestor,
|
||||
board: BrokerBoard,
|
||||
buy_value: Option<i64>,
|
||||
sell_value: Option<i64>,
|
||||
foreign_net: Option<i64>,
|
||||
) -> Option<String> {
|
||||
let side = match investor {
|
||||
BrokerInvestor::All => return None,
|
||||
BrokerInvestor::Foreign => "Foreign",
|
||||
BrokerInvestor::Domestic => "Domestic",
|
||||
};
|
||||
let buy = format_compact_option(buy_value);
|
||||
Some(match sell_value {
|
||||
Some(sell) => {
|
||||
let net = foreign_net
|
||||
.map(|net| match investor {
|
||||
BrokerInvestor::Domestic => -net,
|
||||
_ => net,
|
||||
})
|
||||
.map(|net| format!(" (net {:+})", format_compact(net)))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{side} investors bought {buy} IDR and sold {} IDR{net}.",
|
||||
format_compact(sell)
|
||||
)
|
||||
}
|
||||
None => format!(
|
||||
"{side} investors bought {buy} IDR on the {} board; the source gives one side's \
|
||||
selling only across all boards.",
|
||||
board.as_str()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn print_broker_summary(summary: &BrokerSummary) -> Result<(), IdxError> {
|
||||
println!(
|
||||
"{}",
|
||||
|
|
@ -995,8 +1031,14 @@ pub fn print_broker_summary(summary: &BrokerSummary) -> Result<(), IdxError> {
|
|||
totals.push_str(&format!("; foreign net {:+}", format_compact(net)));
|
||||
}
|
||||
println!("{totals}.");
|
||||
} else if summary.investor != BrokerInvestor::All {
|
||||
println!("No totals: for one investor side the source totals only that side's buying.");
|
||||
} else if let Some(line) = side_totals_line(
|
||||
summary.investor,
|
||||
summary.board,
|
||||
summary.buy_value,
|
||||
summary.sell_value,
|
||||
summary.foreign_net_value,
|
||||
) {
|
||||
println!("{line}");
|
||||
}
|
||||
for (kind, label) in [
|
||||
(BrokerKind::Foreign, "Foreign brokers"),
|
||||
|
|
@ -1049,9 +1091,9 @@ fn format_bandar_signal(signal: Option<BandarSignal>, no_color: bool) -> String
|
|||
|
||||
fn bandar_method_note(top: u8) -> String {
|
||||
format!(
|
||||
"Method {BANDAR_METHOD}: the top {top} buyers' value minus the top {top} sellers' value, \
|
||||
as a share of value traded; {}% or more reads accumulation, {}% or more big (negative: \
|
||||
distribution). Brokers trade for many clients, so this is a heuristic, not advice.",
|
||||
"Method {BANDAR_METHOD}: the top {top} buyers' share of the value bought minus the top \
|
||||
{top} sellers' share of the value sold; {}% or more reads accumulation, {}% or more big \
|
||||
(negative: distribution). Brokers trade for many clients, so this is a heuristic, not advice.",
|
||||
ACCUMULATION_BPS / 100,
|
||||
BIG_BPS / 100
|
||||
)
|
||||
|
|
@ -1067,7 +1109,11 @@ pub fn print_bandar_report(report: &BandarReport, no_color: bool) -> Result<(),
|
|||
)
|
||||
.bold()
|
||||
);
|
||||
if report.total_value.is_none_or(|value| value <= 0) {
|
||||
if report
|
||||
.buy_value
|
||||
.zip(report.sell_value)
|
||||
.is_none_or(|(buy, sell)| buy <= 0 && sell <= 0)
|
||||
{
|
||||
println!("No trades.");
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -1105,18 +1151,28 @@ pub fn print_bandar_report(report: &BandarReport, no_color: bool) -> Result<(),
|
|||
]);
|
||||
}
|
||||
println!("{table}");
|
||||
let mut totals = format!("Total {} IDR", format_compact_option(report.total_value));
|
||||
if let Some(average) = report.average_price {
|
||||
totals.push_str(&format!(", average {}", format_idr(average)));
|
||||
if let Some(line) = side_totals_line(
|
||||
report.investor,
|
||||
report.board,
|
||||
report.buy_value,
|
||||
report.sell_value,
|
||||
report.foreign_net_value,
|
||||
) {
|
||||
println!("{line}");
|
||||
} else {
|
||||
let mut totals = format!("Total {} IDR", format_compact_option(report.total_value));
|
||||
if let Some(average) = report.average_price {
|
||||
totals.push_str(&format!(", average {}", format_idr(average)));
|
||||
}
|
||||
if let Some(net) = report.foreign_net_value {
|
||||
totals.push_str(&format!(
|
||||
"; foreign net {:+} ({} of value)",
|
||||
format_compact(net),
|
||||
format_bps_pct(report.foreign_net_bps)
|
||||
));
|
||||
}
|
||||
println!("{totals}.");
|
||||
}
|
||||
if let Some(net) = report.foreign_net_value {
|
||||
totals.push_str(&format!(
|
||||
"; foreign net {:+} ({} of value)",
|
||||
format_compact(net),
|
||||
format_bps_pct(report.foreign_net_bps)
|
||||
));
|
||||
}
|
||||
println!("{totals}.");
|
||||
if !report.both_sides.is_empty() {
|
||||
println!(
|
||||
"On both sides: {}; their gross figures overstate which way they traded.",
|
||||
|
|
|
|||
|
|
@ -2499,10 +2499,12 @@ fn broker_summary_json_names_brokers_and_keeps_source_totals() {
|
|||
"total_value",
|
||||
"total_lots",
|
||||
"average_price",
|
||||
"sell_value",
|
||||
"foreign_net_value",
|
||||
] {
|
||||
assert!(filtered[field].is_null(), "{field}: {filtered}");
|
||||
}
|
||||
assert_eq!(filtered["buy_value"], 1_600_000_000_000_i64);
|
||||
let table = run_success_stdout(mock_bin("broker-filtered-table").args([
|
||||
"stocks",
|
||||
"broker",
|
||||
|
|
@ -2515,7 +2517,27 @@ fn broker_summary_json_names_brokers_and_keeps_source_totals() {
|
|||
"regular",
|
||||
]));
|
||||
assert!(!table.contains("Total "), "{table}");
|
||||
assert!(table.contains("No totals"), "{table}");
|
||||
assert!(
|
||||
table.contains("Foreign investors bought 1.6T IDR on the regular board"),
|
||||
"{table}"
|
||||
);
|
||||
|
||||
// On all boards, the foreign net gives foreign selling.
|
||||
let foreign = run_success_json(mock_bin("broker-foreign").args([
|
||||
"-o",
|
||||
"json",
|
||||
"stocks",
|
||||
"broker",
|
||||
"BBCA",
|
||||
"--date",
|
||||
"2026-10-16",
|
||||
"--investor",
|
||||
"foreign",
|
||||
]));
|
||||
assert!(foreign["total_value"].is_null());
|
||||
assert_eq!(foreign["buy_value"], 1_700_000_000_000_i64);
|
||||
assert_eq!(foreign["sell_value"], 4_000_000_000_000_i64);
|
||||
assert_eq!(foreign["foreign_net_value"], -2_300_000_000_000_i64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2717,7 +2739,15 @@ fn bandar_errors_follow_the_output_contract() {
|
|||
"INVALIDINPUT",
|
||||
),
|
||||
(
|
||||
vec!["BBCA".to_string(), "--investor".into(), "foreign".into()],
|
||||
vec![
|
||||
"BBCA".to_string(),
|
||||
"--date".into(),
|
||||
"2026-10-16".into(),
|
||||
"--investor".into(),
|
||||
"foreign".into(),
|
||||
"--board".into(),
|
||||
"regular".into(),
|
||||
],
|
||||
"INVALIDINPUT",
|
||||
),
|
||||
] {
|
||||
|
|
@ -2857,3 +2887,49 @@ fn foreign_flow_errors_follow_the_output_contract() {
|
|||
.args(["-o", "json", "stocks", "flow", "BBCA"]);
|
||||
run_error_json(&mut failing, "PROVIDERUNAVAILABLE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bandar_reads_one_investor_side_against_its_own_trading() {
|
||||
let reports = run_success_json(mock_bin("bandar-foreign").args([
|
||||
"-o",
|
||||
"json",
|
||||
"stocks",
|
||||
"bandar",
|
||||
"BBCA",
|
||||
"--date",
|
||||
"2026-10-16",
|
||||
"--investor",
|
||||
"foreign",
|
||||
]));
|
||||
let report = &reports[0];
|
||||
assert_eq!(report["investor"], "foreign");
|
||||
assert_eq!(report["buy_value"], 1_700_000_000_000_i64);
|
||||
assert_eq!(report["sell_value"], 4_000_000_000_000_i64);
|
||||
let top5 = &report["tiers"][2];
|
||||
// Shares of foreign buying and of foreign selling, each within 100%.
|
||||
let (buy, sell) = (
|
||||
top5["buy_share_bps"].as_i64().unwrap(),
|
||||
top5["sell_share_bps"].as_i64().unwrap(),
|
||||
);
|
||||
assert!(
|
||||
(0..=10_000).contains(&buy) && (0..=10_000).contains(&sell),
|
||||
"{top5}"
|
||||
);
|
||||
assert!((top5["imbalance_bps"].as_i64().unwrap() - (buy - sell)).abs() <= 1);
|
||||
assert!(report["signal"].is_string());
|
||||
|
||||
let table = run_success_stdout(mock_bin("bandar-foreign-table").args([
|
||||
"--no-color",
|
||||
"stocks",
|
||||
"bandar",
|
||||
"BBCA",
|
||||
"--date",
|
||||
"2026-10-16",
|
||||
"--investor",
|
||||
"foreign",
|
||||
]));
|
||||
assert!(
|
||||
table.contains("Foreign investors bought 1.7T IDR and sold 4.0T IDR (net -2.3T)."),
|
||||
"{table}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[{"symbol":"BBRI.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1748,"tiers":[{"top":1,"buy_value":504300000000,"sell_value":1000000000000,"imbalance_value":-495700000000,"imbalance_bps":-1502,"buy_share_bps":1528,"sell_share_bps":3030,"buy_average":2709,"sell_average":2716,"signal":"big_distribution"},{"top":3,"buy_value":1262700000000,"sell_value":1839600000000,"imbalance_value":-576900000000,"imbalance_bps":-1748,"buy_share_bps":3826,"sell_share_bps":5574,"buy_average":2715,"sell_average":2705,"signal":"big_distribution"},{"top":5,"buy_value":1737100000000,"sell_value":2283000000000,"imbalance_value":-545900000000,"imbalance_bps":-1654,"buy_share_bps":5263,"sell_share_bps":6918,"buy_average":2711,"sell_average":2707,"signal":"big_distribution"},{"top":10,"buy_value":2337100000000,"sell_value":2773100000000,"imbalance_value":-436000000000,"imbalance_bps":-1321,"buy_share_bps":7082,"sell_share_bps":8403,"buy_average":2709,"sell_average":2708,"signal":"distribution"}],"total_value":3300000000000,"average_price":2708,"foreign_net_value":-1100000000000,"foreign_net_bps":-3333,"both_sides":["AK","CC","NI","XL","YP","YU"],"rounded":true},{"symbol":"TLKM.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1993,"tiers":[{"top":1,"buy_value":381600000000,"sell_value":721600000000,"imbalance_value":-340000000000,"imbalance_bps":-1545,"buy_share_bps":1734,"sell_share_bps":3280,"buy_average":1960,"sell_average":1933,"signal":"big_distribution"},{"top":3,"buy_value":949100000000,"sell_value":1387700000000,"imbalance_value":-438600000000,"imbalance_bps":-1993,"buy_share_bps":4314,"sell_share_bps":6307,"buy_average":1939,"sell_average":1931,"signal":"big_distribution"},{"top":5,"buy_value":1341400000000,"sell_value":1722500000000,"imbalance_value":-381100000000,"imbalance_bps":-1732,"buy_share_bps":6097,"sell_share_bps":7829,"buy_average":1938,"sell_average":1932,"signal":"big_distribution"},{"top":10,"buy_value":1869600000000,"sell_value":1992600000000,"imbalance_value":-123000000000,"imbalance_bps":-559,"buy_share_bps":8498,"sell_share_bps":9057,"buy_average":1932,"sell_average":1930,"signal":"distribution"}],"total_value":2200000000000,"average_price":1931,"foreign_net_value":-90200000000,"foreign_net_bps":-410,"both_sides":["AK","BK","CC","KZ","MG","XL","YP","YU","ZP"],"rounded":true},{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-2913,"tiers":[{"top":1,"buy_value":841200000000,"sell_value":2200000000000,"imbalance_value":-1358800000000,"imbalance_bps":-2058,"buy_share_bps":1274,"sell_share_bps":3333,"buy_average":5191,"sell_average":5211,"signal":"big_distribution"},{"top":3,"buy_value":1984700000000,"sell_value":3907600000000,"imbalance_value":-1922900000000,"imbalance_bps":-2913,"buy_share_bps":3007,"sell_share_bps":5920,"buy_average":5212,"sell_average":5233,"signal":"big_distribution"},{"top":5,"buy_value":2939500000000,"sell_value":4540400000000,"imbalance_value":-1600900000000,"imbalance_bps":-2425,"buy_share_bps":4453,"sell_share_bps":6879,"buy_average":5211,"sell_average":5229,"signal":"big_distribution"},{"top":10,"buy_value":4477100000000,"sell_value":5551800000000,"imbalance_value":-1074700000000,"imbalance_bps":-1628,"buy_share_bps":6783,"sell_share_bps":8411,"buy_average":5213,"sell_average":5228,"signal":"big_distribution"}],"total_value":6600000000000,"average_price":5228,"foreign_net_value":-2300000000000,"foreign_net_bps":-3484,"both_sides":["AK","CC","MG","XL","YU","ZP"],"rounded":true}]
|
||||
[{"symbol":"BBRI.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1748,"tiers":[{"top":1,"buy_value":504300000000,"sell_value":1000000000000,"imbalance_value":-495700000000,"imbalance_bps":-1502,"buy_share_bps":1528,"sell_share_bps":3030,"buy_average":2709,"sell_average":2716,"signal":"big_distribution"},{"top":3,"buy_value":1262700000000,"sell_value":1839600000000,"imbalance_value":-576900000000,"imbalance_bps":-1748,"buy_share_bps":3826,"sell_share_bps":5574,"buy_average":2715,"sell_average":2705,"signal":"big_distribution"},{"top":5,"buy_value":1737100000000,"sell_value":2283000000000,"imbalance_value":-545900000000,"imbalance_bps":-1654,"buy_share_bps":5263,"sell_share_bps":6918,"buy_average":2711,"sell_average":2707,"signal":"big_distribution"},{"top":10,"buy_value":2337100000000,"sell_value":2773100000000,"imbalance_value":-436000000000,"imbalance_bps":-1321,"buy_share_bps":7082,"sell_share_bps":8403,"buy_average":2709,"sell_average":2708,"signal":"distribution"}],"total_value":3300000000000,"buy_value":3300000000000,"sell_value":3300000000000,"average_price":2708,"foreign_net_value":-1100000000000,"foreign_net_bps":-3333,"both_sides":["AK","CC","NI","XL","YP","YU"],"rounded":true},{"symbol":"TLKM.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1993,"tiers":[{"top":1,"buy_value":381600000000,"sell_value":721600000000,"imbalance_value":-340000000000,"imbalance_bps":-1545,"buy_share_bps":1734,"sell_share_bps":3280,"buy_average":1960,"sell_average":1933,"signal":"big_distribution"},{"top":3,"buy_value":949100000000,"sell_value":1387700000000,"imbalance_value":-438600000000,"imbalance_bps":-1993,"buy_share_bps":4314,"sell_share_bps":6307,"buy_average":1939,"sell_average":1931,"signal":"big_distribution"},{"top":5,"buy_value":1341400000000,"sell_value":1722500000000,"imbalance_value":-381100000000,"imbalance_bps":-1732,"buy_share_bps":6097,"sell_share_bps":7829,"buy_average":1938,"sell_average":1932,"signal":"big_distribution"},{"top":10,"buy_value":1869600000000,"sell_value":1992600000000,"imbalance_value":-123000000000,"imbalance_bps":-559,"buy_share_bps":8498,"sell_share_bps":9057,"buy_average":1932,"sell_average":1930,"signal":"distribution"}],"total_value":2200000000000,"buy_value":2200000000000,"sell_value":2200000000000,"average_price":1931,"foreign_net_value":-90200000000,"foreign_net_bps":-410,"both_sides":["AK","BK","CC","KZ","MG","XL","YP","YU","ZP"],"rounded":true},{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-2913,"tiers":[{"top":1,"buy_value":841200000000,"sell_value":2200000000000,"imbalance_value":-1358800000000,"imbalance_bps":-2058,"buy_share_bps":1274,"sell_share_bps":3333,"buy_average":5191,"sell_average":5211,"signal":"big_distribution"},{"top":3,"buy_value":1984700000000,"sell_value":3907600000000,"imbalance_value":-1922900000000,"imbalance_bps":-2913,"buy_share_bps":3007,"sell_share_bps":5920,"buy_average":5212,"sell_average":5233,"signal":"big_distribution"},{"top":5,"buy_value":2939500000000,"sell_value":4540400000000,"imbalance_value":-1600900000000,"imbalance_bps":-2425,"buy_share_bps":4453,"sell_share_bps":6879,"buy_average":5211,"sell_average":5229,"signal":"big_distribution"},{"top":10,"buy_value":4477100000000,"sell_value":5551800000000,"imbalance_value":-1074700000000,"imbalance_bps":-1628,"buy_share_bps":6783,"sell_share_bps":8411,"buy_average":5213,"sell_average":5228,"signal":"big_distribution"}],"total_value":6600000000000,"buy_value":6600000000000,"sell_value":6600000000000,"average_price":5228,"foreign_net_value":-2300000000000,"foreign_net_bps":-3484,"both_sides":["AK","CC","MG","XL","YU","ZP"],"rounded":true}]
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{"symbol":"BBRI.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1748,"tiers":[{"top":1,"buy_value":504300000000,"sell_value":1000000000000,"imbalance_value":-495700000000,"imbalance_bps":-1502,"buy_share_bps":1528,"sell_share_bps":3030,"buy_average":2709,"sell_average":2716,"signal":"big_distribution"},{"top":3,"buy_value":1262700000000,"sell_value":1839600000000,"imbalance_value":-576900000000,"imbalance_bps":-1748,"buy_share_bps":3826,"sell_share_bps":5574,"buy_average":2715,"sell_average":2705,"signal":"big_distribution"},{"top":5,"buy_value":1737100000000,"sell_value":2283000000000,"imbalance_value":-545900000000,"imbalance_bps":-1654,"buy_share_bps":5263,"sell_share_bps":6918,"buy_average":2711,"sell_average":2707,"signal":"big_distribution"},{"top":10,"buy_value":2337100000000,"sell_value":2773100000000,"imbalance_value":-436000000000,"imbalance_bps":-1321,"buy_share_bps":7082,"sell_share_bps":8403,"buy_average":2709,"sell_average":2708,"signal":"distribution"}],"total_value":3300000000000,"average_price":2708,"foreign_net_value":-1100000000000,"foreign_net_bps":-3333,"both_sides":["AK","CC","NI","XL","YP","YU"],"rounded":true}
|
||||
{"symbol":"TLKM.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1993,"tiers":[{"top":1,"buy_value":381600000000,"sell_value":721600000000,"imbalance_value":-340000000000,"imbalance_bps":-1545,"buy_share_bps":1734,"sell_share_bps":3280,"buy_average":1960,"sell_average":1933,"signal":"big_distribution"},{"top":3,"buy_value":949100000000,"sell_value":1387700000000,"imbalance_value":-438600000000,"imbalance_bps":-1993,"buy_share_bps":4314,"sell_share_bps":6307,"buy_average":1939,"sell_average":1931,"signal":"big_distribution"},{"top":5,"buy_value":1341400000000,"sell_value":1722500000000,"imbalance_value":-381100000000,"imbalance_bps":-1732,"buy_share_bps":6097,"sell_share_bps":7829,"buy_average":1938,"sell_average":1932,"signal":"big_distribution"},{"top":10,"buy_value":1869600000000,"sell_value":1992600000000,"imbalance_value":-123000000000,"imbalance_bps":-559,"buy_share_bps":8498,"sell_share_bps":9057,"buy_average":1932,"sell_average":1930,"signal":"distribution"}],"total_value":2200000000000,"average_price":1931,"foreign_net_value":-90200000000,"foreign_net_bps":-410,"both_sides":["AK","BK","CC","KZ","MG","XL","YP","YU","ZP"],"rounded":true}
|
||||
{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-2913,"tiers":[{"top":1,"buy_value":841200000000,"sell_value":2200000000000,"imbalance_value":-1358800000000,"imbalance_bps":-2058,"buy_share_bps":1274,"sell_share_bps":3333,"buy_average":5191,"sell_average":5211,"signal":"big_distribution"},{"top":3,"buy_value":1984700000000,"sell_value":3907600000000,"imbalance_value":-1922900000000,"imbalance_bps":-2913,"buy_share_bps":3007,"sell_share_bps":5920,"buy_average":5212,"sell_average":5233,"signal":"big_distribution"},{"top":5,"buy_value":2939500000000,"sell_value":4540400000000,"imbalance_value":-1600900000000,"imbalance_bps":-2425,"buy_share_bps":4453,"sell_share_bps":6879,"buy_average":5211,"sell_average":5229,"signal":"big_distribution"},{"top":10,"buy_value":4477100000000,"sell_value":5551800000000,"imbalance_value":-1074700000000,"imbalance_bps":-1628,"buy_share_bps":6783,"sell_share_bps":8411,"buy_average":5213,"sell_average":5228,"signal":"big_distribution"}],"total_value":6600000000000,"average_price":5228,"foreign_net_value":-2300000000000,"foreign_net_bps":-3484,"both_sides":["AK","CC","MG","XL","YU","ZP"],"rounded":true}
|
||||
{"symbol":"BBRI.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1748,"tiers":[{"top":1,"buy_value":504300000000,"sell_value":1000000000000,"imbalance_value":-495700000000,"imbalance_bps":-1502,"buy_share_bps":1528,"sell_share_bps":3030,"buy_average":2709,"sell_average":2716,"signal":"big_distribution"},{"top":3,"buy_value":1262700000000,"sell_value":1839600000000,"imbalance_value":-576900000000,"imbalance_bps":-1748,"buy_share_bps":3826,"sell_share_bps":5574,"buy_average":2715,"sell_average":2705,"signal":"big_distribution"},{"top":5,"buy_value":1737100000000,"sell_value":2283000000000,"imbalance_value":-545900000000,"imbalance_bps":-1654,"buy_share_bps":5263,"sell_share_bps":6918,"buy_average":2711,"sell_average":2707,"signal":"big_distribution"},{"top":10,"buy_value":2337100000000,"sell_value":2773100000000,"imbalance_value":-436000000000,"imbalance_bps":-1321,"buy_share_bps":7082,"sell_share_bps":8403,"buy_average":2709,"sell_average":2708,"signal":"distribution"}],"total_value":3300000000000,"buy_value":3300000000000,"sell_value":3300000000000,"average_price":2708,"foreign_net_value":-1100000000000,"foreign_net_bps":-3333,"both_sides":["AK","CC","NI","XL","YP","YU"],"rounded":true}
|
||||
{"symbol":"TLKM.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-1993,"tiers":[{"top":1,"buy_value":381600000000,"sell_value":721600000000,"imbalance_value":-340000000000,"imbalance_bps":-1545,"buy_share_bps":1734,"sell_share_bps":3280,"buy_average":1960,"sell_average":1933,"signal":"big_distribution"},{"top":3,"buy_value":949100000000,"sell_value":1387700000000,"imbalance_value":-438600000000,"imbalance_bps":-1993,"buy_share_bps":4314,"sell_share_bps":6307,"buy_average":1939,"sell_average":1931,"signal":"big_distribution"},{"top":5,"buy_value":1341400000000,"sell_value":1722500000000,"imbalance_value":-381100000000,"imbalance_bps":-1732,"buy_share_bps":6097,"sell_share_bps":7829,"buy_average":1938,"sell_average":1932,"signal":"big_distribution"},{"top":10,"buy_value":1869600000000,"sell_value":1992600000000,"imbalance_value":-123000000000,"imbalance_bps":-559,"buy_share_bps":8498,"sell_share_bps":9057,"buy_average":1932,"sell_average":1930,"signal":"distribution"}],"total_value":2200000000000,"buy_value":2200000000000,"sell_value":2200000000000,"average_price":1931,"foreign_net_value":-90200000000,"foreign_net_bps":-410,"both_sides":["AK","BK","CC","KZ","MG","XL","YP","YU","ZP"],"rounded":true}
|
||||
{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":3,"signal":"big_distribution","imbalance_bps":-2913,"tiers":[{"top":1,"buy_value":841200000000,"sell_value":2200000000000,"imbalance_value":-1358800000000,"imbalance_bps":-2058,"buy_share_bps":1274,"sell_share_bps":3333,"buy_average":5191,"sell_average":5211,"signal":"big_distribution"},{"top":3,"buy_value":1984700000000,"sell_value":3907600000000,"imbalance_value":-1922900000000,"imbalance_bps":-2913,"buy_share_bps":3007,"sell_share_bps":5920,"buy_average":5212,"sell_average":5233,"signal":"big_distribution"},{"top":5,"buy_value":2939500000000,"sell_value":4540400000000,"imbalance_value":-1600900000000,"imbalance_bps":-2425,"buy_share_bps":4453,"sell_share_bps":6879,"buy_average":5211,"sell_average":5229,"signal":"big_distribution"},{"top":10,"buy_value":4477100000000,"sell_value":5551800000000,"imbalance_value":-1074700000000,"imbalance_bps":-1628,"buy_share_bps":6783,"sell_share_bps":8411,"buy_average":5213,"sell_average":5228,"signal":"big_distribution"}],"total_value":6600000000000,"buy_value":6600000000000,"sell_value":6600000000000,"average_price":5228,"foreign_net_value":-2300000000000,"foreign_net_bps":-3484,"both_sides":["AK","CC","MG","XL","YU","ZP"],"rounded":true}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ exit: 0
|
|||
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ BBCA.JK ┆ 2026-10-16 ┆ big distribution ┆ -29.1% ┆ 30.1% ┆ 59.2% ┆ 6.6T ┆ -2.3T │
|
||||
└─────────┴────────────┴──────────────────┴─────────────┴───────────┴────────────┴───────┴─────────────┘
|
||||
Method top_concentration_v1: the top 3 buyers' value minus the top 3 sellers' value, as a share of value traded; 5% or more reads accumulation, 15% or more big (negative: distribution). Brokers trade for many clients, so this is a heuristic, not advice.
|
||||
Method top_concentration_v1: the top 3 buyers' share of the value bought minus the top 3 sellers' share of the value sold; 5% or more reads accumulation, 15% or more big (negative: distribution). Brokers trade for many clients, so this is a heuristic, not advice.
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":5,"signal":"big_distribution","imbalance_bps":-2425,"tiers":[{"top":1,"buy_value":841200000000,"sell_value":2200000000000,"imbalance_value":-1358800000000,"imbalance_bps":-2058,"buy_share_bps":1274,"sell_share_bps":3333,"buy_average":5191,"sell_average":5211,"signal":"big_distribution"},{"top":3,"buy_value":1984700000000,"sell_value":3907600000000,"imbalance_value":-1922900000000,"imbalance_bps":-2913,"buy_share_bps":3007,"sell_share_bps":5920,"buy_average":5212,"sell_average":5233,"signal":"big_distribution"},{"top":5,"buy_value":2939500000000,"sell_value":4540400000000,"imbalance_value":-1600900000000,"imbalance_bps":-2425,"buy_share_bps":4453,"sell_share_bps":6879,"buy_average":5211,"sell_average":5229,"signal":"big_distribution"},{"top":10,"buy_value":4477100000000,"sell_value":5551800000000,"imbalance_value":-1074700000000,"imbalance_bps":-1628,"buy_share_bps":6783,"sell_share_bps":8411,"buy_average":5213,"sell_average":5228,"signal":"big_distribution"}],"total_value":6600000000000,"average_price":5228,"foreign_net_value":-2300000000000,"foreign_net_bps":-3484,"both_sides":["AK","CC","MG","XL","YU","ZP"],"rounded":true}]
|
||||
[{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","method":"top_concentration_v1","top":5,"signal":"big_distribution","imbalance_bps":-2425,"tiers":[{"top":1,"buy_value":841200000000,"sell_value":2200000000000,"imbalance_value":-1358800000000,"imbalance_bps":-2058,"buy_share_bps":1274,"sell_share_bps":3333,"buy_average":5191,"sell_average":5211,"signal":"big_distribution"},{"top":3,"buy_value":1984700000000,"sell_value":3907600000000,"imbalance_value":-1922900000000,"imbalance_bps":-2913,"buy_share_bps":3007,"sell_share_bps":5920,"buy_average":5212,"sell_average":5233,"signal":"big_distribution"},{"top":5,"buy_value":2939500000000,"sell_value":4540400000000,"imbalance_value":-1600900000000,"imbalance_bps":-2425,"buy_share_bps":4453,"sell_share_bps":6879,"buy_average":5211,"sell_average":5229,"signal":"big_distribution"},{"top":10,"buy_value":4477100000000,"sell_value":5551800000000,"imbalance_value":-1074700000000,"imbalance_bps":-1628,"buy_share_bps":6783,"sell_share_bps":8411,"buy_average":5213,"sell_average":5228,"signal":"big_distribution"}],"total_value":6600000000000,"buy_value":6600000000000,"sell_value":6600000000000,"average_price":5228,"foreign_net_value":-2300000000000,"foreign_net_bps":-3484,"both_sides":["AK","CC","MG","XL","YU","ZP"],"rounded":true}]
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ Signal (top 5): big distribution, -24.2% of value
|
|||
└─────┴────────┴──────┴───────┴────────┴─────────┴──────────┴──────────────────┘
|
||||
Total 6.6T IDR, average 5,228; foreign net -2.3T (-34.8% of value).
|
||||
On both sides: AK, CC, MG, XL, YU, ZP; their gross figures overstate which way they traded.
|
||||
Method top_concentration_v1: the top 5 buyers' value minus the top 5 sellers' value, as a share of value traded; 5% or more reads accumulation, 15% or more big (negative: distribution). Brokers trade for many clients, so this is a heuristic, not advice.
|
||||
Method top_concentration_v1: the top 5 buyers' share of the value bought minus the top 5 sellers' share of the value sold; 5% or more reads accumulation, 15% or more big (negative: distribution). Brokers trade for many clients, so this is a heuristic, not advice.
|
||||
Note: the source rounds figures of a million and more to one decimal.
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{"symbol":"BBCA.JK","from":"2026-10-17","to":"2026-10-17","investor":"all","board":"all","buyers":[],"sellers":[],"total_value":0,"total_lots":0,"average_price":null,"foreign_net_value":0,"rounded":false}
|
||||
{"symbol":"BBCA.JK","from":"2026-10-17","to":"2026-10-17","investor":"all","board":"all","buyers":[],"sellers":[],"total_value":0,"total_lots":0,"average_price":null,"buy_value":0,"sell_value":0,"foreign_net_value":0,"rounded":false}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{"symbol":"BBCA.JK","from":"2026-10-12","to":"2026-10-16","investor":"all","board":"all","buyers":[{"rank":1,"code":"KZ","name":"CLSA SEKURITAS INDONESIA","kind":"foreign","lots":1700000,"value":963000000000,"average_price":5573},{"rank":2,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1600000,"value":842000000000,"average_price":5192},{"rank":3,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":1600000,"value":900900000000,"average_price":5780},{"rank":4,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":1400000,"value":781700000000,"average_price":5626},{"rank":5,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":1400000,"value":737900000000,"average_price":5408},{"rank":6,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":1300000,"value":687200000000,"average_price":5313},{"rank":7,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":1100000,"value":643900000000,"average_price":5771},{"rank":8,"code":"SQ","name":"BCA SEKURITAS","kind":"local","lots":973921,"value":519400000000,"average_price":5333},{"rank":9,"code":"YP","name":"MIRAE ASSET SEKURITAS INDONESIA","kind":"foreign","lots":919655,"value":487100000000,"average_price":5296},{"rank":10,"code":"PD","name":"INDO PREMIER SEKURITAS","kind":"local","lots":646162,"value":341200000000,"average_price":5280}],"sellers":[{"rank":1,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":5100000,"value":2700000000000,"average_price":5336},{"rank":2,"code":"RX","name":"MACQUARIE SEKURITAS INDONESIA","kind":"foreign","lots":1800000,"value":928500000000,"average_price":5295},{"rank":3,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":1600000,"value":942100000000,"average_price":5756},{"rank":4,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1500000,"value":800500000000,"average_price":5239},{"rank":5,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":1400000,"value":786500000000,"average_price":5785},{"rank":6,"code":"BB","name":"VERDHANA SEKURITAS INDONESIA","kind":"local","lots":852704,"value":457700000000,"average_price":5368},{"rank":7,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":830940,"value":466700000000,"average_price":5617},{"rank":8,"code":"BK","name":"J.P. MORGAN SEKURITAS INDONESIA","kind":"foreign","lots":709658,"value":393300000000,"average_price":5542},{"rank":9,"code":"KZ","name":"CLSA SEKURITAS INDONESIA","kind":"foreign","lots":621425,"value":363300000000,"average_price":5847},{"rank":10,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":441903,"value":242600000000,"average_price":5490}],"total_value":9900000000000,"total_lots":18100000,"average_price":5471,"foreign_net_value":-4100000000000,"rounded":true}
|
||||
{"symbol":"BBCA.JK","from":"2026-10-12","to":"2026-10-16","investor":"all","board":"all","buyers":[{"rank":1,"code":"KZ","name":"CLSA SEKURITAS INDONESIA","kind":"foreign","lots":1700000,"value":963000000000,"average_price":5573},{"rank":2,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1600000,"value":842000000000,"average_price":5192},{"rank":3,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":1600000,"value":900900000000,"average_price":5780},{"rank":4,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":1400000,"value":781700000000,"average_price":5626},{"rank":5,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":1400000,"value":737900000000,"average_price":5408},{"rank":6,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":1300000,"value":687200000000,"average_price":5313},{"rank":7,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":1100000,"value":643900000000,"average_price":5771},{"rank":8,"code":"SQ","name":"BCA SEKURITAS","kind":"local","lots":973921,"value":519400000000,"average_price":5333},{"rank":9,"code":"YP","name":"MIRAE ASSET SEKURITAS INDONESIA","kind":"foreign","lots":919655,"value":487100000000,"average_price":5296},{"rank":10,"code":"PD","name":"INDO PREMIER SEKURITAS","kind":"local","lots":646162,"value":341200000000,"average_price":5280}],"sellers":[{"rank":1,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":5100000,"value":2700000000000,"average_price":5336},{"rank":2,"code":"RX","name":"MACQUARIE SEKURITAS INDONESIA","kind":"foreign","lots":1800000,"value":928500000000,"average_price":5295},{"rank":3,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":1600000,"value":942100000000,"average_price":5756},{"rank":4,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1500000,"value":800500000000,"average_price":5239},{"rank":5,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":1400000,"value":786500000000,"average_price":5785},{"rank":6,"code":"BB","name":"VERDHANA SEKURITAS INDONESIA","kind":"local","lots":852704,"value":457700000000,"average_price":5368},{"rank":7,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":830940,"value":466700000000,"average_price":5617},{"rank":8,"code":"BK","name":"J.P. MORGAN SEKURITAS INDONESIA","kind":"foreign","lots":709658,"value":393300000000,"average_price":5542},{"rank":9,"code":"KZ","name":"CLSA SEKURITAS INDONESIA","kind":"foreign","lots":621425,"value":363300000000,"average_price":5847},{"rank":10,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":441903,"value":242600000000,"average_price":5490}],"total_value":9900000000000,"total_lots":18100000,"average_price":5471,"buy_value":9900000000000,"sell_value":9900000000000,"foreign_net_value":-4100000000000,"rounded":true}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","buyers":[{"rank":1,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1600000,"value":841200000000,"average_price":5191},{"rank":2,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":1100000,"value":600000000000,"average_price":5229},{"rank":3,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":1000000,"value":543500000000,"average_price":5227},{"rank":4,"code":"KZ","name":"CLSA SEKURITAS INDONESIA","kind":"foreign","lots":1000000,"value":522400000000,"average_price":5198},{"rank":5,"code":"SQ","name":"BCA SEKURITAS","kind":"local","lots":828163,"value":432400000000,"average_price":5221},{"rank":6,"code":"YP","name":"MIRAE ASSET SEKURITAS INDONESIA","kind":"foreign","lots":827340,"value":431700000000,"average_price":5218},{"rank":7,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":687603,"value":356600000000,"average_price":5186},{"rank":8,"code":"PD","name":"INDO PREMIER SEKURITAS","kind":"local","lots":592667,"value":309200000000,"average_price":5218},{"rank":9,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":441000,"value":230200000000,"average_price":5220},{"rank":10,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":398474,"value":209900000000,"average_price":5268}],"sellers":[{"rank":1,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":4300000,"value":2200000000000,"average_price":5211},{"rank":2,"code":"RX","name":"MACQUARIE SEKURITAS INDONESIA","kind":"foreign","lots":1700000,"value":908200000000,"average_price":5282},{"rank":3,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1500000,"value":799400000000,"average_price":5238},{"rank":4,"code":"BB","name":"VERDHANA SEKURITAS INDONESIA","kind":"local","lots":687067,"value":357400000000,"average_price":5202},{"rank":5,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":529223,"value":275400000000,"average_price":5203},{"rank":6,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":444696,"value":233500000000,"average_price":5251},{"rank":7,"code":"BK","name":"J.P. MORGAN SEKURITAS INDONESIA","kind":"foreign","lots":419266,"value":218800000000,"average_price":5219},{"rank":8,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":418986,"value":218000000000,"average_price":5202},{"rank":9,"code":"NI","name":"BNI SEKURITAS","kind":"state_owned","lots":357177,"value":186100000000,"average_price":5212},{"rank":10,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":296269,"value":155000000000,"average_price":5232}],"total_value":6600000000000,"total_lots":12600000,"average_price":5228,"foreign_net_value":-2300000000000,"rounded":true}
|
||||
{"symbol":"BBCA.JK","from":"2026-10-16","to":"2026-10-16","investor":"all","board":"all","buyers":[{"rank":1,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1600000,"value":841200000000,"average_price":5191},{"rank":2,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":1100000,"value":600000000000,"average_price":5229},{"rank":3,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":1000000,"value":543500000000,"average_price":5227},{"rank":4,"code":"KZ","name":"CLSA SEKURITAS INDONESIA","kind":"foreign","lots":1000000,"value":522400000000,"average_price":5198},{"rank":5,"code":"SQ","name":"BCA SEKURITAS","kind":"local","lots":828163,"value":432400000000,"average_price":5221},{"rank":6,"code":"YP","name":"MIRAE ASSET SEKURITAS INDONESIA","kind":"foreign","lots":827340,"value":431700000000,"average_price":5218},{"rank":7,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":687603,"value":356600000000,"average_price":5186},{"rank":8,"code":"PD","name":"INDO PREMIER SEKURITAS","kind":"local","lots":592667,"value":309200000000,"average_price":5218},{"rank":9,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":441000,"value":230200000000,"average_price":5220},{"rank":10,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":398474,"value":209900000000,"average_price":5268}],"sellers":[{"rank":1,"code":"CC","name":"MANDIRI SEKURITAS","kind":"state_owned","lots":4300000,"value":2200000000000,"average_price":5211},{"rank":2,"code":"RX","name":"MACQUARIE SEKURITAS INDONESIA","kind":"foreign","lots":1700000,"value":908200000000,"average_price":5282},{"rank":3,"code":"MG","name":"SEMESTA INDOVEST SEKURITAS","kind":"local","lots":1500000,"value":799400000000,"average_price":5238},{"rank":4,"code":"BB","name":"VERDHANA SEKURITAS INDONESIA","kind":"local","lots":687067,"value":357400000000,"average_price":5202},{"rank":5,"code":"ZP","name":"MAYBANK SEKURITAS INDONESIA","kind":"foreign","lots":529223,"value":275400000000,"average_price":5203},{"rank":6,"code":"AK","name":"UBS SEKURITAS INDONESIA","kind":"foreign","lots":444696,"value":233500000000,"average_price":5251},{"rank":7,"code":"BK","name":"J.P. MORGAN SEKURITAS INDONESIA","kind":"foreign","lots":419266,"value":218800000000,"average_price":5219},{"rank":8,"code":"YU","name":"CGS INTERNATIONAL SEKURITAS INDONESIA","kind":"foreign","lots":418986,"value":218000000000,"average_price":5202},{"rank":9,"code":"NI","name":"BNI SEKURITAS","kind":"state_owned","lots":357177,"value":186100000000,"average_price":5212},{"rank":10,"code":"XL","name":"STOCKBIT SEKURITAS DIGITAL","kind":"local","lots":296269,"value":155000000000,"average_price":5232}],"total_value":6600000000000,"total_lots":12600000,"average_price":5228,"buy_value":6600000000000,"sell_value":6600000000000,"foreign_net_value":-2300000000000,"rounded":true}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
//! brokers than selling? No network; see docs/ARCHITECTURE.md.
|
||||
//!
|
||||
//! The summary lists the top 10 buyers and the top 10 sellers by gross lots.
|
||||
//! For N in 1, 3, 5 and 10 the top N buyers' value is set against the top N
|
||||
//! sellers' value, as a share of the value traded:
|
||||
//! For N in 1, 3, 5 and 10 the top N buyers' share of the value bought is set
|
||||
//! against the top N sellers' share of the value sold. For all investors both
|
||||
//! are the value traded, so this is the top N's buy value less their sell
|
||||
//! value as a share of it; for one investor side (foreign or domestic), each
|
||||
//! is that side's own buying or selling.
|
||||
//!
|
||||
//! | top-N buy - sell value, % of value | signal |
|
||||
//! | top-N buy share - sell share, % | signal |
|
||||
//! |------------------------------------|--------------------|
|
||||
//! | >= +15 | `big_accumulation` |
|
||||
//! | +5 to +15 | `accumulation` |
|
||||
|
|
@ -79,11 +82,12 @@ pub struct BandarTier {
|
|||
pub sell_value: Option<i64>,
|
||||
/// `buy_value - sell_value`, whole IDR.
|
||||
pub imbalance_value: Option<i64>,
|
||||
/// `buy_share_bps - sell_share_bps`, computed exactly: for all investors,
|
||||
/// `imbalance_value` in basis points of the value traded.
|
||||
pub imbalance_bps: Option<i64>,
|
||||
/// The top N buyers' share of the value traded, basis points.
|
||||
/// The top N buyers' share of the value bought, basis points.
|
||||
pub buy_share_bps: Option<i64>,
|
||||
/// The top N sellers' share of the value traded, basis points.
|
||||
/// The top N sellers' share of the value sold, basis points.
|
||||
pub sell_share_bps: Option<i64>,
|
||||
/// Volume-weighted average price the top N buyers paid, whole IDR.
|
||||
pub buy_average: Option<i64>,
|
||||
|
|
@ -109,8 +113,12 @@ pub struct BandarReport {
|
|||
/// The headline tier's imbalance, basis points of value traded.
|
||||
pub imbalance_bps: Option<i64>,
|
||||
pub tiers: Vec<BandarTier>,
|
||||
/// Value traded over the range, whole IDR.
|
||||
/// Value traded over the range, whole IDR; `None` for one investor side.
|
||||
pub total_value: Option<i64>,
|
||||
/// Value the selected investors bought, whole IDR.
|
||||
pub buy_value: Option<i64>,
|
||||
/// Value the selected investors sold, whole IDR.
|
||||
pub sell_value: Option<i64>,
|
||||
/// Average traded price, whole IDR.
|
||||
pub average_price: Option<i64>,
|
||||
/// Foreign net value, whole IDR; `None` for a filtered summary.
|
||||
|
|
@ -175,15 +183,28 @@ pub fn bandar_tier(summary: &BrokerSummary, top: u8) -> BandarTier {
|
|||
let imbalance_value = buy_value
|
||||
.zip(sell_value)
|
||||
.and_then(|(buy, sell)| buy.checked_sub(sell));
|
||||
let imbalance_bps = imbalance_value.and_then(|value| bps(value, summary.total_value));
|
||||
let imbalance_bps = match (buy_value, sell_value, summary.buy_value, summary.sell_value) {
|
||||
(Some(buy), Some(sell), Some(bought), Some(sold)) if bought > 0 && sold > 0 => {
|
||||
// buy/bought - sell/sold over one denominator, so equal totals
|
||||
// give exactly (buy - sell) / total.
|
||||
let (buy, sell, bought, sold) = (
|
||||
i128::from(buy),
|
||||
i128::from(sell),
|
||||
i128::from(bought),
|
||||
i128::from(sold),
|
||||
);
|
||||
i64::try_from((buy * sold - sell * bought) * 10_000 / (bought * sold)).ok()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
BandarTier {
|
||||
top,
|
||||
buy_value,
|
||||
sell_value,
|
||||
imbalance_value,
|
||||
imbalance_bps,
|
||||
buy_share_bps: buy_value.and_then(|value| bps(value, summary.total_value)),
|
||||
sell_share_bps: sell_value.and_then(|value| bps(value, summary.total_value)),
|
||||
buy_share_bps: buy_value.and_then(|value| bps(value, summary.buy_value)),
|
||||
sell_share_bps: sell_value.and_then(|value| bps(value, summary.sell_value)),
|
||||
buy_average: vwap(buyers),
|
||||
sell_average: vwap(sellers),
|
||||
signal: imbalance_bps.map(BandarSignal::from_imbalance_bps),
|
||||
|
|
@ -223,6 +244,8 @@ pub fn analyze_bandar(summary: &BrokerSummary, top: u8) -> BandarReport {
|
|||
imbalance_bps: headline.imbalance_bps,
|
||||
tiers,
|
||||
total_value: summary.total_value,
|
||||
buy_value: summary.buy_value,
|
||||
sell_value: summary.sell_value,
|
||||
average_price: summary.average_price,
|
||||
foreign_net_value: summary.foreign_net_value,
|
||||
foreign_net_bps: summary
|
||||
|
|
@ -262,6 +285,8 @@ mod tests {
|
|||
total_value: Some(total),
|
||||
total_lots: None,
|
||||
average_price: None,
|
||||
buy_value: Some(total),
|
||||
sell_value: Some(total),
|
||||
foreign_net_value: Some(-total / 10),
|
||||
rounded: false,
|
||||
}
|
||||
|
|
@ -359,6 +384,36 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bandar_tier_weighs_one_side_against_its_own_buying_and_selling() {
|
||||
// Foreign investors bought 1,000 and sold 4,000: the top buyer took
|
||||
// half of the buying, the top seller a quarter of the selling.
|
||||
let mut s = summary(
|
||||
vec![flow("AA", Some(500), 100)],
|
||||
vec![flow("BB", Some(1_000), 100)],
|
||||
0,
|
||||
);
|
||||
s.investor = BrokerInvestor::Foreign;
|
||||
s.total_value = None;
|
||||
s.buy_value = Some(1_000);
|
||||
s.sell_value = Some(4_000);
|
||||
s.foreign_net_value = Some(-3_000);
|
||||
let report = analyze_bandar(&s, 1);
|
||||
let tier = &report.tiers[0];
|
||||
assert_eq!(
|
||||
(tier.buy_share_bps, tier.sell_share_bps),
|
||||
(Some(5000), Some(2500))
|
||||
);
|
||||
assert_eq!(tier.imbalance_bps, Some(2500));
|
||||
assert_eq!(tier.imbalance_value, Some(-500));
|
||||
assert_eq!(report.signal, Some(BandarSignal::BigAccumulation));
|
||||
assert_eq!(report.foreign_net_bps, None, "no total to weigh it against");
|
||||
|
||||
// Without the selling total (one side on one board) there is no reading.
|
||||
s.sell_value = None;
|
||||
assert_eq!(analyze_bandar(&s, 1).signal, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_bandar_never_counts_a_missing_figure_as_zero() {
|
||||
let s = summary(
|
||||
|
|
|
|||
|
|
@ -31,14 +31,15 @@ pub struct Comparison {
|
|||
pub failures: Vec<(String, IdxError)>,
|
||||
}
|
||||
|
||||
/// Readings weigh brokers against the value traded, which the source does
|
||||
/// not total for one investor side.
|
||||
fn check_investor(investor: BrokerInvestor) -> Result<(), IdxError> {
|
||||
if investor == BrokerInvestor::All {
|
||||
/// One investor side is weighed against its own selling, which the source
|
||||
/// gives only across all boards.
|
||||
fn check_scope(investor: BrokerInvestor, board: BrokerBoard) -> Result<(), IdxError> {
|
||||
if investor == BrokerInvestor::All || board == BrokerBoard::All {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(IdxError::InvalidInput(format!(
|
||||
"bandar needs --investor all: the broker summary has no total for {} investors alone",
|
||||
"bandar for {} investors needs --board all: the broker summary gives one side's \
|
||||
selling only across all boards",
|
||||
investor.as_str()
|
||||
)))
|
||||
}
|
||||
|
|
@ -70,7 +71,7 @@ impl Analysis<'_> {
|
|||
policy: CachePolicy,
|
||||
) -> Result<Fetched<BandarReport>, IdxError> {
|
||||
let top = check_top(query.top)?;
|
||||
check_investor(query.investor)?;
|
||||
check_scope(query.investor, query.board)?;
|
||||
let summary =
|
||||
self.idx
|
||||
.broker()
|
||||
|
|
@ -87,7 +88,7 @@ impl Analysis<'_> {
|
|||
policy: CachePolicy,
|
||||
) -> Result<BandarScreen, IdxError> {
|
||||
check_top(query.top)?;
|
||||
check_investor(query.investor)?;
|
||||
check_scope(query.investor, query.board)?;
|
||||
let mut reports = Vec::new();
|
||||
let mut failures = Vec::new();
|
||||
for symbol in symbols {
|
||||
|
|
|
|||
|
|
@ -155,13 +155,27 @@ impl Broker<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The public summary of one parsed page. With one investor side selected,
|
||||
/// the page's footer covers only part of the trades the lists show (foreign
|
||||
/// buying, not foreign selling), so no totals are given; `F. NVal` ignores
|
||||
/// both filters, so the foreign net goes with any filter.
|
||||
/// The public summary of one parsed page.
|
||||
///
|
||||
/// With one investor side selected, the page's footer (`T. Val`) covers only
|
||||
/// that side's buying, so it is the side's `buy_value`, not a total of the
|
||||
/// trades the lists show. `F. NVal`, the foreign net, ignores both filters:
|
||||
/// on all boards it gives the side's selling (foreign buying less the foreign
|
||||
/// net; domestic buying plus it), and with a board filter it is dropped.
|
||||
fn summary_from_raw(symbol: &str, query: &BrokerQuery, raw: BrokerSummaryRaw) -> BrokerSummary {
|
||||
let one_side = query.investor != BrokerInvestor::All;
|
||||
let filtered = one_side || query.board != BrokerBoard::All;
|
||||
let all_boards = query.board == BrokerBoard::All;
|
||||
let foreign_net = raw.foreign_net_value.filter(|_| all_boards);
|
||||
let buy_value = raw.total_value;
|
||||
let sell_value = match query.investor {
|
||||
BrokerInvestor::All => raw.total_value,
|
||||
BrokerInvestor::Foreign => buy_value
|
||||
.zip(foreign_net)
|
||||
.and_then(|(buy, net)| buy.checked_sub(net)),
|
||||
BrokerInvestor::Domestic => buy_value
|
||||
.zip(foreign_net)
|
||||
.and_then(|(buy, net)| buy.checked_add(net)),
|
||||
};
|
||||
let mut summary = BrokerSummary {
|
||||
symbol: symbol.to_string(),
|
||||
from: raw.from.unwrap_or(query.from),
|
||||
|
|
@ -173,7 +187,9 @@ fn summary_from_raw(symbol: &str, query: &BrokerQuery, raw: BrokerSummaryRaw) ->
|
|||
total_value: raw.total_value.filter(|_| !one_side),
|
||||
total_lots: raw.total_lots.filter(|_| !one_side),
|
||||
average_price: raw.average_price.filter(|_| !one_side),
|
||||
foreign_net_value: raw.foreign_net_value.filter(|_| !filtered),
|
||||
buy_value,
|
||||
sell_value,
|
||||
foreign_net_value: foreign_net,
|
||||
rounded: raw.rounded,
|
||||
};
|
||||
for flow in summary.buyers.iter_mut().chain(summary.sellers.iter_mut()) {
|
||||
|
|
@ -228,7 +244,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary_from_raw_drops_totals_that_do_not_cover_the_lists() {
|
||||
fn test_summary_from_raw_gives_each_side_its_own_totals() {
|
||||
let day = date(2026, 10, 16);
|
||||
let read = |name: &str| {
|
||||
let html = std::fs::read_to_string(format!("tests/fixtures/{name}")).unwrap();
|
||||
|
|
@ -243,26 +259,68 @@ mod tests {
|
|||
assert_eq!(all.foreign_net_value, Some(-2_300_000_000_000));
|
||||
assert_eq!(all.sellers[0].name.as_deref(), Some("MANDIRI SEKURITAS"));
|
||||
|
||||
// Foreign, regular board: the footer's 1.6 T is less than the 2.0 T
|
||||
// its top seller alone sold.
|
||||
let foreign = BrokerQuery {
|
||||
investor: BrokerInvestor::Foreign,
|
||||
board: BrokerBoard::Regular,
|
||||
..query(day, day)
|
||||
assert_eq!(
|
||||
(all.buy_value, all.sell_value),
|
||||
(all.total_value, all.total_value)
|
||||
);
|
||||
|
||||
// Foreign on all boards: the footer's 1.7 T is foreign buying; with
|
||||
// the -2.3 T foreign net, foreign selling is 4.0 T (IDX: about
|
||||
// 3.95 T). Domestic buying 4.9 T makes up the rest of 6.6 T, and its
|
||||
// selling is 2.6 T.
|
||||
let side = |investor, board, name: &str| {
|
||||
let query = BrokerQuery {
|
||||
investor,
|
||||
board,
|
||||
..query(day, day)
|
||||
};
|
||||
summary_from_raw("BBCA.JK", &query, read(name))
|
||||
};
|
||||
let foreign = side(
|
||||
BrokerInvestor::Foreign,
|
||||
BrokerBoard::All,
|
||||
"ipot_brokersummary_bbca_20261016_foreign_all.html",
|
||||
);
|
||||
assert_eq!(
|
||||
(foreign.buy_value, foreign.sell_value),
|
||||
(Some(1_700_000_000_000), Some(4_000_000_000_000))
|
||||
);
|
||||
assert_eq!(foreign.total_value, None);
|
||||
assert_eq!(foreign.foreign_net_value, Some(-2_300_000_000_000));
|
||||
let domestic = side(
|
||||
BrokerInvestor::Domestic,
|
||||
BrokerBoard::All,
|
||||
"ipot_brokersummary_bbca_20261016_domestic_all.html",
|
||||
);
|
||||
assert_eq!(
|
||||
(domestic.buy_value, domestic.sell_value),
|
||||
(Some(4_900_000_000_000), Some(2_600_000_000_000))
|
||||
);
|
||||
|
||||
// Foreign, regular board: the footer's 1.6 T is regular foreign
|
||||
// buying, less than the 2.0 T its top seller alone sold, and the net
|
||||
// covers all boards, so selling cannot be derived.
|
||||
let raw = read("ipot_brokersummary_bbca_20261016_foreign_regular.html");
|
||||
assert!(raw.total_value < raw.sellers[0].value);
|
||||
let filtered = summary_from_raw("BBCA.JK", &foreign, raw);
|
||||
let regular = side(
|
||||
BrokerInvestor::Foreign,
|
||||
BrokerBoard::Regular,
|
||||
"ipot_brokersummary_bbca_20261016_foreign_regular.html",
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
filtered.total_value,
|
||||
filtered.total_lots,
|
||||
filtered.average_price
|
||||
regular.total_value,
|
||||
regular.total_lots,
|
||||
regular.average_price
|
||||
),
|
||||
(None, None, None)
|
||||
);
|
||||
assert_eq!(filtered.foreign_net_value, None);
|
||||
assert_eq!(filtered.sellers[0].value, Some(2_000_000_000_000));
|
||||
assert_eq!(
|
||||
(regular.buy_value, regular.sell_value),
|
||||
(Some(1_600_000_000_000), None)
|
||||
);
|
||||
assert_eq!(regular.foreign_net_value, None);
|
||||
assert_eq!(regular.sellers[0].value, Some(2_000_000_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -215,9 +215,18 @@ pub struct BrokerSummary {
|
|||
/// Average traded price, whole IDR; `None` when nothing traded, or as
|
||||
/// for `total_value`.
|
||||
pub average_price: Option<i64>,
|
||||
/// Value the selected investors bought over the range on the selected
|
||||
/// board, whole IDR: `total_value` for all investors, and for one side
|
||||
/// the source's footer, which then covers that side's buying.
|
||||
pub buy_value: Option<i64>,
|
||||
/// Value the selected investors sold, whole IDR: `total_value` for all
|
||||
/// investors; for one side on all boards, its buying less its net
|
||||
/// (foreign net, or its negative for domestic investors). `None` for one
|
||||
/// side on one board, where the source gives no net to derive it from.
|
||||
pub sell_value: Option<i64>,
|
||||
/// Foreign buy value minus foreign sell value over the range, whole IDR,
|
||||
/// across all boards. The source does not apply the investor or board
|
||||
/// filter to it, so it is `None` for a filtered summary.
|
||||
/// across all boards. The source does not apply the board filter to it,
|
||||
/// so it is `None` for a summary of one board.
|
||||
pub foreign_net_value: Option<i64>,
|
||||
/// True when the source abbreviated at least one figure (`1.6 M` lots,
|
||||
/// `841.2 B` IDR), so those figures carry only one decimal of precision.
|
||||
|
|
|
|||
181
crates/idx-core/tests/fixtures/ipot_brokersummary_bbca_20261016_domestic_all.html
vendored
Normal file
181
crates/idx-core/tests/fixtures/ipot_brokersummary_bbca_20261016_domestic_all.html
vendored
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<div id="modContentBrokerSum"></div>
|
||||
|
||||
<!--DATA BROKER SUMMARY-->
|
||||
<div class="pa5">
|
||||
<div class="row mincol-3">
|
||||
<div class="col col-xs-5 col-sm-3">
|
||||
<input class="form-control input-xs text-center" id="startDateBrokerSummary" name="startDate" placeholder="DD Month YYYY" value="10/16/2026" type="text">
|
||||
</div>
|
||||
<div class="col col-xs-5 col-sm-3">
|
||||
<input class="form-control input-xs text-center" id="endDateBrokerSummary" name="endDate" placeholder="DD Month YYYY" value="10/16/2026" type="text">
|
||||
</div>
|
||||
<div class="mt15 visible-xs"> </div>
|
||||
<div class="col col-xs-5 col-sm-3">
|
||||
<select class="form-control input-xs" id="FDBrokerSummary">
|
||||
<option value="all">All</option>
|
||||
<option value="F" >Foreign</option>
|
||||
<option value="D" selected>Domestic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col col-xs-5 col-sm-2">
|
||||
<select class="form-control input-xs" id="boardBrokerSummary">
|
||||
<option value="all">All Trade</option>
|
||||
<option value="RG" >Regular</option>
|
||||
<option value="TN" >Tunai</option>
|
||||
<option value="NG" >Nego</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col col-xs-2 col-sm-1">
|
||||
<button type="button" class="btn btn-default btn-block btn-xs-input" onClick="quickinfo_brokersummary($('#stockPageCode').val()!==undefined?$('#stockPageCode').val():$('#stockInfoCode').val(),$('#startDateBrokerSummary').val(),$('#endDateBrokerSummary').val(),$('#FDBrokerSummary').val(),$('#boardBrokerSummary').val());">GO</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" class="table table-summary table-hover noborder nm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">Buyer</th>
|
||||
<th align="right">B.Lot</th>
|
||||
<th align="right">B.Val</th>
|
||||
<th align="right">B.Avg</th>
|
||||
<th align="center">#</th>
|
||||
<th align="center">Seller</th>
|
||||
<th align="right">S.Lot</th>
|
||||
<th align="right">S.Val</th>
|
||||
<th align="right">S.Avg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">MG</span></td>
|
||||
<td align="right"><span class="text-up">1.6 M</span></td>
|
||||
<td align="right"><span class="text-up">841.2 B</span></td>
|
||||
<td align="right"><span class="text-up">5,191 </span></td>
|
||||
<td align="center">1</td>
|
||||
<td align="center"><span class="text-local">MG</span></td>
|
||||
<td align="right"><span class="text-down">1.5 M</span></td>
|
||||
<td align="right"><span class="text-down">799.4 B</span></td>
|
||||
<td align="right"><span class="text-down">5,238 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">XL</span></td>
|
||||
<td align="right"><span class="text-up">1.1 M</span></td>
|
||||
<td align="right"><span class="text-up">595.0 B</span></td>
|
||||
<td align="right"><span class="text-up">5,229 </span></td>
|
||||
<td align="center">2</td>
|
||||
<td align="center"><span class="text-local">BB</span></td>
|
||||
<td align="right"><span class="text-down">577,558 </span></td>
|
||||
<td align="right"><span class="text-down">300.5 B</span></td>
|
||||
<td align="right"><span class="text-down">5,203 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-bumn">CC</span></td>
|
||||
<td align="right"><span class="text-up">841,738 </span></td>
|
||||
<td align="right"><span class="text-up">439.2 B</span></td>
|
||||
<td align="right"><span class="text-up">5,217 </span></td>
|
||||
<td align="center">3</td>
|
||||
<td align="center"><span class="text-bumn">CC</span></td>
|
||||
<td align="right"><span class="text-down">448,606 </span></td>
|
||||
<td align="right"><span class="text-down">233.8 B</span></td>
|
||||
<td align="right"><span class="text-down">5,211 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">YP</span></td>
|
||||
<td align="right"><span class="text-up">824,562 </span></td>
|
||||
<td align="right"><span class="text-up">430.3 B</span></td>
|
||||
<td align="right"><span class="text-up">5,218 </span></td>
|
||||
<td align="center">4</td>
|
||||
<td align="center"><span class="text-bumn">NI</span></td>
|
||||
<td align="right"><span class="text-down">356,533 </span></td>
|
||||
<td align="right"><span class="text-down">185.8 B</span></td>
|
||||
<td align="right"><span class="text-down">5,212 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">SQ</span></td>
|
||||
<td align="right"><span class="text-up">824,521 </span></td>
|
||||
<td align="right"><span class="text-up">430.5 B</span></td>
|
||||
<td align="right"><span class="text-up">5,221 </span></td>
|
||||
<td align="center">5</td>
|
||||
<td align="center"><span class="text-local">XL</span></td>
|
||||
<td align="right"><span class="text-down">296,269 </span></td>
|
||||
<td align="right"><span class="text-down">155.0 B</span></td>
|
||||
<td align="right"><span class="text-down">5,232 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">PD</span></td>
|
||||
<td align="right"><span class="text-up">587,559 </span></td>
|
||||
<td align="right"><span class="text-up">306.6 B</span></td>
|
||||
<td align="right"><span class="text-up">5,217 </span></td>
|
||||
<td align="center">6</td>
|
||||
<td align="center"><span class="text-foreign">YP</span></td>
|
||||
<td align="right"><span class="text-down">264,188 </span></td>
|
||||
<td align="right"><span class="text-down">138.5 B</span></td>
|
||||
<td align="right"><span class="text-down">5,242 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">CP</span></td>
|
||||
<td align="right"><span class="text-up">347,229 </span></td>
|
||||
<td align="right"><span class="text-up">181.1 B</span></td>
|
||||
<td align="right"><span class="text-up">5,216 </span></td>
|
||||
<td align="center">7</td>
|
||||
<td align="center"><span class="text-local">CP</span></td>
|
||||
<td align="right"><span class="text-down">188,644 </span></td>
|
||||
<td align="right"><span class="text-down">98.9 B</span></td>
|
||||
<td align="right"><span class="text-down">5,245 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">XC</span></td>
|
||||
<td align="right"><span class="text-up">336,997 </span></td>
|
||||
<td align="right"><span class="text-up">176.1 B</span></td>
|
||||
<td align="right"><span class="text-up">5,226 </span></td>
|
||||
<td align="center">8</td>
|
||||
<td align="center"><span class="text-foreign">YU</span></td>
|
||||
<td align="right"><span class="text-down">166,603 </span></td>
|
||||
<td align="right"><span class="text-down">86.6 B</span></td>
|
||||
<td align="right"><span class="text-down">5,201 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">KK</span></td>
|
||||
<td align="right"><span class="text-up">302,943 </span></td>
|
||||
<td align="right"><span class="text-up">157.9 B</span></td>
|
||||
<td align="right"><span class="text-up">5,211 </span></td>
|
||||
<td align="center">9</td>
|
||||
<td align="center"><span class="text-local">BQ</span></td>
|
||||
<td align="right"><span class="text-down">142,119 </span></td>
|
||||
<td align="right"><span class="text-down">74.7 B</span></td>
|
||||
<td align="right"><span class="text-down">5,254 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">BQ</span></td>
|
||||
<td align="right"><span class="text-up">245,981 </span></td>
|
||||
<td align="right"><span class="text-up">128.6 B</span></td>
|
||||
<td align="right"><span class="text-up">5,228 </span></td>
|
||||
<td align="center">10</td>
|
||||
<td align="center"><span class="text-foreign">KK</span></td>
|
||||
<td align="right"><span class="text-down">133,633 </span></td>
|
||||
<td align="right"><span class="text-down">70.0 B</span></td>
|
||||
<td align="right"><span class="text-down">5,236 </span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="bdr-t">
|
||||
<th colspan="9" align="center">
|
||||
<div class="text-center pt20 valign-middle" style="height:50px;">
|
||||
<span class="fsize12 ml5 mr5">T. Val : 4.9 T</span>
|
||||
<span class="fsize12 ml5 mr5">F. NVal : -2.3 T</span>
|
||||
<span class="fsize12 ml5 mr5">T.Lot : 9.3 M</span>
|
||||
<span class="fsize12 ml5 mr5">Avg : 5,216 </span>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- =======================
|
||||
======SCRIPT MODULE=========
|
||||
============================-->
|
||||
<script src="../../../../module/saham/include/js/data-brokersummary.js"></script>
|
||||
181
crates/idx-core/tests/fixtures/ipot_brokersummary_bbca_20261016_foreign_all.html
vendored
Normal file
181
crates/idx-core/tests/fixtures/ipot_brokersummary_bbca_20261016_foreign_all.html
vendored
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<div id="modContentBrokerSum"></div>
|
||||
|
||||
<!--DATA BROKER SUMMARY-->
|
||||
<div class="pa5">
|
||||
<div class="row mincol-3">
|
||||
<div class="col col-xs-5 col-sm-3">
|
||||
<input class="form-control input-xs text-center" id="startDateBrokerSummary" name="startDate" placeholder="DD Month YYYY" value="10/16/2026" type="text">
|
||||
</div>
|
||||
<div class="col col-xs-5 col-sm-3">
|
||||
<input class="form-control input-xs text-center" id="endDateBrokerSummary" name="endDate" placeholder="DD Month YYYY" value="10/16/2026" type="text">
|
||||
</div>
|
||||
<div class="mt15 visible-xs"> </div>
|
||||
<div class="col col-xs-5 col-sm-3">
|
||||
<select class="form-control input-xs" id="FDBrokerSummary">
|
||||
<option value="all">All</option>
|
||||
<option value="F" selected>Foreign</option>
|
||||
<option value="D" >Domestic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col col-xs-5 col-sm-2">
|
||||
<select class="form-control input-xs" id="boardBrokerSummary">
|
||||
<option value="all">All Trade</option>
|
||||
<option value="RG" >Regular</option>
|
||||
<option value="TN" >Tunai</option>
|
||||
<option value="NG" >Nego</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col col-xs-2 col-sm-1">
|
||||
<button type="button" class="btn btn-default btn-block btn-xs-input" onClick="quickinfo_brokersummary($('#stockPageCode').val()!==undefined?$('#stockPageCode').val():$('#stockInfoCode').val(),$('#startDateBrokerSummary').val(),$('#endDateBrokerSummary').val(),$('#FDBrokerSummary').val(),$('#boardBrokerSummary').val());">GO</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" class="table table-summary table-hover noborder nm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">Buyer</th>
|
||||
<th align="right">B.Lot</th>
|
||||
<th align="right">B.Val</th>
|
||||
<th align="right">B.Avg</th>
|
||||
<th align="center">#</th>
|
||||
<th align="center">Seller</th>
|
||||
<th align="right">S.Lot</th>
|
||||
<th align="right">S.Val</th>
|
||||
<th align="right">S.Avg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">KZ</span></td>
|
||||
<td align="right"><span class="text-up">1.0 M</span></td>
|
||||
<td align="right"><span class="text-up">521.3 B</span></td>
|
||||
<td align="right"><span class="text-up">5,198 </span></td>
|
||||
<td align="center">1</td>
|
||||
<td align="center"><span class="text-bumn">CC</span></td>
|
||||
<td align="right"><span class="text-down">3.9 M</span></td>
|
||||
<td align="right"><span class="text-down">2.0 T</span></td>
|
||||
<td align="right"><span class="text-down">5,211 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">YU</span></td>
|
||||
<td align="right"><span class="text-up">575,965 </span></td>
|
||||
<td align="right"><span class="text-up">298.4 B</span></td>
|
||||
<td align="right"><span class="text-up">5,181 </span></td>
|
||||
<td align="center">2</td>
|
||||
<td align="center"><span class="text-foreign">RX</span></td>
|
||||
<td align="right"><span class="text-down">1.7 M</span></td>
|
||||
<td align="right"><span class="text-down">908.2 B</span></td>
|
||||
<td align="right"><span class="text-down">5,282 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">BK</span></td>
|
||||
<td align="right"><span class="text-up">369,446 </span></td>
|
||||
<td align="right"><span class="text-up">196.0 B</span></td>
|
||||
<td align="right"><span class="text-up">5,304 </span></td>
|
||||
<td align="center">3</td>
|
||||
<td align="center"><span class="text-foreign">ZP</span></td>
|
||||
<td align="right"><span class="text-down">474,808 </span></td>
|
||||
<td align="right"><span class="text-down">247.1 B</span></td>
|
||||
<td align="right"><span class="text-down">5,203 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">AK</span></td>
|
||||
<td align="right"><span class="text-up">366,209 </span></td>
|
||||
<td align="right"><span class="text-up">193.1 B</span></td>
|
||||
<td align="right"><span class="text-up">5,274 </span></td>
|
||||
<td align="center">4</td>
|
||||
<td align="center"><span class="text-foreign">AK</span></td>
|
||||
<td align="right"><span class="text-down">444,447 </span></td>
|
||||
<td align="right"><span class="text-down">233.4 B</span></td>
|
||||
<td align="right"><span class="text-down">5,251 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">ZP</span></td>
|
||||
<td align="right"><span class="text-up">287,065 </span></td>
|
||||
<td align="right"><span class="text-up">150.0 B</span></td>
|
||||
<td align="right"><span class="text-up">5,226 </span></td>
|
||||
<td align="center">5</td>
|
||||
<td align="center"><span class="text-foreign">BK</span></td>
|
||||
<td align="right"><span class="text-down">419,266 </span></td>
|
||||
<td align="right"><span class="text-down">218.8 B</span></td>
|
||||
<td align="right"><span class="text-down">5,219 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-bumn">CC</span></td>
|
||||
<td align="right"><span class="text-up">198,140 </span></td>
|
||||
<td align="right"><span class="text-up">104.4 B</span></td>
|
||||
<td align="right"><span class="text-up">5,267 </span></td>
|
||||
<td align="center">6</td>
|
||||
<td align="center"><span class="text-foreign">YU</span></td>
|
||||
<td align="right"><span class="text-down">252,383 </span></td>
|
||||
<td align="right"><span class="text-down">131.3 B</span></td>
|
||||
<td align="right"><span class="text-down">5,203 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-foreign">RX</span></td>
|
||||
<td align="right"><span class="text-up">162,162 </span></td>
|
||||
<td align="right"><span class="text-up">97.1 B</span></td>
|
||||
<td align="right"><span class="text-up">5,987 </span></td>
|
||||
<td align="center">7</td>
|
||||
<td align="center"><span class="text-bumn">DX</span></td>
|
||||
<td align="right"><span class="text-down">252,100 </span></td>
|
||||
<td align="right"><span class="text-down">130.5 B</span></td>
|
||||
<td align="right"><span class="text-down">5,175 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">BB</span></td>
|
||||
<td align="right"><span class="text-up">123,966 </span></td>
|
||||
<td align="right"><span class="text-up">64.8 B</span></td>
|
||||
<td align="right"><span class="text-up">5,225 </span></td>
|
||||
<td align="center">8</td>
|
||||
<td align="center"><span class="text-foreign">KZ</span></td>
|
||||
<td align="right"><span class="text-down">115,380 </span></td>
|
||||
<td align="right"><span class="text-down">60.1 B</span></td>
|
||||
<td align="right"><span class="text-down">5,212 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-local">TP</span></td>
|
||||
<td align="right"><span class="text-up">70,621 </span></td>
|
||||
<td align="right"><span class="text-up">36.6 B</span></td>
|
||||
<td align="right"><span class="text-up">5,189 </span></td>
|
||||
<td align="center">9</td>
|
||||
<td align="center"><span class="text-local">BB</span></td>
|
||||
<td align="right"><span class="text-down">109,509 </span></td>
|
||||
<td align="right"><span class="text-down">56.9 B</span></td>
|
||||
<td align="right"><span class="text-down">5,200 </span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><span class="text-bumn">DX</span></td>
|
||||
<td align="right"><span class="text-up">15,000 </span></td>
|
||||
<td align="right"><span class="text-up">7.8 B</span></td>
|
||||
<td align="right"><span class="text-up">5,181 </span></td>
|
||||
<td align="center">10</td>
|
||||
<td align="center"><span class="text-local">TP</span></td>
|
||||
<td align="right"><span class="text-down">34,490 </span></td>
|
||||
<td align="right"><span class="text-down">17.9 B</span></td>
|
||||
<td align="right"><span class="text-down">5,195 </span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="bdr-t">
|
||||
<th colspan="9" align="center">
|
||||
<div class="text-center pt20 valign-middle" style="height:50px;">
|
||||
<span class="fsize12 ml5 mr5">T. Val : 1.7 T</span>
|
||||
<span class="fsize12 ml5 mr5">F. NVal : -2.3 T</span>
|
||||
<span class="fsize12 ml5 mr5">T.Lot : 3.2 M</span>
|
||||
<span class="fsize12 ml5 mr5">Avg : 5,263 </span>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- =======================
|
||||
======SCRIPT MODULE=========
|
||||
============================-->
|
||||
<script src="../../../../module/saham/include/js/data-brokersummary.js"></script>
|
||||
|
|
@ -175,8 +175,11 @@ for one or more IDX tickers."
|
|||
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."
|
||||
broker's own status (local, foreign, state_owned), not its clients'. With `investor` foreign \
|
||||
or domestic, each row is that broker's foreign or domestic clients' trades, `total_value` is \
|
||||
null, and `buy_value`/`sell_value` give what that side bought and sold (`sell_value` needs \
|
||||
`board` all). `rounded` = true means figures of a million and more carry one decimal of \
|
||||
precision."
|
||||
)]
|
||||
async fn get_broker_summary(&self, params: Parameters<BrokerParams>) -> ToolResult {
|
||||
let BrokerParams { symbol, scope } = params.0;
|
||||
|
|
@ -206,8 +209,9 @@ traded (`imbalance_bps`), with a `signal` from big_accumulation (>= +1500) throu
|
|||
(within +-500) to big_distribution (<= -1500), headlined by `top` (default 5). Reports come \
|
||||
strongest accumulation first; tickers that fail are listed under `failures`. A heuristic over \
|
||||
end-of-day broker codes: brokers trade for many clients, and `both_sides` lists brokers whose \
|
||||
gross figures overstate their direction. `method` names the rules applied. `investor` must be \
|
||||
`all`: the summary has no total for one investor side."
|
||||
gross figures overstate their direction. `method` names the rules applied. With `investor` \
|
||||
foreign or domestic, shares are of that side's own buying (`buy_value`) and selling \
|
||||
(`sell_value`); that needs `board` all."
|
||||
)]
|
||||
async fn analyze_bandar(&self, params: Parameters<BandarParams>) -> ToolResult {
|
||||
let BandarParams {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[
|
||||
{
|
||||
"description": "Bandarmology for one or more IDX tickers from the broker summary: for the top 1, 3, 5 and 10 brokers per side, buy value minus sell value as basis points of value traded (`imbalance_bps`), with a `signal` from big_accumulation (>= +1500) through neutral (within +-500) to big_distribution (<= -1500), headlined by `top` (default 5). Reports come strongest accumulation first; tickers that fail are listed under `failures`. A heuristic over end-of-day broker codes: brokers trade for many clients, and `both_sides` lists brokers whose gross figures overstate their direction. `method` names the rules applied. `investor` must be `all`: the summary has no total for one investor side.",
|
||||
"description": "Bandarmology for one or more IDX tickers from the broker summary: for the top 1, 3, 5 and 10 brokers per side, buy value minus sell value as basis points of value traded (`imbalance_bps`), with a `signal` from big_accumulation (>= +1500) through neutral (within +-500) to big_distribution (<= -1500), headlined by `top` (default 5). Reports come strongest accumulation first; tickers that fail are listed under `failures`. A heuristic over end-of-day broker codes: brokers trade for many clients, and `both_sides` lists brokers whose gross figures overstate their direction. `method` names the rules applied. With `investor` foreign or domestic, shares are of that side's own buying (`buy_value`) and selling (`sell_value`); that needs `board` all.",
|
||||
"inputSchema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
"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.",
|
||||
"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'. With `investor` foreign or domestic, each row is that broker's foreign or domestic clients' trades, `total_value` is null, and `buy_value`/`sell_value` give what that side bought and sold (`sell_value` needs `board` all). `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": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue