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:
hermes 2026-10-18 06:08:02 +00:00
commit 5f64319157
23 changed files with 717 additions and 86 deletions

View file

@ -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 -"
)]

View file

@ -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.",

View file

@ -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}"
);
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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 {

View file

@ -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]

View file

@ -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.

View 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">&nbsp;</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>

View 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">&nbsp;</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>

View file

@ -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 {

View file

@ -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": {

View file

@ -95,10 +95,13 @@ WIB as the fallback.
sides), plus range totals. `api/ipot/parse.rs` parses it purely.
- Figures of a million and more arrive abbreviated (`1.6 M`); they are scaled
in decimal and the summary's `rounded` flag says so. `F. NVal` ignores the
investor and board filters, so `foreign_net_value` is null for a filtered
summary. With `fd=F` or `fd=D` the footer covers only that side's buying
(BBCA 2026-10-16 foreign: `T. Val` 1.6 T while seller CC alone sold
2.0 T), so totals are null for an investor-filtered summary.
investor and board filters, so `foreign_net_value` is null for a summary of
one board. With `fd=F` or `fd=D` the footer covers only that
side's buying (BBCA 2026-10-16: foreign 1.7 T and domestic 4.9 T of 6.6 T),
so `total_value` is null and the footer is `buy_value`. On all boards the
side's `sell_value` is its buying less the foreign net (foreign: 1.7 + 2.3
= 4.0 T, against IDX's 3.95 T regular) or plus it (domestic); with a board
filter it is null.
- An unknown code is a 200 with `Invalid stock code!` (`SYMBOLNOTFOUND`). The
server also answers spurious nginx 404s for seconds at a time, so the client
retries 404 and 5xx with backoff. A range without trades is an empty summary.
@ -106,8 +109,9 @@ WIB as the fallback.
ended before today are cached for 7 days, today's for 5 minutes. Broker
names come from `broker/codes.tsv` (IDX `GetBrokerCodeList`).
- Bandarmology (`analysis/bandar.rs`, method `top_concentration_v1`) is pure
over one summary: for the top 1/3/5/10 per side, buy value minus sell value
in basis points of value traded, with thresholds ±500/±1500 bps taken from
over one summary: for the top 1/3/5/10 per side, the buyers' share of the
value bought minus the sellers' share of the value sold (for all investors,
buy value minus sell value in basis points of value traded), with thresholds ±500/±1500 bps taken from
fzn0x/idx. A tier with a missing figure has no reading; missing values are
never summed as zero. A change to the rules gets a new method name.

View file

@ -356,7 +356,7 @@ Examples:
### `idx stocks bandar`
```text
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 heuristic over end-of-day broker codes, not advice.
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). `--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.
Usage: idx stocks bandar [OPTIONS] <SYMBOLS>...

View file

@ -39,8 +39,10 @@ idx stocks broker BBCA --from 2026-10-12 --to 2026-10-16 --board regular
Which brokers bought and sold a stock: the top 10 buyers and the top 10
sellers by lots, each with lots, value and average price, plus totals and the
net foreign value. Without dates it shows the latest session with trades.
`--investor foreign|domestic` counts only one side's clients, and
`--board regular` leaves out negotiated block trades.
`--investor foreign|domestic` counts only one side's clients (each broker row
is then that broker's foreign or domestic clients' trades, and the footer
gives what that side bought and sold), and `--board regular` leaves out
negotiated block trades.
The data comes from Indo Premier (IPOT) and is end of day: IDX hides broker
codes during trading. Buyers and sellers are ranked separately on gross
@ -87,8 +89,11 @@ traded. The headline tier (`--top`, default 5) gives the signal:
| -15% or less | big distribution |
Several symbols give a screen ranked by that figure (at most 100, one request
each), and it takes the same date and board flags as `broker` (all investors
only: the summary has no total for one side). It is a
each), and it takes the same date and filter flags as `broker`. With
`--investor foreign` (or `domestic`) it reads that side alone: the top buyers'
share of foreign buying against the top sellers' share of foreign selling.
That needs `--board all`, since IPOT gives one side's selling only across all
boards. It is a
heuristic: a broker trades for many clients, and a broker on both sides of
the summary is listed, since its gross figures overstate which way it traded.
The JSON names the rules in `method` (`top_concentration_v1`).

View file

@ -12,7 +12,7 @@ message}` with `isError` set.
## `analyze_bandar`
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.
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.
| Input | Type | Required | Description |
| --- | --- | --- | --- |
@ -43,7 +43,7 @@ Compare fundamental reports (growth, valuation, risk) across IDX tickers. Ticker
## `get_broker_summary`
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.
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.
| Input | Type | Required | Description |
| --- | --- | --- | --- |