fix(core): non-regular share is of all trading

IDX's Value is regular-board turnover, so NonRegularValue / Value gave
non-regular over regular (BBRI 2026-10-14: 50.91%) instead of the share of
all trading (33.74%). non_regular_bps now divides by regular plus
non-regular value, with a regression test on that fixture. The docs now say
volume, value and the foreign figures are the regular board: IPOT's
regular total matches Value, and in a week of every stock no foreign figure
exceeded regular volume, even with non-regular volume above twice it.

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:41:55 +00:00
commit 83783684cb
13 changed files with 50 additions and 27 deletions

View file

@ -184,10 +184,11 @@ fn flow_day(row: &StockDay) -> ForeignFlowDay {
foreign_sell: row.foreign_sell,
foreign_net,
foreign_net_value,
// `value` is the regular board only; the share is of all trading.
non_regular_bps: row
.non_regular_value
.zip(row.value)
.and_then(|(part, whole)| bps(part, whole)),
.and_then(|(part, regular)| bps(part, part.checked_add(regular)?)),
}
}
@ -255,7 +256,8 @@ mod tests {
);
// 400 shares at 1,100 (value over volume).
assert_eq!(first.foreign_net_value, Some(440_000));
assert_eq!(first.non_regular_bps, Some(1000));
// 110,000 of 1,100,000 regular plus 110,000 non-regular.
assert_eq!(first.non_regular_bps, Some(909));
assert_eq!(flow.days[1].change_bps, Some(-500));
assert_eq!(
(flow.foreign_net, flow.foreign_net_value),
@ -278,6 +280,17 @@ mod tests {
assert!(foreign_flow("TEST.JK", &[]).is_none());
}
#[test]
fn test_non_regular_share_is_of_all_trading() {
let raw = include_str!("../../tests/fixtures/idx_stocksummary_20261014.json");
let date = NaiveDate::from_ymd_opt(2026, 10, 14).unwrap();
let rows = crate::api::idx::parse_stock_summary(raw, date).unwrap();
let bbri: Vec<StockDay> = rows.into_iter().filter(|row| row.code == "BBRI").collect();
let flow = foreign_flow("BBRI.JK", &bbri).unwrap();
// 172.3B non-regular against 338.5B regular: 33.74%, not 50.91%.
assert_eq!(flow.days[0].non_regular_bps, Some(3374));
}
#[test]
fn test_check_range_bounds() {
let day = |d| NaiveDate::from_ymd_opt(2026, 10, d).unwrap();

View file

@ -10,7 +10,10 @@ use crate::error::IdxError;
pub const DEFAULT_SESSIONS: usize = 10;
/// One stock's trading summary for one session, as IDX publishes it.
/// Prices are whole IDR, volumes are shares.
/// Prices are whole IDR, volumes are shares. Volume, value and the foreign
/// figures cover the regular board only (across a week of every stock, no
/// foreign figure exceeded regular volume, even where non-regular volume was
/// more than twice it); the non-regular figures cover the rest.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StockDay {
/// Bare IDX code, e.g. `BBCA`.
@ -21,7 +24,7 @@ pub struct StockDay {
pub low: Option<i64>,
pub close: Option<i64>,
pub volume: Option<i64>,
/// Value traded, whole IDR, all boards.
/// Value traded on the regular board, whole IDR.
pub value: Option<i64>,
pub frequency: Option<i64>,
/// Shares bought by foreign investors.
@ -80,11 +83,11 @@ pub struct ForeignFlowDay {
pub close: Option<i64>,
/// Close against the previous close, basis points.
pub change_bps: Option<i64>,
/// Shares traded.
/// Shares traded on the regular board.
pub volume: Option<i64>,
/// Value traded, whole IDR.
/// Value traded on the regular board, whole IDR.
pub value: Option<i64>,
/// Shares foreign investors bought.
/// Shares foreign investors bought (regular board).
pub foreign_buy: Option<i64>,
/// Shares foreign investors sold.
pub foreign_sell: Option<i64>,
@ -94,7 +97,7 @@ pub struct ForeignFlowDay {
/// whole IDR: an estimate.
pub foreign_net_value: Option<i64>,
/// Value traded outside the regular board (negotiated and cash), basis
/// points of the session's value.
/// points of all value traded (regular plus non-regular).
pub non_regular_bps: Option<i64>,
}
@ -112,6 +115,6 @@ pub struct ForeignFlow {
/// Sum of the sessions' estimated net values, whole IDR.
pub foreign_net_value: Option<i64>,
/// Foreign buys and sells over twice the volume: foreign investors'
/// share of trading, basis points.
/// share of regular-board trading, basis points.
pub foreign_share_bps: Option<i64>,
}