fix(core): symmetric bandar thresholds; bandar needs all investors

- Basis points truncate toward zero instead of flooring, so a reading of
  -4.9952% stays neutral like its mirror image and -14.9952% stays
  distribution; tests cover both signs at both thresholds through
  bandar_tier. Goldens move by the rounding (BBCA top 5: -2426 -> -2425).
- With one investor side selected the summary has no total to weigh
  brokers against, so bandar and analyze_bandar reject it (INVALIDINPUT)
  instead of reading every tier as missing.

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 05:39:05 +00:00
commit 4c2f0acff3
13 changed files with 73 additions and 18 deletions

View file

@ -31,6 +31,19 @@ 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 {
Ok(())
} else {
Err(IdxError::InvalidInput(format!(
"bandar needs --investor all: the broker summary has no total for {} investors alone",
investor.as_str()
)))
}
}
/// Bandar readings for several symbols, strongest accumulation first;
/// symbols that failed are listed with their error.
pub struct BandarScreen {
@ -57,6 +70,7 @@ impl Analysis<'_> {
policy: CachePolicy,
) -> Result<Fetched<BandarReport>, IdxError> {
let top = check_top(query.top)?;
check_investor(query.investor)?;
let summary =
self.idx
.broker()
@ -73,6 +87,7 @@ impl Analysis<'_> {
policy: CachePolicy,
) -> Result<BandarScreen, IdxError> {
check_top(query.top)?;
check_investor(query.investor)?;
let mut reports = Vec::new();
let mut failures = Vec::new();
for symbol in symbols {